Spaces:
Sleeping
Sleeping
Upload 45 files
Browse files- .gitattributes +5 -0
- data/forecasting/competitive_dynamics.csv +37 -0
- data/forecasting/economic_impact.csv +37 -0
- data/forecasting/event_log.json +6 -0
- data/forecasting/seasonal_usage.csv +37 -0
- data/forecasting/tech_adoption.csv +37 -0
- data/models/churn_model.pkl +3 -0
- data/models/churn_scaler.pkl +3 -0
- data/models/ltv_feature_importance.csv +86 -0
- data/models/ltv_model.pkl +3 -0
- data/models/ltv_scaler.pkl +3 -0
- data/models/model_metrics.json +12 -0
- data/processed/customer_scores.csv +0 -0
- data/processed/master_feature_table.csv +3 -0
- data/processed/priority_customers.csv +0 -0
- data/processed/tower_features.csv +0 -0
- data/synthetic/billing.csv +3 -0
- data/synthetic/churn_labels.csv +0 -0
- data/synthetic/customer_service.csv +0 -0
- data/synthetic/customer_usage.csv +3 -0
- data/synthetic/customers.csv +3 -0
- data/synthetic/network_infrastructure.csv +0 -0
- data/synthetic/network_performance.csv +3 -0
- data/synthetic/service_quality.csv +0 -0
- src/data_engineering/comprehensive_feature_engineering.py +502 -0
- src/forecasting/__init__.py +1 -0
- src/forecasting/__pycache__/__init__.cpython-313.pyc +0 -0
- src/forecasting/__pycache__/forecasting_engine.cpython-313.pyc +0 -0
- src/forecasting/forecasting_engine.py +427 -0
- src/forecasting/time_series_generator.py +309 -0
- src/utils/enhanced_data_generator.py +360 -0
- src/utils/synthetic_data_generator.py +601 -0
- templates/base.html +679 -0
- templates/churn.html +121 -0
- templates/customer.html +374 -0
- templates/executive.html +273 -0
- templates/financial.html +305 -0
- templates/forecasting.html +840 -0
- templates/geographic.html +234 -0
- templates/journey.html +283 -0
- templates/landing.html +966 -0
- templates/network.html +286 -0
- templates/network_enhanced.html +299 -0
- templates/predictions.html +906 -0
- templates/quality.html +254 -0
- templates/segmentation.html +236 -0
.gitattributes
CHANGED
|
@@ -35,3 +35,8 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
Final_Paper_Music_AVI_For_Relaxation\[1\].docx filter=lfs diff=lfs merge=lfs -text
|
| 37 |
TelecomIQ_Research_Paper.docx filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
Final_Paper_Music_AVI_For_Relaxation\[1\].docx filter=lfs diff=lfs merge=lfs -text
|
| 37 |
TelecomIQ_Research_Paper.docx filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
data/processed/master_feature_table.csv filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
data/synthetic/billing.csv filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
data/synthetic/customer_usage.csv filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
data/synthetic/customers.csv filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
data/synthetic/network_performance.csv filter=lfs diff=lfs merge=lfs -text
|
data/forecasting/competitive_dynamics.csv
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
date,our_market_share,competitor_a_share,competitor_b_share,competitor_c_share,pricing_war_intensity,our_avg_price,market_avg_price,competitive_churn_pct,net_subscriber_adds,competitor_promo_index
|
| 2 |
+
2023-01-31,31.77,28.52,21.76,17.94,0.0,53.73,55.03,1.28,1061,30.5
|
| 3 |
+
2023-02-28,33.22,27.84,22.16,16.78,0.0,54.61,54.75,0.89,1056,35.1
|
| 4 |
+
2023-03-31,33.83,28.51,21.87,15.79,0.0,54.59,54.61,1.25,1079,31.9
|
| 5 |
+
2023-04-30,33.26,28.07,22.49,16.18,0.0,54.56,54.71,1.2,1349,32.6
|
| 6 |
+
2023-05-31,33.18,27.67,21.82,17.34,0.0,53.89,53.38,1.26,1311,25.0
|
| 7 |
+
2023-06-30,32.64,26.91,21.93,18.52,0.0,53.01,53.14,1.1,1350,12.9
|
| 8 |
+
2023-07-31,33.04,27.31,21.87,17.78,0.0,53.94,51.74,1.36,1266,28.4
|
| 9 |
+
2023-08-31,31.84,26.36,21.78,20.03,0.0,52.98,53.84,1.06,1093,34.7
|
| 10 |
+
2023-09-30,31.33,27.09,22.26,19.32,1.0,48.89,46.91,2.18,1206,90.1
|
| 11 |
+
2023-10-31,31.39,27.14,21.98,19.5,0.8,49.52,47.61,1.78,881,68.0
|
| 12 |
+
2023-11-30,31.21,27.69,22.49,18.61,0.6,50.08,48.17,1.4,711,43.7
|
| 13 |
+
2023-12-31,32.08,28.19,22.23,17.51,0.4,51.62,48.67,1.44,1178,47.0
|
| 14 |
+
2024-01-31,32.6,28.8,21.77,16.83,0.2,52.2,50.68,1.63,1081,42.1
|
| 15 |
+
2024-02-29,33.97,28.69,22.04,15.29,0.0,51.86,51.86,0.95,1181,36.4
|
| 16 |
+
2024-03-31,34.13,28.47,22.27,15.13,0.0,51.27,52.46,1.04,1207,33.5
|
| 17 |
+
2024-04-30,34.81,28.27,22.61,14.32,0.0,52.67,51.76,1.25,1300,24.0
|
| 18 |
+
2024-05-31,34.9,27.41,22.19,15.5,0.0,52.92,52.13,1.05,1065,20.0
|
| 19 |
+
2024-06-30,34.07,26.98,22.26,16.69,0.0,51.21,51.24,1.13,1159,11.7
|
| 20 |
+
2024-07-31,33.69,26.46,21.97,17.88,0.0,51.52,50.29,1.08,1200,38.5
|
| 21 |
+
2024-08-31,32.41,26.53,22.68,18.38,0.0,50.69,52.54,1.3,1369,35.1
|
| 22 |
+
2024-09-30,31.94,26.82,21.84,19.4,0.0,50.64,49.9,1.16,1027,38.3
|
| 23 |
+
2024-10-31,32.25,26.72,22.48,18.56,0.0,52.25,49.78,1.38,1060,27.9
|
| 24 |
+
2024-11-30,32.4,27.08,22.43,18.09,0.0,50.79,49.98,0.95,1332,22.4
|
| 25 |
+
2024-12-31,32.67,27.26,22.53,17.55,0.0,50.42,49.93,1.41,1264,25.9
|
| 26 |
+
2025-01-31,33.78,28.19,22.63,15.4,1.0,45.45,42.8,2.12,924,66.7
|
| 27 |
+
2025-02-28,35.11,27.8,22.76,14.33,0.8,45.52,43.89,1.87,873,82.1
|
| 28 |
+
2025-03-31,35.17,27.84,22.52,14.47,0.6,47.09,44.61,1.74,1219,68.0
|
| 29 |
+
2025-04-30,35.67,27.29,22.1,14.94,0.4,48.6,46.78,1.44,1124,55.4
|
| 30 |
+
2025-05-31,35.42,27.06,22.95,14.57,0.2,49.16,49.56,1.21,1194,33.3
|
| 31 |
+
2025-06-30,35.13,25.93,22.42,16.53,0.0,48.94,49.49,1.46,1237,13.6
|
| 32 |
+
2025-07-31,34.5,26.17,22.82,16.51,0.0,48.54,49.17,1.18,1214,23.8
|
| 33 |
+
2025-08-31,33.91,25.78,22.54,17.78,0.0,49.13,49.37,1.02,1300,44.7
|
| 34 |
+
2025-09-30,33.42,26.4,22.82,17.36,0.0,48.61,47.88,1.26,1110,42.8
|
| 35 |
+
2025-10-31,33.39,26.48,22.64,17.49,0.0,48.33,50.64,1.23,800,28.6
|
| 36 |
+
2025-11-30,33.55,26.52,22.57,17.37,0.0,47.81,49.41,1.32,1102,24.6
|
| 37 |
+
2025-12-31,34.11,26.75,22.91,16.23,0.0,48.4,47.54,1.13,1165,18.3
|
data/forecasting/economic_impact.csv
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
date,gdp_growth_rate,consumer_confidence_index,unemployment_rate,recession_indicator,arpu_index,plan_downgrade_rate,payment_delinquency_rate,new_subscription_rate,revenue_at_risk_millions,customer_sentiment_index
|
| 2 |
+
2023-01-31,2.2,72.2,4.1,0,101.2,3.26,3.68,5.66,12.65,62.1
|
| 3 |
+
2023-02-28,2.39,74.5,4.2,0,99.8,3.18,3.21,5.51,13.49,65.0
|
| 4 |
+
2023-03-31,2.4,73.3,4.2,0,102.3,3.64,2.95,5.64,12.4,63.7
|
| 5 |
+
2023-04-30,2.47,73.7,4.2,0,100.9,3.01,2.89,5.32,14.03,65.4
|
| 6 |
+
2023-05-31,2.27,73.7,3.7,0,100.4,3.39,3.48,5.63,15.83,61.8
|
| 7 |
+
2023-06-30,1.96,72.7,4.3,0,101.8,3.48,4.33,4.76,18.2,61.1
|
| 8 |
+
2023-07-31,2.24,71.9,4.4,0,99.5,3.38,3.89,5.38,15.09,60.8
|
| 9 |
+
2023-08-31,1.92,70.3,4.6,0,98.3,3.29,4.77,4.8,14.48,59.5
|
| 10 |
+
2023-09-30,2.0,71.0,4.2,0,100.3,3.56,3.87,5.26,13.96,59.8
|
| 11 |
+
2023-10-31,2.04,72.0,4.1,0,99.3,3.46,4.3,4.92,13.69,61.8
|
| 12 |
+
2023-11-30,1.94,69.6,4.1,0,99.0,3.82,3.7,4.63,16.18,60.2
|
| 13 |
+
2023-12-31,2.09,72.0,4.1,0,100.1,3.35,4.25,5.12,14.95,60.6
|
| 14 |
+
2024-01-31,1.9,73.2,4.2,0,100.0,3.88,4.15,5.18,14.93,61.3
|
| 15 |
+
2024-02-29,1.87,69.3,4.4,0,99.4,4.02,4.63,4.75,14.92,60.5
|
| 16 |
+
2024-03-31,1.11,67.2,4.6,1,97.8,4.28,5.09,4.21,16.29,51.2
|
| 17 |
+
2024-04-30,0.93,69.3,4.6,1,95.7,4.35,4.32,3.59,16.42,51.0
|
| 18 |
+
2024-05-31,0.65,64.4,4.8,1,97.0,4.59,5.97,2.89,18.17,49.9
|
| 19 |
+
2024-06-30,1.2,66.7,4.6,1,96.2,4.35,5.27,3.79,17.31,57.7
|
| 20 |
+
2024-07-31,1.58,70.3,4.3,0,98.9,3.78,3.83,4.52,15.08,59.2
|
| 21 |
+
2024-08-31,1.77,70.8,4.0,0,100.7,4.04,3.89,5.02,15.0,57.3
|
| 22 |
+
2024-09-30,2.31,74.8,4.0,0,100.5,3.6,3.89,6.12,15.16,60.6
|
| 23 |
+
2024-10-31,1.69,68.1,4.2,0,99.9,3.77,4.59,4.89,15.19,60.3
|
| 24 |
+
2024-11-30,1.85,71.3,4.2,0,98.7,3.5,4.87,5.04,15.85,59.0
|
| 25 |
+
2024-12-31,2.1,72.7,3.9,0,99.2,3.82,4.4,5.39,12.42,58.6
|
| 26 |
+
2025-01-31,1.97,71.7,4.3,0,98.4,3.24,4.04,5.31,13.26,58.3
|
| 27 |
+
2025-02-28,2.6,73.9,4.0,0,101.4,2.88,2.64,5.51,13.01,66.7
|
| 28 |
+
2025-03-31,2.38,74.2,3.5,0,102.5,2.87,3.46,5.87,15.12,64.7
|
| 29 |
+
2025-04-30,2.6,76.8,4.1,0,102.1,2.89,3.24,5.89,13.22,64.5
|
| 30 |
+
2025-05-31,2.8,76.1,3.8,0,102.9,3.24,3.06,6.55,14.15,64.5
|
| 31 |
+
2025-06-30,2.1,72.8,4.2,0,102.7,3.58,4.49,5.62,15.42,59.5
|
| 32 |
+
2025-07-31,2.13,73.2,4.3,0,100.6,3.46,3.47,5.32,15.78,60.2
|
| 33 |
+
2025-08-31,2.0,71.7,4.3,0,99.8,3.64,4.36,4.64,17.35,57.3
|
| 34 |
+
2025-09-30,1.8,70.0,4.1,0,99.6,4.04,5.08,4.45,16.34,58.0
|
| 35 |
+
2025-10-31,2.07,72.8,4.5,0,101.0,3.32,3.66,4.73,14.5,58.8
|
| 36 |
+
2025-11-30,1.91,73.8,4.1,0,99.8,3.71,3.87,5.25,15.4,58.9
|
| 37 |
+
2025-12-31,2.24,74.2,4.0,0,101.0,3.47,4.06,5.37,14.8,59.6
|
data/forecasting/event_log.json
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"2025-12-31": "Major Sports Final",
|
| 3 |
+
"2024-02-29": "Product Launch Event",
|
| 4 |
+
"2025-03-31": "Music Festival",
|
| 5 |
+
"2025-07-31": "Emergency Weather"
|
| 6 |
+
}
|
data/forecasting/seasonal_usage.csv
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
date,avg_data_usage_gb,avg_voice_minutes,avg_sms_count,network_load_factor,peak_concurrent_users,is_holiday_month,event_spike_magnitude
|
| 2 |
+
2023-01-31,7.64,312.2,176,0.5,44927,0,0.0
|
| 3 |
+
2023-02-28,8.49,321.5,187,0.592,51890,1,0.0
|
| 4 |
+
2023-03-31,8.06,333.6,181,0.599,49680,0,0.0
|
| 5 |
+
2023-04-30,9.55,332.4,169,0.602,49868,0,0.0
|
| 6 |
+
2023-05-31,9.12,328.5,162,0.567,50852,0,0.0
|
| 7 |
+
2023-06-30,10.66,318.4,156,0.579,50553,0,0.0
|
| 8 |
+
2023-07-31,12.08,311.3,164,0.628,56260,1,0.0
|
| 9 |
+
2023-08-31,10.31,299.7,171,0.576,48987,0,0.0
|
| 10 |
+
2023-09-30,11.3,290.2,165,0.609,52128,1,0.0
|
| 11 |
+
2023-10-31,10.03,292.8,154,0.584,49751,0,0.0
|
| 12 |
+
2023-11-30,11.16,285.8,147,0.625,58477,1,0.0
|
| 13 |
+
2023-12-31,11.34,286.7,146,0.595,65384,1,0.0
|
| 14 |
+
2024-01-31,9.36,302.1,145,0.566,54107,0,0.0
|
| 15 |
+
2024-02-29,12.62,307.7,150,0.629,80274,1,2.43
|
| 16 |
+
2024-03-31,9.69,297.1,151,0.618,57358,0,0.0
|
| 17 |
+
2024-04-30,10.75,308.2,138,0.596,59598,0,0.0
|
| 18 |
+
2024-05-31,11.48,290.8,138,0.596,60396,0,0.0
|
| 19 |
+
2024-06-30,12.23,298.8,129,0.633,59908,0,0.0
|
| 20 |
+
2024-07-31,13.68,279.3,131,0.651,65169,1,0.0
|
| 21 |
+
2024-08-31,12.89,278.4,136,0.606,58144,0,0.0
|
| 22 |
+
2024-09-30,13.05,263.9,134,0.617,61836,1,0.0
|
| 23 |
+
2024-10-31,11.75,264.7,124,0.631,59279,0,0.0
|
| 24 |
+
2024-11-30,12.88,256.1,110,0.634,69211,1,0.0
|
| 25 |
+
2024-12-31,13.93,265.1,118,0.591,73457,1,0.0
|
| 26 |
+
2025-01-31,11.31,271.7,118,0.612,65337,0,0.0
|
| 27 |
+
2025-02-28,13.09,282.3,123,0.646,71073,1,0.0
|
| 28 |
+
2025-03-31,14.14,290.0,123,0.626,85257,0,2.07
|
| 29 |
+
2025-04-30,13.24,283.9,107,0.644,68851,0,0.0
|
| 30 |
+
2025-05-31,13.64,278.5,99,0.673,69992,0,0.0
|
| 31 |
+
2025-06-30,14.12,264.4,100,0.651,69907,0,0.0
|
| 32 |
+
2025-07-31,18.14,252.9,103,0.687,98728,1,2.97
|
| 33 |
+
2025-08-31,14.63,251.5,108,0.639,68736,0,0.0
|
| 34 |
+
2025-09-30,14.48,243.7,104,0.648,71652,1,0.0
|
| 35 |
+
2025-10-31,13.53,242.0,94,0.642,67728,0,0.0
|
| 36 |
+
2025-11-30,15.03,242.5,90,0.657,79027,1,0.0
|
| 37 |
+
2025-12-31,17.01,244.3,86,0.694,97284,1,1.64
|
data/forecasting/tech_adoption.csv
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
date,five_g_adoption_pct,four_g_pct,three_g_pct,five_g_towers_cumulative,avg_5g_speed_mbps,monthly_migration_rate,five_g_revenue_premium_pct
|
| 2 |
+
2023-01-31,2.0,72.59,25.41,26,229.5,0.13,12.8
|
| 3 |
+
2023-02-28,2.0,74.32,23.68,43,262.3,0.02,13.9
|
| 4 |
+
2023-03-31,2.0,73.28,24.72,63,258.5,0.0,12.6
|
| 5 |
+
2023-04-30,2.0,74.38,23.62,80,259.2,0.0,12.0
|
| 6 |
+
2023-05-31,3.54,73.16,23.3,109,262.5,1.54,14.6
|
| 7 |
+
2023-06-30,3.19,73.3,23.51,135,282.5,0.12,14.4
|
| 8 |
+
2023-07-31,4.73,72.14,23.13,174,257.0,1.79,10.1
|
| 9 |
+
2023-08-31,6.92,71.35,21.74,226,286.1,2.05,11.6
|
| 10 |
+
2023-09-30,7.49,69.51,22.99,279,299.3,0.48,14.3
|
| 11 |
+
2023-10-31,7.53,69.49,22.97,332,287.9,0.18,14.0
|
| 12 |
+
2023-11-30,9.26,68.26,22.48,399,298.5,1.62,14.3
|
| 13 |
+
2023-12-31,11.84,67.27,20.89,479,301.4,2.44,16.5
|
| 14 |
+
2024-01-31,12.37,65.79,21.84,566,311.6,0.74,18.4
|
| 15 |
+
2024-02-29,15.54,64.74,19.71,662,344.5,3.15,14.8
|
| 16 |
+
2024-03-31,19.45,61.39,19.16,792,324.4,3.88,17.3
|
| 17 |
+
2024-04-30,22.05,59.2,18.75,930,366.4,2.55,18.1
|
| 18 |
+
2024-05-31,27.01,55.76,17.23,1097,386.7,5.02,19.8
|
| 19 |
+
2024-06-30,28.44,54.91,16.65,1276,402.1,1.32,18.0
|
| 20 |
+
2024-07-31,31.66,52.34,16.0,1470,447.1,3.4,21.5
|
| 21 |
+
2024-08-31,35.35,50.78,13.87,1692,433.6,3.76,21.8
|
| 22 |
+
2024-09-30,39.55,48.05,12.41,1936,485.3,4.19,26.0
|
| 23 |
+
2024-10-31,42.72,45.9,11.38,2201,496.6,3.12,24.4
|
| 24 |
+
2024-11-30,47.73,41.64,10.63,2492,507.4,5.02,24.2
|
| 25 |
+
2024-12-31,48.45,41.61,9.94,2790,512.2,0.82,23.2
|
| 26 |
+
2025-01-31,51.34,39.46,9.2,3106,529.2,3.15,26.1
|
| 27 |
+
2025-02-28,54.23,37.8,7.97,3439,529.6,3.04,25.9
|
| 28 |
+
2025-03-31,55.37,36.89,7.74,3784,557.6,1.24,26.7
|
| 29 |
+
2025-04-30,57.56,35.2,7.24,4139,550.7,2.19,28.5
|
| 30 |
+
2025-05-31,58.83,33.34,7.83,4501,567.3,1.4,28.6
|
| 31 |
+
2025-06-30,60.36,33.13,6.51,4866,542.1,1.5,29.0
|
| 32 |
+
2025-07-31,59.54,33.83,6.62,5232,557.7,0.03,30.6
|
| 33 |
+
2025-08-31,62.13,31.64,6.23,5616,592.5,2.7,29.8
|
| 34 |
+
2025-09-30,62.03,31.92,6.05,5994,567.2,0.06,33.2
|
| 35 |
+
2025-10-31,62.84,30.85,6.31,6378,593.4,0.74,29.6
|
| 36 |
+
2025-11-30,62.82,31.35,5.83,6754,580.7,0.0,28.8
|
| 37 |
+
2025-12-31,64.82,30.38,4.8,7147,613.9,2.05,27.7
|
data/models/churn_model.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:db99e87da522d914ad1217378db0dce3dc1701acaa79fc05b3763f238aa770ab
|
| 3 |
+
size 3919
|
data/models/churn_scaler.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:87f43be0390ff00ea3ccfcb21d6d68b03262227012b596172b71255bdc6a03b0
|
| 3 |
+
size 5047
|
data/models/ltv_feature_importance.csv
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
feature,importance
|
| 2 |
+
tenure_months_y,0.3239845925799207
|
| 3 |
+
tenure_months_x,0.2746354368606078
|
| 4 |
+
total_revenue_x,0.11821116795284461
|
| 5 |
+
total_revenue_y,0.10613986358587005
|
| 6 |
+
avg_monthly_revenue,0.07955488882238393
|
| 7 |
+
arpu,0.06060366630593919
|
| 8 |
+
avg_bill_amount,0.03575068489217384
|
| 9 |
+
monthly_plan_cost_x,0.0010553440212846797
|
| 10 |
+
billing_periods,6.11951500841308e-05
|
| 11 |
+
monthly_plan_cost_y,1.503190273801748e-06
|
| 12 |
+
max_bill_amount,1.1610729370107107e-06
|
| 13 |
+
total_overage,9.957284142281584e-08
|
| 14 |
+
total_roaming_min,7.750413649937797e-08
|
| 15 |
+
zip_code,5.7786708347528234e-08
|
| 16 |
+
churn_probability,4.061068121355601e-08
|
| 17 |
+
total_data_gb,2.947902544001635e-08
|
| 18 |
+
avg_download_speed,2.8109555576044805e-08
|
| 19 |
+
avg_connection_time,2.4021565708949185e-08
|
| 20 |
+
avg_payment_days_y,1.994957172798459e-08
|
| 21 |
+
number_of_lines,1.772177512006317e-08
|
| 22 |
+
recent_billing_trend,1.491495097830759e-08
|
| 23 |
+
avg_daily_voice_min,1.0538906796906579e-08
|
| 24 |
+
total_roaming,9.90533625691273e-09
|
| 25 |
+
credit_score,9.900271804619213e-09
|
| 26 |
+
data_overage_charge,7.404756708304807e-09
|
| 27 |
+
device_age_months,7.038819536253136e-09
|
| 28 |
+
voice_overage_charge,6.919037911835877e-09
|
| 29 |
+
avg_payment_days_x,5.5646976363447545e-09
|
| 30 |
+
total_data_overage,4.786389142911716e-09
|
| 31 |
+
days_to_contract_end,4.375566894235601e-09
|
| 32 |
+
age,3.739178113966209e-09
|
| 33 |
+
total_sms,3.2350104540719638e-09
|
| 34 |
+
complaint_Network Coverage,2.4177621004772584e-09
|
| 35 |
+
total_voice_min,2.380791562846248e-09
|
| 36 |
+
quality_events_count,2.311986220158468e-09
|
| 37 |
+
revenue_std,2.1835676757720286e-09
|
| 38 |
+
has_no_contract,9.573798737810243e-10
|
| 39 |
+
revenue_volatility,6.224266133248666e-10
|
| 40 |
+
churn_risk_score,6.195425982884673e-10
|
| 41 |
+
sentiment_negative,3.156751510579822e-10
|
| 42 |
+
roaming_charges,2.6046560982283706e-10
|
| 43 |
+
avg_upload_speed,2.1270464288105915e-10
|
| 44 |
+
avg_mos_score,1.726574515816164e-10
|
| 45 |
+
call_drops,3.192354309687541e-11
|
| 46 |
+
service_call_count,1.4020770451352543e-14
|
| 47 |
+
months_with_data_overage,0.0
|
| 48 |
+
unresolved_rate,0.0
|
| 49 |
+
months_with_roaming,0.0
|
| 50 |
+
complaint_Billing Issue,0.0
|
| 51 |
+
avg_satisfaction,0.0
|
| 52 |
+
total_service_calls,0.0
|
| 53 |
+
complaint_Call Drops,0.0
|
| 54 |
+
avg_csat_score,0.0
|
| 55 |
+
total_escalations,0.0
|
| 56 |
+
late_payments,0.0
|
| 57 |
+
service_calls_last_30_days,0.0
|
| 58 |
+
avg_resolution_time,0.0
|
| 59 |
+
escalation_count,0.0
|
| 60 |
+
resolution_rate,0.0
|
| 61 |
+
total_voice_overage,0.0
|
| 62 |
+
referral_count,0.0
|
| 63 |
+
complaint_Connectivity,0.0
|
| 64 |
+
video_quality_Poor,0.0
|
| 65 |
+
complaint_Customer Service,0.0
|
| 66 |
+
contract_expiring_soon,0.0
|
| 67 |
+
sentiment_positive,0.0
|
| 68 |
+
avg_daily_data_gb,0.0
|
| 69 |
+
data_volatility,0.0
|
| 70 |
+
max_daily_data_gb,0.0
|
| 71 |
+
complaint_Technical Support,0.0
|
| 72 |
+
complaint_Slow Data Speed,0.0
|
| 73 |
+
voice_volatility,0.0
|
| 74 |
+
max_daily_voice_min,0.0
|
| 75 |
+
complaint_Roaming Charges,0.0
|
| 76 |
+
complaint_Plan Change,0.0
|
| 77 |
+
total_intl_min,0.0
|
| 78 |
+
usage_days_tracked,0.0
|
| 79 |
+
avg_jitter,0.0
|
| 80 |
+
total_buffering_events,0.0
|
| 81 |
+
complaint_Device Problem,0.0
|
| 82 |
+
video_quality_Excellent,0.0
|
| 83 |
+
video_quality_Fair,0.0
|
| 84 |
+
video_quality_Good,0.0
|
| 85 |
+
sentiment_neutral,0.0
|
| 86 |
+
avg_daily_sms,0.0
|
data/models/ltv_model.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c0b95b9b3299b4f5bf1acb6b60d70908a65492490b4a283c8b51f591d8131b9d
|
| 3 |
+
size 643852
|
data/models/ltv_scaler.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d3f41a09d9a6a60eb2a062a4bec3ad4e88b8e51cbd5ae150d801ce16007e901e
|
| 3 |
+
size 4991
|
data/models/model_metrics.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"churn": {
|
| 3 |
+
"accuracy": 0.59505,
|
| 4 |
+
"f1_score": 0.4373740882250782,
|
| 5 |
+
"roc_auc": 0.6429282593774956
|
| 6 |
+
},
|
| 7 |
+
"ltv": {
|
| 8 |
+
"rmse": 14.478468766152435,
|
| 9 |
+
"mae": 9.733511085249125,
|
| 10 |
+
"r2": 0.9998870329351713
|
| 11 |
+
}
|
| 12 |
+
}
|
data/processed/customer_scores.csv
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
data/processed/master_feature_table.csv
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3dcace9f4a8247ad6e5faeb03e1b87e733d1e42d18ff1da2e7cd75db8d1b1366
|
| 3 |
+
size 63371339
|
data/processed/priority_customers.csv
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
data/processed/tower_features.csv
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
data/synthetic/billing.csv
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:922c34387dbf121a265601a8e18ce2320a75e86a904058ed8f7bec73b01b05d9
|
| 3 |
+
size 83221828
|
data/synthetic/churn_labels.csv
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
data/synthetic/customer_service.csv
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
data/synthetic/customer_usage.csv
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c1b51194ddf338f1843c9b45d5950e9505b5fd7696bddc8b062ab00bddc64dd0
|
| 3 |
+
size 82657388
|
data/synthetic/customers.csv
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0e37203117ecfca22b5ff32195a030577d753679d88eac0e2256a47cfcca4e06
|
| 3 |
+
size 12350742
|
data/synthetic/network_infrastructure.csv
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
data/synthetic/network_performance.csv
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ad77095328f1e046f75dfd25e0483f97d210240883a745135060add6d25b3fbf
|
| 3 |
+
size 29380027
|
data/synthetic/service_quality.csv
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
src/data_engineering/comprehensive_feature_engineering.py
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Comprehensive Feature Engineering Pipeline
|
| 3 |
+
Implements ALL transformation requirements from Technical Specification
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import pandas as pd
|
| 7 |
+
import numpy as np
|
| 8 |
+
from datetime import datetime, timedelta
|
| 9 |
+
import warnings
|
| 10 |
+
warnings.filterwarnings('ignore')
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class ComprehensiveFeatureEngineer:
|
| 14 |
+
"""
|
| 15 |
+
Implements complete feature engineering per technical requirements:
|
| 16 |
+
- Customer Journey Analytics
|
| 17 |
+
- Network Performance Indicators
|
| 18 |
+
- Service Quality Features
|
| 19 |
+
- Customer Behavior Patterns
|
| 20 |
+
- Churn Risk Indicators
|
| 21 |
+
- Geographic and Temporal Features
|
| 22 |
+
- Financial Performance Metrics
|
| 23 |
+
- Competitive Intelligence Integration
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
def __init__(self):
|
| 27 |
+
print("Initializing Comprehensive Feature Engineering Pipeline...")
|
| 28 |
+
self.features_df = None
|
| 29 |
+
|
| 30 |
+
def load_data(self):
|
| 31 |
+
"""Load all data sources"""
|
| 32 |
+
print("\n📂 Loading All Data Sources...")
|
| 33 |
+
|
| 34 |
+
data = {}
|
| 35 |
+
|
| 36 |
+
# Core data
|
| 37 |
+
data['customers'] = pd.read_csv('data/synthetic/customers.csv')
|
| 38 |
+
data['billing'] = pd.read_csv('data/synthetic/billing.csv')
|
| 39 |
+
data['churn'] = pd.read_csv('data/synthetic/churn_labels.csv')
|
| 40 |
+
data['service'] = pd.read_csv('data/synthetic/customer_service.csv')
|
| 41 |
+
data['quality'] = pd.read_csv('data/synthetic/service_quality.csv')
|
| 42 |
+
data['network'] = pd.read_csv('data/synthetic/network_performance.csv')
|
| 43 |
+
data['towers'] = pd.read_csv('data/synthetic/network_infrastructure.csv')
|
| 44 |
+
data['usage'] = pd.read_csv('data/synthetic/customer_usage.csv')
|
| 45 |
+
|
| 46 |
+
# Enhanced data (if exists)
|
| 47 |
+
try:
|
| 48 |
+
data['device'] = pd.read_csv('data/synthetic/device_data.csv')
|
| 49 |
+
data['journey'] = pd.read_csv('data/synthetic/customer_journey.csv')
|
| 50 |
+
data['competitive'] = pd.read_csv('data/synthetic/competitive_intelligence.csv')
|
| 51 |
+
data['weather'] = pd.read_csv('data/synthetic/weather_data.csv')
|
| 52 |
+
data['demographics'] = pd.read_csv('data/synthetic/demographics_data.csv')
|
| 53 |
+
print(" ✅ Enhanced datasets loaded")
|
| 54 |
+
except FileNotFoundError:
|
| 55 |
+
print(" ⚠ Enhanced datasets not found - run generate_enhanced_data.py first")
|
| 56 |
+
data['device'] = None
|
| 57 |
+
data['journey'] = None
|
| 58 |
+
|
| 59 |
+
print(f" ✅ Loaded {len(data['customers']):,} customers")
|
| 60 |
+
|
| 61 |
+
return data
|
| 62 |
+
|
| 63 |
+
def customer_journey_analytics(self, data):
|
| 64 |
+
"""
|
| 65 |
+
Calculate customer lifetime value and lifecycle patterns
|
| 66 |
+
Track customer service interaction history
|
| 67 |
+
Analyze payment behavior
|
| 68 |
+
Create customer segmentation
|
| 69 |
+
"""
|
| 70 |
+
print("\n👤 Customer Journey Analytics...")
|
| 71 |
+
|
| 72 |
+
customers = data['customers'].copy()
|
| 73 |
+
billing = data['billing']
|
| 74 |
+
service = data['service']
|
| 75 |
+
|
| 76 |
+
# Calculate CLV
|
| 77 |
+
customer_revenue = billing.groupby('customer_id')['total_amount'].agg([
|
| 78 |
+
('total_revenue', 'sum'),
|
| 79 |
+
('avg_monthly_revenue', 'mean'),
|
| 80 |
+
('revenue_std', 'std'),
|
| 81 |
+
('billing_cycles', 'count')
|
| 82 |
+
]).reset_index()
|
| 83 |
+
|
| 84 |
+
# Service interaction patterns
|
| 85 |
+
service_stats = service.groupby('customer_id').agg({
|
| 86 |
+
'interaction_id': 'count',
|
| 87 |
+
'was_resolved': 'mean',
|
| 88 |
+
'was_escalated': 'mean',
|
| 89 |
+
'customer_satisfaction_score': ['mean', 'std', 'min'],
|
| 90 |
+
'resolution_time_minutes': 'mean'
|
| 91 |
+
}).reset_index()
|
| 92 |
+
|
| 93 |
+
service_stats.columns = ['customer_id', 'total_service_calls', 'resolution_rate',
|
| 94 |
+
'escalation_rate', 'avg_csat', 'csat_volatility',
|
| 95 |
+
'min_csat', 'avg_resolution_time']
|
| 96 |
+
|
| 97 |
+
# Merge journey data
|
| 98 |
+
customers = customers.merge(customer_revenue, on='customer_id', how='left')
|
| 99 |
+
customers = customers.merge(service_stats, on='customer_id', how='left')
|
| 100 |
+
|
| 101 |
+
# Fill NaNs
|
| 102 |
+
customers['total_service_calls'] = customers['total_service_calls'].fillna(0)
|
| 103 |
+
customers['avg_csat'] = customers['avg_csat'].fillna(7.0)
|
| 104 |
+
|
| 105 |
+
# Calculate ARPU
|
| 106 |
+
customers['arpu'] = customers['total_revenue'] / customers['tenure_months'].clip(lower=1)
|
| 107 |
+
|
| 108 |
+
# Customer lifecycle stage
|
| 109 |
+
customers['lifecycle_stage'] = pd.cut(
|
| 110 |
+
customers['tenure_months'],
|
| 111 |
+
bins=[0, 3, 12, 36, 1000],
|
| 112 |
+
labels=['New', 'Growing', 'Mature', 'Tenured']
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
# Payment behavior score
|
| 116 |
+
customers['payment_score'] = (
|
| 117 |
+
(customers.get('autopay_enabled', 0).astype(int) * 3) +
|
| 118 |
+
(customers.get('paperless_billing', 0).astype(int) * 2) +
|
| 119 |
+
((10 - customers.get('late_payments', 0).clip(upper=10)) / 2)
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
print(f" ✅ Generated {len(customers.columns)} journey features")
|
| 123 |
+
|
| 124 |
+
return customers
|
| 125 |
+
|
| 126 |
+
def network_performance_indicators(self, data):
|
| 127 |
+
"""
|
| 128 |
+
Cell tower load balancing and capacity utilization
|
| 129 |
+
Signal quality metrics
|
| 130 |
+
Network availability calculations
|
| 131 |
+
Coverage analysis
|
| 132 |
+
"""
|
| 133 |
+
print("\n📡 Network Performance Indicators...")
|
| 134 |
+
|
| 135 |
+
network = data['network']
|
| 136 |
+
towers = data['towers']
|
| 137 |
+
|
| 138 |
+
# Tower-level aggregations
|
| 139 |
+
tower_metrics = network.groupby('tower_id').agg({
|
| 140 |
+
'bandwidth_utilization_pct': ['mean', 'max', 'std'],
|
| 141 |
+
'latency_ms': ['mean', 'p95', 'max'],
|
| 142 |
+
'packet_loss_pct': ['mean', 'max'],
|
| 143 |
+
'throughput_mbps': ['mean', 'min'],
|
| 144 |
+
'availability_pct': 'mean',
|
| 145 |
+
'active_users': ['mean', 'max'],
|
| 146 |
+
'handover_success_rate_pct': 'mean',
|
| 147 |
+
'call_setup_success_rate_pct': 'mean'
|
| 148 |
+
}).reset_index()
|
| 149 |
+
|
| 150 |
+
# Flatten column names
|
| 151 |
+
tower_metrics.columns = ['_'.join(col).strip('_') for col in tower_metrics.columns.values]
|
| 152 |
+
|
| 153 |
+
# Merge with tower data
|
| 154 |
+
tower_features = towers.merge(tower_metrics, on='tower_id', how='left')
|
| 155 |
+
|
| 156 |
+
# Calculate efficiency metrics
|
| 157 |
+
tower_features['capacity_efficiency'] = (
|
| 158 |
+
tower_features['bandwidth_utilization_pct_mean'] /
|
| 159 |
+
tower_features['max_capacity_mbps']
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
tower_features['quality_score'] = (
|
| 163 |
+
(tower_features['availability_pct_mean'] / 100) * 0.4 +
|
| 164 |
+
(tower_features['handover_success_rate_pct_mean'] / 100) * 0.3 +
|
| 165 |
+
(tower_features['call_setup_success_rate_pct_mean'] / 100) * 0.3
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
print(f" ✅ Generated {len(tower_features.columns)} network features")
|
| 169 |
+
|
| 170 |
+
return tower_features
|
| 171 |
+
|
| 172 |
+
def service_quality_features(self, data):
|
| 173 |
+
"""
|
| 174 |
+
Customer-reported vs. network-measured quality correlation
|
| 175 |
+
Speed test results by location and device
|
| 176 |
+
Call drop patterns
|
| 177 |
+
Video streaming quality indicators
|
| 178 |
+
"""
|
| 179 |
+
print("\n⚡ Service Quality Features...")
|
| 180 |
+
|
| 181 |
+
quality = data['quality']
|
| 182 |
+
customers = data['customers']
|
| 183 |
+
|
| 184 |
+
# Customer-level quality metrics
|
| 185 |
+
quality_metrics = quality.groupby('customer_id').agg({
|
| 186 |
+
'call_drop_occurred': 'sum',
|
| 187 |
+
'download_speed_mbps': ['mean', 'std', 'min'],
|
| 188 |
+
'upload_speed_mbps': ['mean', 'min'],
|
| 189 |
+
'mos_score': 'mean',
|
| 190 |
+
'jitter_ms': 'mean',
|
| 191 |
+
'buffering_events': 'sum',
|
| 192 |
+
'connection_time_sec': 'mean'
|
| 193 |
+
}).reset_index()
|
| 194 |
+
|
| 195 |
+
quality_metrics.columns = ['_'.join(col).strip('_') for col in quality_metrics.columns.values]
|
| 196 |
+
|
| 197 |
+
# Merge with customers
|
| 198 |
+
customer_quality = customers.merge(quality_metrics, on='customer_id', how='left')
|
| 199 |
+
|
| 200 |
+
# Quality score
|
| 201 |
+
customer_quality['overall_quality_score'] = (
|
| 202 |
+
(customer_quality.get('download_speed_mbps_mean', 50) / 100) * 0.3 +
|
| 203 |
+
(customer_quality.get('mos_score_mean', 4) / 5) * 0.3 +
|
| 204 |
+
((20 - customer_quality.get('call_drop_occurred', 0).clip(upper=20)) / 20) * 0.4
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
print(f" ✅ Generated {quality_metrics.shape[1]} quality features")
|
| 208 |
+
|
| 209 |
+
return customer_quality
|
| 210 |
+
|
| 211 |
+
def customer_behavior_patterns(self, data):
|
| 212 |
+
"""
|
| 213 |
+
Data usage trends and seasonal patterns
|
| 214 |
+
Peak usage times
|
| 215 |
+
Roaming behavior
|
| 216 |
+
Device upgrade cycles
|
| 217 |
+
Plan change patterns
|
| 218 |
+
"""
|
| 219 |
+
print("\n📊 Customer Behavior Patterns...")
|
| 220 |
+
|
| 221 |
+
usage = data['usage']
|
| 222 |
+
customers = data['customers']
|
| 223 |
+
|
| 224 |
+
# Usage pattern analysis
|
| 225 |
+
usage_patterns = usage.groupby('customer_id').agg({
|
| 226 |
+
'data_usage_gb': ['mean', 'std', 'max', 'sum'],
|
| 227 |
+
'voice_minutes': ['mean', 'max', 'sum'],
|
| 228 |
+
'sms_count': 'sum',
|
| 229 |
+
'roaming_minutes': 'sum',
|
| 230 |
+
'international_calls_min': 'sum',
|
| 231 |
+
'peak_hour_usage_gb': 'sum',
|
| 232 |
+
'data_session_count': 'mean'
|
| 233 |
+
}).reset_index()
|
| 234 |
+
|
| 235 |
+
usage_patterns.columns = ['_'.join(col).strip('_') for col in usage_patterns.columns.values]
|
| 236 |
+
|
| 237 |
+
# Merge with customers
|
| 238 |
+
customer_behavior = customers.merge(usage_patterns, on='customer_id', how='left')
|
| 239 |
+
|
| 240 |
+
# Calculate behavior scores
|
| 241 |
+
customer_behavior['data_intensity_score'] = (
|
| 242 |
+
customer_behavior.get('data_usage_gb_mean', 10) / 50 # Normalize to 0-1
|
| 243 |
+
).clip(upper=1)
|
| 244 |
+
|
| 245 |
+
customer_behavior['roaming_frequency'] = pd.cut(
|
| 246 |
+
customer_behavior.get('roaming_minutes_sum', 0),
|
| 247 |
+
bins=[0, 1, 100, 500, 10000],
|
| 248 |
+
labels=['Never', 'Rare', 'Occasional', 'Frequent']
|
| 249 |
+
)
|
| 250 |
+
|
| 251 |
+
customer_behavior['usage_volatility'] = (
|
| 252 |
+
customer_behavior.get('data_usage_gb_std', 0) /
|
| 253 |
+
customer_behavior.get('data_usage_gb_mean', 1).clip(lower=0.1)
|
| 254 |
+
)
|
| 255 |
+
|
| 256 |
+
print(f" ✅ Generated {usage_patterns.shape[1]} behavior features")
|
| 257 |
+
|
| 258 |
+
return customer_behavior
|
| 259 |
+
|
| 260 |
+
def churn_risk_indicators(self, data):
|
| 261 |
+
"""
|
| 262 |
+
Service quality degradation trends
|
| 263 |
+
Billing complaint patterns
|
| 264 |
+
Usage pattern changes
|
| 265 |
+
Customer service interaction frequency
|
| 266 |
+
Contract expiration proximity
|
| 267 |
+
"""
|
| 268 |
+
print("\n🎯 Churn Risk Indicators...")
|
| 269 |
+
|
| 270 |
+
customers = data['customers']
|
| 271 |
+
churn = data['churn']
|
| 272 |
+
service = data['service']
|
| 273 |
+
|
| 274 |
+
# Merge churn labels
|
| 275 |
+
customers_churn = customers.merge(churn, on='customer_id', how='left')
|
| 276 |
+
|
| 277 |
+
# Calculate risk factors
|
| 278 |
+
# 1. Contract expiration risk
|
| 279 |
+
if 'contract_end_date' in customers_churn.columns:
|
| 280 |
+
customers_churn['contract_end_date'] = pd.to_datetime(
|
| 281 |
+
customers_churn['contract_end_date'], errors='coerce'
|
| 282 |
+
)
|
| 283 |
+
reference_date = pd.Timestamp('2024-12-31')
|
| 284 |
+
customers_churn['days_to_contract_end'] = (
|
| 285 |
+
(customers_churn['contract_end_date'] - reference_date).dt.days
|
| 286 |
+
).fillna(999)
|
| 287 |
+
|
| 288 |
+
customers_churn['contract_expiring_soon'] = (
|
| 289 |
+
customers_churn['days_to_contract_end'] < 90
|
| 290 |
+
).astype(int)
|
| 291 |
+
else:
|
| 292 |
+
customers_churn['days_to_contract_end'] = 999
|
| 293 |
+
customers_churn['contract_expiring_soon'] = 0
|
| 294 |
+
|
| 295 |
+
# 2. Service complaint intensity
|
| 296 |
+
complaints = service[service['complaint_type'].str.contains('Issue|Problem', na=False, case=False)]
|
| 297 |
+
complaint_counts = complaints.groupby('customer_id').size().reset_index(name='complaint_count')
|
| 298 |
+
customers_churn = customers_churn.merge(complaint_counts, on='customer_id', how='left')
|
| 299 |
+
customers_churn['complaint_count'] = customers_churn['complaint_count'].fillna(0)
|
| 300 |
+
|
| 301 |
+
# 3. Tenure risk (early vs late stage)
|
| 302 |
+
customers_churn['early_tenure_risk'] = (customers_churn['tenure_months'] < 6).astype(int)
|
| 303 |
+
customers_churn['mid_tenure_risk'] = (
|
| 304 |
+
(customers_churn['tenure_months'] >= 6) &
|
| 305 |
+
(customers_churn['tenure_months'] <= 12)
|
| 306 |
+
).astype(int)
|
| 307 |
+
|
| 308 |
+
# 4. Price sensitivity indicator
|
| 309 |
+
customers_churn['price_to_arpu_ratio'] = (
|
| 310 |
+
customers_churn['monthly_plan_cost'] /
|
| 311 |
+
customers_churn.get('arpu', customers_churn['monthly_plan_cost']).clip(lower=1)
|
| 312 |
+
)
|
| 313 |
+
|
| 314 |
+
# 5. Composite churn risk score
|
| 315 |
+
customers_churn['computed_churn_risk_score'] = (
|
| 316 |
+
customers_churn['contract_expiring_soon'] * 0.25 +
|
| 317 |
+
(customers_churn['complaint_count'] / 10).clip(upper=1) * 0.25 +
|
| 318 |
+
customers_churn['early_tenure_risk'] * 0.20 +
|
| 319 |
+
(customers_churn['price_to_arpu_ratio'] - 1).clip(lower=0, upper=1) * 0.15 +
|
| 320 |
+
((10 - customers_churn.get('avg_csat', 7)) / 10).clip(lower=0) * 0.15
|
| 321 |
+
)
|
| 322 |
+
|
| 323 |
+
print(f" ✅ Generated churn risk indicators")
|
| 324 |
+
|
| 325 |
+
return customers_churn
|
| 326 |
+
|
| 327 |
+
def geographic_temporal_features(self, data):
|
| 328 |
+
"""
|
| 329 |
+
Location-based service quality
|
| 330 |
+
Urban vs rural performance
|
| 331 |
+
Time-of-day patterns
|
| 332 |
+
Seasonal variations
|
| 333 |
+
Weather impact
|
| 334 |
+
"""
|
| 335 |
+
print("\n🌍 Geographic & Temporal Features...")
|
| 336 |
+
|
| 337 |
+
customers = data['customers']
|
| 338 |
+
network = data['network']
|
| 339 |
+
towers = data['towers']
|
| 340 |
+
|
| 341 |
+
# City-level aggregations
|
| 342 |
+
city_metrics = customers.groupby('city').agg({
|
| 343 |
+
'customer_id': 'count',
|
| 344 |
+
'tenure_months': 'mean',
|
| 345 |
+
'monthly_plan_cost': 'mean'
|
| 346 |
+
}).reset_index()
|
| 347 |
+
|
| 348 |
+
city_metrics.columns = ['city', 'customers_in_city', 'avg_tenure_city', 'avg_price_city']
|
| 349 |
+
|
| 350 |
+
# Merge demographics if available
|
| 351 |
+
if 'demographics' in data and data['demographics'] is not None:
|
| 352 |
+
demo = data['demographics']
|
| 353 |
+
city_metrics = city_metrics.merge(demo, on='city', how='left')
|
| 354 |
+
|
| 355 |
+
# Merge city features back to customers
|
| 356 |
+
customers_geo = customers.merge(city_metrics, on='city', how='left')
|
| 357 |
+
|
| 358 |
+
# Urban/rural classification
|
| 359 |
+
if 'population_density_per_sqkm' in customers_geo.columns:
|
| 360 |
+
customers_geo['location_type'] = pd.cut(
|
| 361 |
+
customers_geo['population_density_per_sqkm'],
|
| 362 |
+
bins=[0, 500, 2000, 100000],
|
| 363 |
+
labels=['Rural', 'Suburban', 'Urban']
|
| 364 |
+
)
|
| 365 |
+
else:
|
| 366 |
+
customers_geo['location_type'] = 'Unknown'
|
| 367 |
+
|
| 368 |
+
print(f" ✅ Generated geographic/temporal features")
|
| 369 |
+
|
| 370 |
+
return customers_geo
|
| 371 |
+
|
| 372 |
+
def financial_performance_metrics(self, data):
|
| 373 |
+
"""
|
| 374 |
+
ARPU and revenue calculations
|
| 375 |
+
Customer acquisition cost
|
| 376 |
+
Churn cost analysis
|
| 377 |
+
Pricing optimization features
|
| 378 |
+
"""
|
| 379 |
+
print("\n💰 Financial Performance Metrics...")
|
| 380 |
+
|
| 381 |
+
customers = data['customers']
|
| 382 |
+
billing = data['billing']
|
| 383 |
+
|
| 384 |
+
# Revenue metrics already calculated in journey analytics
|
| 385 |
+
# Additional financial features
|
| 386 |
+
|
| 387 |
+
# Revenue growth
|
| 388 |
+
billing_sorted = billing.sort_values(['customer_id', 'billing_period'])
|
| 389 |
+
billing_sorted['billing_period'] = pd.to_datetime(billing_sorted['billing_period'])
|
| 390 |
+
|
| 391 |
+
# Calculate month-over-month change (simplified)
|
| 392 |
+
revenue_change = billing.groupby('customer_id')['total_amount'].agg([
|
| 393 |
+
('first_month_revenue', 'first'),
|
| 394 |
+
('last_month_revenue', 'last'),
|
| 395 |
+
('revenue_trend', lambda x: x.iloc[-1] - x.iloc[0] if len(x) > 1 else 0)
|
| 396 |
+
]).reset_index()
|
| 397 |
+
|
| 398 |
+
customers_financial = customers.merge(revenue_change, on='customer_id', how='left')
|
| 399 |
+
|
| 400 |
+
# Profitability indicators
|
| 401 |
+
customers_financial['revenue_trend_pct'] = (
|
| 402 |
+
customers_financial['revenue_trend'] /
|
| 403 |
+
customers_financial['first_month_revenue'].clip(lower=1) * 100
|
| 404 |
+
).fillna(0)
|
| 405 |
+
|
| 406 |
+
# Value segment
|
| 407 |
+
arpu_quartiles = customers_financial['arpu'].quantile([0.25, 0.5, 0.75])
|
| 408 |
+
customers_financial['value_segment'] = pd.cut(
|
| 409 |
+
customers_financial['arpu'],
|
| 410 |
+
bins=[0, arpu_quartiles[0.25], arpu_quartiles[0.5], arpu_quartiles[0.75], 1000],
|
| 411 |
+
labels=['Low', 'Medium', 'High', 'Premium']
|
| 412 |
+
)
|
| 413 |
+
|
| 414 |
+
print(f" ✅ Generated financial metrics")
|
| 415 |
+
|
| 416 |
+
return customers_financial
|
| 417 |
+
|
| 418 |
+
def create_master_feature_table(self, data):
|
| 419 |
+
"""Combine all features into master table"""
|
| 420 |
+
print("\n🔗 Creating Master Feature Table...")
|
| 421 |
+
|
| 422 |
+
# Start with base customers
|
| 423 |
+
features = data['customers'].copy()
|
| 424 |
+
|
| 425 |
+
# Add journey analytics
|
| 426 |
+
features = self.customer_journey_analytics(data)
|
| 427 |
+
|
| 428 |
+
# Add quality features
|
| 429 |
+
quality_features = self.service_quality_features(data)
|
| 430 |
+
quality_cols = [c for c in quality_features.columns if c not in features.columns or c == 'customer_id']
|
| 431 |
+
features = features.merge(quality_features[quality_cols], on='customer_id', how='left')
|
| 432 |
+
|
| 433 |
+
# Add behavior patterns
|
| 434 |
+
behavior_features = self.customer_behavior_patterns(data)
|
| 435 |
+
behavior_cols = [c for c in behavior_features.columns if c not in features.columns or c == 'customer_id']
|
| 436 |
+
features = features.merge(behavior_features[behavior_cols], on='customer_id', how='left')
|
| 437 |
+
|
| 438 |
+
# Add churn indicators
|
| 439 |
+
churn_features = self.churn_risk_indicators(data)
|
| 440 |
+
churn_cols = [c for c in churn_features.columns if c not in features.columns or c == 'customer_id']
|
| 441 |
+
features = features.merge(churn_features[churn_cols], on='customer_id', how='left')
|
| 442 |
+
|
| 443 |
+
# Add geographic features
|
| 444 |
+
geo_features = self.geographic_temporal_features(data)
|
| 445 |
+
geo_cols = [c for c in geo_features.columns if c not in features.columns or c == 'customer_id']
|
| 446 |
+
features = features.merge(geo_features[geo_cols], on='customer_id', how='left')
|
| 447 |
+
|
| 448 |
+
# Add financial metrics
|
| 449 |
+
financial_features = self.financial_performance_metrics(data)
|
| 450 |
+
financial_cols = [c for c in financial_features.columns if c not in features.columns or c == 'customer_id']
|
| 451 |
+
features = features.merge(financial_features[financial_cols], on='customer_id', how='left')
|
| 452 |
+
|
| 453 |
+
# Add device data if available
|
| 454 |
+
if data.get('device') is not None:
|
| 455 |
+
device_cols = [c for c in data['device'].columns if c not in features.columns or c == 'customer_id']
|
| 456 |
+
features = features.merge(data['device'][device_cols], on='customer_id', how='left')
|
| 457 |
+
|
| 458 |
+
# Add journey data if available
|
| 459 |
+
if data.get('journey') is not None:
|
| 460 |
+
journey_cols = [c for c in data['journey'].columns if c not in features.columns or c == 'customer_id']
|
| 461 |
+
features = features.merge(data['journey'][journey_cols], on='customer_id', how='left')
|
| 462 |
+
|
| 463 |
+
print(f" ✅ Master feature table: {features.shape[0]:,} rows × {features.shape[1]} columns")
|
| 464 |
+
|
| 465 |
+
return features
|
| 466 |
+
|
| 467 |
+
def run_pipeline(self):
|
| 468 |
+
"""Execute complete feature engineering pipeline"""
|
| 469 |
+
print("\n" + "="*80)
|
| 470 |
+
print("COMPREHENSIVE FEATURE ENGINEERING PIPELINE")
|
| 471 |
+
print("="*80)
|
| 472 |
+
|
| 473 |
+
# Load data
|
| 474 |
+
data = self.load_data()
|
| 475 |
+
|
| 476 |
+
# Create master feature table
|
| 477 |
+
features = self.create_master_feature_table(data)
|
| 478 |
+
|
| 479 |
+
# Save
|
| 480 |
+
output_path = 'data/processed/comprehensive_features.csv'
|
| 481 |
+
features.to_csv(output_path, index=False)
|
| 482 |
+
|
| 483 |
+
print(f"\n✅ Saved comprehensive features to: {output_path}")
|
| 484 |
+
print(f"\n📊 Feature Summary:")
|
| 485 |
+
print(f" - Total Features: {features.shape[1]}")
|
| 486 |
+
print(f" - Total Records: {features.shape[0]:,}")
|
| 487 |
+
print(f" - Data Quality: {(1 - features.isnull().sum().sum() / (features.shape[0] * features.shape[1])) * 100:.1f}% complete")
|
| 488 |
+
|
| 489 |
+
print("\n" + "="*80)
|
| 490 |
+
|
| 491 |
+
return features
|
| 492 |
+
|
| 493 |
+
|
| 494 |
+
def main():
|
| 495 |
+
"""Run comprehensive feature engineering"""
|
| 496 |
+
engineer = ComprehensiveFeatureEngineer()
|
| 497 |
+
features = engineer.run_pipeline()
|
| 498 |
+
return features
|
| 499 |
+
|
| 500 |
+
|
| 501 |
+
if __name__ == "__main__":
|
| 502 |
+
main()
|
src/forecasting/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Forecasting module for telecom time series analysis."""
|
src/forecasting/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (232 Bytes). View file
|
|
|
src/forecasting/__pycache__/forecasting_engine.cpython-313.pyc
ADDED
|
Binary file (22.9 kB). View file
|
|
|
src/forecasting/forecasting_engine.py
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Time Series Forecasting Engine for Telecom Analytics
|
| 3 |
+
Provides forecasting methods for all four domains:
|
| 4 |
+
1. Seasonal Usage Patterns
|
| 5 |
+
2. Technology Adoption Curves
|
| 6 |
+
3. Competitive Market Dynamics
|
| 7 |
+
4. Economic Impact Forecasting
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import pandas as pd
|
| 11 |
+
import numpy as np
|
| 12 |
+
from datetime import datetime, timedelta
|
| 13 |
+
import os
|
| 14 |
+
import json
|
| 15 |
+
|
| 16 |
+
# ---------------------------------------------------------------------------
|
| 17 |
+
# Helpers
|
| 18 |
+
# ---------------------------------------------------------------------------
|
| 19 |
+
|
| 20 |
+
def _moving_average_forecast(series, window=3, forecast_periods=12):
|
| 21 |
+
"""Weighted moving average forecast."""
|
| 22 |
+
values = series.values.astype(float)
|
| 23 |
+
weights = np.arange(1, window + 1, dtype=float)
|
| 24 |
+
weights /= weights.sum()
|
| 25 |
+
|
| 26 |
+
forecasts = list(values)
|
| 27 |
+
for _ in range(forecast_periods):
|
| 28 |
+
recent = np.array(forecasts[-window:])
|
| 29 |
+
forecasts.append(float(np.dot(recent, weights)))
|
| 30 |
+
return np.array(forecasts[-forecast_periods:])
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _seasonal_decompose_forecast(series, period=12, forecast_periods=12):
|
| 34 |
+
"""Simple seasonal decomposition + trend extrapolation."""
|
| 35 |
+
values = series.values.astype(float)
|
| 36 |
+
n = len(values)
|
| 37 |
+
|
| 38 |
+
# Trend via centered moving average
|
| 39 |
+
if n >= period:
|
| 40 |
+
trend = pd.Series(values).rolling(window=period, center=True).mean().values
|
| 41 |
+
# Fill edges
|
| 42 |
+
for i in range(n):
|
| 43 |
+
if np.isnan(trend[i]):
|
| 44 |
+
trend[i] = values[i]
|
| 45 |
+
else:
|
| 46 |
+
trend = values.copy()
|
| 47 |
+
|
| 48 |
+
# Seasonal component
|
| 49 |
+
detrended = values - trend
|
| 50 |
+
seasonal = np.zeros(period)
|
| 51 |
+
for i in range(period):
|
| 52 |
+
indices = list(range(i, n, period))
|
| 53 |
+
seasonal[i] = np.mean(detrended[indices])
|
| 54 |
+
|
| 55 |
+
# Extrapolate trend linearly
|
| 56 |
+
x = np.arange(n)
|
| 57 |
+
valid = ~np.isnan(trend)
|
| 58 |
+
if valid.sum() > 1:
|
| 59 |
+
coeffs = np.polyfit(x[valid], trend[valid], 1)
|
| 60 |
+
else:
|
| 61 |
+
coeffs = [0, values[-1]]
|
| 62 |
+
|
| 63 |
+
forecast_trend = np.polyval(coeffs, np.arange(n, n + forecast_periods))
|
| 64 |
+
forecast_seasonal = np.tile(seasonal, (forecast_periods // period + 2))[:forecast_periods]
|
| 65 |
+
|
| 66 |
+
forecast = forecast_trend + forecast_seasonal
|
| 67 |
+
return forecast
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def _logistic_curve_forecast(current_values, k=0.65, forecast_periods=12):
|
| 71 |
+
"""Forecast S-curve / logistic adoption."""
|
| 72 |
+
values = current_values / 100.0 # convert from pct
|
| 73 |
+
n = len(values)
|
| 74 |
+
|
| 75 |
+
# Fit logistic parameters by least squares grid search
|
| 76 |
+
best_err = float('inf')
|
| 77 |
+
best_x0 = n // 2
|
| 78 |
+
best_L = 0.2
|
| 79 |
+
|
| 80 |
+
for x0_try in range(max(1, n // 4), n + 12):
|
| 81 |
+
for L_try in np.arange(0.05, 0.5, 0.05):
|
| 82 |
+
t = np.arange(n)
|
| 83 |
+
predicted = k / (1 + np.exp(-L_try * (t - x0_try)))
|
| 84 |
+
err = np.sum((predicted - values) ** 2)
|
| 85 |
+
if err < best_err:
|
| 86 |
+
best_err = err
|
| 87 |
+
best_x0 = x0_try
|
| 88 |
+
best_L = L_try
|
| 89 |
+
|
| 90 |
+
t_future = np.arange(n, n + forecast_periods)
|
| 91 |
+
forecast = k / (1 + np.exp(-best_L * (t_future - best_x0)))
|
| 92 |
+
return forecast * 100 # back to pct
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def _exponential_smoothing(series, alpha=0.3, forecast_periods=12):
|
| 96 |
+
"""Simple exponential smoothing forecast."""
|
| 97 |
+
values = series.values.astype(float)
|
| 98 |
+
smoothed = [values[0]]
|
| 99 |
+
for v in values[1:]:
|
| 100 |
+
smoothed.append(alpha * v + (1 - alpha) * smoothed[-1])
|
| 101 |
+
|
| 102 |
+
forecasts = []
|
| 103 |
+
last = smoothed[-1]
|
| 104 |
+
# Add slight trend
|
| 105 |
+
if len(smoothed) > 1:
|
| 106 |
+
trend = (smoothed[-1] - smoothed[-6]) / 6 if len(smoothed) >= 6 else 0
|
| 107 |
+
else:
|
| 108 |
+
trend = 0
|
| 109 |
+
|
| 110 |
+
for i in range(forecast_periods):
|
| 111 |
+
next_val = last + trend
|
| 112 |
+
forecasts.append(next_val)
|
| 113 |
+
last = next_val
|
| 114 |
+
|
| 115 |
+
return np.array(forecasts)
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
# ---------------------------------------------------------------------------
|
| 119 |
+
# Forecast Functions per Domain
|
| 120 |
+
# ---------------------------------------------------------------------------
|
| 121 |
+
|
| 122 |
+
def forecast_seasonal_usage(seasonal_df, forecast_months=12):
|
| 123 |
+
"""Forecast seasonal usage patterns."""
|
| 124 |
+
last_date = pd.to_datetime(seasonal_df['date']).max()
|
| 125 |
+
forecast_dates = pd.date_range(start=last_date + pd.DateOffset(months=1),
|
| 126 |
+
periods=forecast_months, freq='ME')
|
| 127 |
+
|
| 128 |
+
# Forecast each metric
|
| 129 |
+
data_usage_forecast = _seasonal_decompose_forecast(
|
| 130 |
+
seasonal_df['avg_data_usage_gb'], period=12, forecast_periods=forecast_months)
|
| 131 |
+
|
| 132 |
+
voice_forecast = _seasonal_decompose_forecast(
|
| 133 |
+
seasonal_df['avg_voice_minutes'], period=12, forecast_periods=forecast_months)
|
| 134 |
+
|
| 135 |
+
network_load_forecast = _moving_average_forecast(
|
| 136 |
+
seasonal_df['network_load_factor'], window=4, forecast_periods=forecast_months)
|
| 137 |
+
network_load_forecast = np.clip(network_load_forecast, 0.3, 0.98)
|
| 138 |
+
|
| 139 |
+
peak_users_forecast = _seasonal_decompose_forecast(
|
| 140 |
+
seasonal_df['peak_concurrent_users'], period=12, forecast_periods=forecast_months)
|
| 141 |
+
|
| 142 |
+
# Predict holiday months
|
| 143 |
+
holiday_flags = []
|
| 144 |
+
for d in forecast_dates:
|
| 145 |
+
holiday_flags.append(1 if d.month in [2, 7, 9, 11, 12] else 0)
|
| 146 |
+
|
| 147 |
+
# Confidence intervals (wider for further out)
|
| 148 |
+
ci_width = np.linspace(0.05, 0.20, forecast_months)
|
| 149 |
+
|
| 150 |
+
result = {
|
| 151 |
+
'dates': [d.strftime('%Y-%m') for d in forecast_dates],
|
| 152 |
+
'data_usage': {
|
| 153 |
+
'forecast': np.round(np.maximum(data_usage_forecast, 2), 2).tolist(),
|
| 154 |
+
'upper': np.round(data_usage_forecast * (1 + ci_width), 2).tolist(),
|
| 155 |
+
'lower': np.round(data_usage_forecast * (1 - ci_width), 2).tolist(),
|
| 156 |
+
},
|
| 157 |
+
'voice_minutes': {
|
| 158 |
+
'forecast': np.round(np.maximum(voice_forecast, 50), 1).tolist(),
|
| 159 |
+
'upper': np.round(voice_forecast * (1 + ci_width * 0.8), 1).tolist(),
|
| 160 |
+
'lower': np.round(voice_forecast * (1 - ci_width * 0.8), 1).tolist(),
|
| 161 |
+
},
|
| 162 |
+
'network_load': {
|
| 163 |
+
'forecast': np.round(network_load_forecast, 3).tolist(),
|
| 164 |
+
'upper': np.round(np.clip(network_load_forecast + ci_width * 0.3, 0, 1), 3).tolist(),
|
| 165 |
+
'lower': np.round(np.clip(network_load_forecast - ci_width * 0.3, 0, 1), 3).tolist(),
|
| 166 |
+
},
|
| 167 |
+
'peak_users': {
|
| 168 |
+
'forecast': np.round(np.maximum(peak_users_forecast, 10000)).astype(int).tolist(),
|
| 169 |
+
'upper': np.round(peak_users_forecast * (1 + ci_width)).astype(int).tolist(),
|
| 170 |
+
'lower': np.round(peak_users_forecast * (1 - ci_width)).astype(int).tolist(),
|
| 171 |
+
},
|
| 172 |
+
'holiday_months': holiday_flags,
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
# Historical data for chart context
|
| 176 |
+
hist = seasonal_df.tail(12)
|
| 177 |
+
result['historical'] = {
|
| 178 |
+
'dates': [pd.to_datetime(d).strftime('%Y-%m') for d in hist['date']],
|
| 179 |
+
'data_usage': hist['avg_data_usage_gb'].round(2).tolist(),
|
| 180 |
+
'voice_minutes': hist['avg_voice_minutes'].round(1).tolist(),
|
| 181 |
+
'network_load': hist['network_load_factor'].round(3).tolist(),
|
| 182 |
+
'peak_users': hist['peak_concurrent_users'].tolist(),
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
return result
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def forecast_tech_adoption(tech_df, forecast_months=12):
|
| 189 |
+
"""Forecast 5G adoption and technology migration."""
|
| 190 |
+
last_date = pd.to_datetime(tech_df['date']).max()
|
| 191 |
+
forecast_dates = pd.date_range(start=last_date + pd.DateOffset(months=1),
|
| 192 |
+
periods=forecast_months, freq='ME')
|
| 193 |
+
|
| 194 |
+
# 5G adoption via logistic curve
|
| 195 |
+
five_g_forecast = _logistic_curve_forecast(
|
| 196 |
+
tech_df['five_g_adoption_pct'].values, k=0.65, forecast_periods=forecast_months)
|
| 197 |
+
|
| 198 |
+
# Tower deployment trend
|
| 199 |
+
towers_forecast = _exponential_smoothing(
|
| 200 |
+
tech_df['five_g_towers_cumulative'], alpha=0.4, forecast_periods=forecast_months)
|
| 201 |
+
towers_forecast = np.maximum(towers_forecast, tech_df['five_g_towers_cumulative'].iloc[-1])
|
| 202 |
+
|
| 203 |
+
# Speed improvement
|
| 204 |
+
speed_forecast = _moving_average_forecast(
|
| 205 |
+
tech_df['avg_5g_speed_mbps'], window=4, forecast_periods=forecast_months)
|
| 206 |
+
|
| 207 |
+
# Revenue premium
|
| 208 |
+
premium_forecast = _exponential_smoothing(
|
| 209 |
+
tech_df['five_g_revenue_premium_pct'], alpha=0.35, forecast_periods=forecast_months)
|
| 210 |
+
|
| 211 |
+
# 4G and 3G derived
|
| 212 |
+
four_g_forecast = np.maximum(100 - five_g_forecast - 5, 20) # floor at 20%
|
| 213 |
+
three_g_forecast = 100 - five_g_forecast - four_g_forecast
|
| 214 |
+
|
| 215 |
+
ci_width = np.linspace(0.03, 0.15, forecast_months)
|
| 216 |
+
|
| 217 |
+
result = {
|
| 218 |
+
'dates': [d.strftime('%Y-%m') for d in forecast_dates],
|
| 219 |
+
'five_g_adoption': {
|
| 220 |
+
'forecast': np.round(five_g_forecast, 2).tolist(),
|
| 221 |
+
'upper': np.round(five_g_forecast * (1 + ci_width), 2).tolist(),
|
| 222 |
+
'lower': np.round(np.maximum(five_g_forecast * (1 - ci_width), 0), 2).tolist(),
|
| 223 |
+
},
|
| 224 |
+
'four_g_pct': np.round(four_g_forecast, 2).tolist(),
|
| 225 |
+
'three_g_pct': np.round(np.maximum(three_g_forecast, 1), 2).tolist(),
|
| 226 |
+
'towers_deployed': {
|
| 227 |
+
'forecast': np.round(towers_forecast).astype(int).tolist(),
|
| 228 |
+
},
|
| 229 |
+
'avg_speed': {
|
| 230 |
+
'forecast': np.round(speed_forecast, 1).tolist(),
|
| 231 |
+
},
|
| 232 |
+
'revenue_premium': {
|
| 233 |
+
'forecast': np.round(premium_forecast, 1).tolist(),
|
| 234 |
+
},
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
# Historical
|
| 238 |
+
hist = tech_df.tail(12)
|
| 239 |
+
result['historical'] = {
|
| 240 |
+
'dates': [pd.to_datetime(d).strftime('%Y-%m') for d in hist['date']],
|
| 241 |
+
'five_g_adoption': hist['five_g_adoption_pct'].round(2).tolist(),
|
| 242 |
+
'four_g_pct': hist['four_g_pct'].round(2).tolist(),
|
| 243 |
+
'three_g_pct': hist['three_g_pct'].round(2).tolist(),
|
| 244 |
+
'towers_deployed': hist['five_g_towers_cumulative'].tolist(),
|
| 245 |
+
'avg_speed': hist['avg_5g_speed_mbps'].round(1).tolist(),
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
return result
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def forecast_competitive_dynamics(comp_df, forecast_months=12):
|
| 252 |
+
"""Forecast competitive market dynamics."""
|
| 253 |
+
last_date = pd.to_datetime(comp_df['date']).max()
|
| 254 |
+
forecast_dates = pd.date_range(start=last_date + pd.DateOffset(months=1),
|
| 255 |
+
periods=forecast_months, freq='ME')
|
| 256 |
+
|
| 257 |
+
# Market shares via exponential smoothing
|
| 258 |
+
our_share_fc = _exponential_smoothing(comp_df['our_market_share'], alpha=0.35, forecast_periods=forecast_months)
|
| 259 |
+
comp_a_fc = _exponential_smoothing(comp_df['competitor_a_share'], alpha=0.35, forecast_periods=forecast_months)
|
| 260 |
+
comp_b_fc = _exponential_smoothing(comp_df['competitor_b_share'], alpha=0.35, forecast_periods=forecast_months)
|
| 261 |
+
comp_c_fc = 100 - our_share_fc - comp_a_fc - comp_b_fc
|
| 262 |
+
|
| 263 |
+
# Pricing forecast
|
| 264 |
+
our_price_fc = _moving_average_forecast(comp_df['our_avg_price'], window=4, forecast_periods=forecast_months)
|
| 265 |
+
market_price_fc = _moving_average_forecast(comp_df['market_avg_price'], window=4, forecast_periods=forecast_months)
|
| 266 |
+
|
| 267 |
+
# Net subscriber adds
|
| 268 |
+
net_adds_fc = _seasonal_decompose_forecast(comp_df['net_subscriber_adds'], period=12, forecast_periods=forecast_months)
|
| 269 |
+
|
| 270 |
+
# Competitive churn
|
| 271 |
+
comp_churn_fc = _moving_average_forecast(comp_df['competitive_churn_pct'], window=4, forecast_periods=forecast_months)
|
| 272 |
+
|
| 273 |
+
# Pricing war risk assessment (based on price convergence)
|
| 274 |
+
price_gap = np.abs(our_price_fc - market_price_fc)
|
| 275 |
+
pricing_war_risk = np.clip(1 - price_gap / 10, 0, 1)
|
| 276 |
+
|
| 277 |
+
ci_width = np.linspace(0.02, 0.12, forecast_months)
|
| 278 |
+
|
| 279 |
+
result = {
|
| 280 |
+
'dates': [d.strftime('%Y-%m') for d in forecast_dates],
|
| 281 |
+
'market_shares': {
|
| 282 |
+
'ours': np.round(our_share_fc, 2).tolist(),
|
| 283 |
+
'competitor_a': np.round(comp_a_fc, 2).tolist(),
|
| 284 |
+
'competitor_b': np.round(comp_b_fc, 2).tolist(),
|
| 285 |
+
'competitor_c': np.round(np.maximum(comp_c_fc, 5), 2).tolist(),
|
| 286 |
+
},
|
| 287 |
+
'pricing': {
|
| 288 |
+
'our_price': np.round(our_price_fc, 2).tolist(),
|
| 289 |
+
'market_price': np.round(market_price_fc, 2).tolist(),
|
| 290 |
+
},
|
| 291 |
+
'net_adds': {
|
| 292 |
+
'forecast': np.round(net_adds_fc).astype(int).tolist(),
|
| 293 |
+
'upper': np.round(net_adds_fc * (1 + ci_width * 2)).astype(int).tolist(),
|
| 294 |
+
'lower': np.round(net_adds_fc * (1 - ci_width * 2)).astype(int).tolist(),
|
| 295 |
+
},
|
| 296 |
+
'competitive_churn': {
|
| 297 |
+
'forecast': np.round(np.maximum(comp_churn_fc, 0.3), 2).tolist(),
|
| 298 |
+
},
|
| 299 |
+
'pricing_war_risk': np.round(pricing_war_risk * 100, 1).tolist(),
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
# Historical
|
| 303 |
+
hist = comp_df.tail(12)
|
| 304 |
+
result['historical'] = {
|
| 305 |
+
'dates': [pd.to_datetime(d).strftime('%Y-%m') for d in hist['date']],
|
| 306 |
+
'our_share': hist['our_market_share'].round(2).tolist(),
|
| 307 |
+
'competitor_a': hist['competitor_a_share'].round(2).tolist(),
|
| 308 |
+
'competitor_b': hist['competitor_b_share'].round(2).tolist(),
|
| 309 |
+
'competitor_c': hist['competitor_c_share'].round(2).tolist(),
|
| 310 |
+
'our_price': hist['our_avg_price'].round(2).tolist(),
|
| 311 |
+
'market_price': hist['market_avg_price'].round(2).tolist(),
|
| 312 |
+
'net_adds': hist['net_subscriber_adds'].tolist(),
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
return result
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
def forecast_economic_impact(econ_df, forecast_months=12):
|
| 319 |
+
"""Forecast economic impact on telecom behavior."""
|
| 320 |
+
last_date = pd.to_datetime(econ_df['date']).max()
|
| 321 |
+
forecast_dates = pd.date_range(start=last_date + pd.DateOffset(months=1),
|
| 322 |
+
periods=forecast_months, freq='ME')
|
| 323 |
+
|
| 324 |
+
# GDP growth
|
| 325 |
+
gdp_fc = _exponential_smoothing(econ_df['gdp_growth_rate'], alpha=0.3, forecast_periods=forecast_months)
|
| 326 |
+
|
| 327 |
+
# Consumer confidence
|
| 328 |
+
cci_fc = _exponential_smoothing(econ_df['consumer_confidence_index'], alpha=0.3, forecast_periods=forecast_months)
|
| 329 |
+
|
| 330 |
+
# Unemployment
|
| 331 |
+
unemp_fc = _exponential_smoothing(econ_df['unemployment_rate'], alpha=0.25, forecast_periods=forecast_months)
|
| 332 |
+
|
| 333 |
+
# ARPU index
|
| 334 |
+
arpu_fc = _exponential_smoothing(econ_df['arpu_index'], alpha=0.35, forecast_periods=forecast_months)
|
| 335 |
+
|
| 336 |
+
# Downgrade rate
|
| 337 |
+
downgrade_fc = _moving_average_forecast(econ_df['plan_downgrade_rate'], window=4, forecast_periods=forecast_months)
|
| 338 |
+
|
| 339 |
+
# Delinquency
|
| 340 |
+
delinquency_fc = _moving_average_forecast(econ_df['payment_delinquency_rate'], window=4, forecast_periods=forecast_months)
|
| 341 |
+
|
| 342 |
+
# Revenue at risk
|
| 343 |
+
risk_fc = _exponential_smoothing(econ_df['revenue_at_risk_millions'], alpha=0.3, forecast_periods=forecast_months)
|
| 344 |
+
|
| 345 |
+
# Sentiment
|
| 346 |
+
sentiment_fc = _exponential_smoothing(econ_df['customer_sentiment_index'], alpha=0.3, forecast_periods=forecast_months)
|
| 347 |
+
|
| 348 |
+
# Recession probability (simple heuristic)
|
| 349 |
+
recession_prob = np.clip((1.5 - gdp_fc) / 2.0 * 100, 0, 95)
|
| 350 |
+
|
| 351 |
+
ci_width = np.linspace(0.03, 0.18, forecast_months)
|
| 352 |
+
|
| 353 |
+
result = {
|
| 354 |
+
'dates': [d.strftime('%Y-%m') for d in forecast_dates],
|
| 355 |
+
'gdp_growth': {
|
| 356 |
+
'forecast': np.round(gdp_fc, 2).tolist(),
|
| 357 |
+
'upper': np.round(gdp_fc + ci_width * 3, 2).tolist(),
|
| 358 |
+
'lower': np.round(gdp_fc - ci_width * 3, 2).tolist(),
|
| 359 |
+
},
|
| 360 |
+
'consumer_confidence': {
|
| 361 |
+
'forecast': np.round(np.clip(cci_fc, 20, 100), 1).tolist(),
|
| 362 |
+
},
|
| 363 |
+
'unemployment': {
|
| 364 |
+
'forecast': np.round(np.clip(unemp_fc, 2, 12), 1).tolist(),
|
| 365 |
+
},
|
| 366 |
+
'arpu_index': {
|
| 367 |
+
'forecast': np.round(arpu_fc, 1).tolist(),
|
| 368 |
+
},
|
| 369 |
+
'downgrade_rate': {
|
| 370 |
+
'forecast': np.round(np.maximum(downgrade_fc, 0.5), 2).tolist(),
|
| 371 |
+
},
|
| 372 |
+
'delinquency_rate': {
|
| 373 |
+
'forecast': np.round(np.maximum(delinquency_fc, 0.5), 2).tolist(),
|
| 374 |
+
},
|
| 375 |
+
'revenue_at_risk': {
|
| 376 |
+
'forecast': np.round(np.maximum(risk_fc, 0.5), 2).tolist(),
|
| 377 |
+
'upper': np.round(np.maximum(risk_fc, 0.5) * (1 + ci_width), 2).tolist(),
|
| 378 |
+
'lower': np.round(np.maximum(risk_fc, 0.5) * (1 - ci_width), 2).tolist(),
|
| 379 |
+
},
|
| 380 |
+
'sentiment_index': {
|
| 381 |
+
'forecast': np.round(np.clip(sentiment_fc, 10, 100), 1).tolist(),
|
| 382 |
+
},
|
| 383 |
+
'recession_probability': np.round(recession_prob, 1).tolist(),
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
# Historical
|
| 387 |
+
hist = econ_df.tail(12)
|
| 388 |
+
result['historical'] = {
|
| 389 |
+
'dates': [pd.to_datetime(d).strftime('%Y-%m') for d in hist['date']],
|
| 390 |
+
'gdp_growth': hist['gdp_growth_rate'].round(2).tolist(),
|
| 391 |
+
'consumer_confidence': hist['consumer_confidence_index'].round(1).tolist(),
|
| 392 |
+
'unemployment': hist['unemployment_rate'].round(1).tolist(),
|
| 393 |
+
'arpu_index': hist['arpu_index'].round(1).tolist(),
|
| 394 |
+
'revenue_at_risk': hist['revenue_at_risk_millions'].round(2).tolist(),
|
| 395 |
+
'sentiment_index': hist['customer_sentiment_index'].round(1).tolist(),
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
return result
|
| 399 |
+
|
| 400 |
+
|
| 401 |
+
def get_forecast_summary(seasonal_fc, tech_fc, comp_fc, econ_fc):
|
| 402 |
+
"""Generate a high-level summary of all forecasts for KPI cards."""
|
| 403 |
+
summary = {}
|
| 404 |
+
|
| 405 |
+
# Seasonal
|
| 406 |
+
usage_trend = seasonal_fc['data_usage']['forecast']
|
| 407 |
+
summary['data_usage_next_month'] = usage_trend[0]
|
| 408 |
+
summary['data_usage_growth'] = round((usage_trend[-1] - usage_trend[0]) / usage_trend[0] * 100, 1)
|
| 409 |
+
summary['peak_network_load'] = max(seasonal_fc['network_load']['forecast'])
|
| 410 |
+
|
| 411 |
+
# Tech
|
| 412 |
+
five_g = tech_fc['five_g_adoption']['forecast']
|
| 413 |
+
summary['five_g_current'] = tech_fc['historical']['five_g_adoption'][-1]
|
| 414 |
+
summary['five_g_forecast_end'] = five_g[-1]
|
| 415 |
+
summary['five_g_growth'] = round(five_g[-1] - tech_fc['historical']['five_g_adoption'][-1], 1)
|
| 416 |
+
|
| 417 |
+
# Competitive
|
| 418 |
+
summary['market_share_current'] = comp_fc['historical']['our_share'][-1]
|
| 419 |
+
summary['market_share_forecast'] = comp_fc['market_shares']['ours'][-1]
|
| 420 |
+
summary['avg_pricing_war_risk'] = round(np.mean(comp_fc['pricing_war_risk']), 1)
|
| 421 |
+
|
| 422 |
+
# Economic
|
| 423 |
+
summary['recession_probability'] = econ_fc['recession_probability'][0]
|
| 424 |
+
summary['revenue_at_risk'] = econ_fc['revenue_at_risk']['forecast'][0]
|
| 425 |
+
summary['sentiment_forecast'] = econ_fc['sentiment_index']['forecast'][0]
|
| 426 |
+
|
| 427 |
+
return summary
|
src/forecasting/time_series_generator.py
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Time Series Data Generator for Telecom Forecasting
|
| 3 |
+
Generates historical time series data for:
|
| 4 |
+
- Seasonal Usage Patterns (holiday/event-driven demand)
|
| 5 |
+
- Technology Adoption Curves (5G deployment & migration)
|
| 6 |
+
- Competitive Market Dynamics (pricing wars, market share)
|
| 7 |
+
- Economic Impact Forecasting (recession effects)
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import pandas as pd
|
| 11 |
+
import numpy as np
|
| 12 |
+
from datetime import datetime, timedelta
|
| 13 |
+
import os
|
| 14 |
+
import json
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def generate_seasonal_usage(start_date='2023-01-01', periods=36):
|
| 18 |
+
"""Generate monthly usage data with seasonal patterns, holidays, and event spikes."""
|
| 19 |
+
np.random.seed(42)
|
| 20 |
+
dates = pd.date_range(start=start_date, periods=periods, freq='ME')
|
| 21 |
+
|
| 22 |
+
# Base data usage growth trend (GB per user)
|
| 23 |
+
base_usage = np.linspace(8.5, 14.0, periods)
|
| 24 |
+
|
| 25 |
+
# Seasonal pattern: higher in winter (streaming), lower in summer (outdoor)
|
| 26 |
+
seasonal = 1.2 * np.sin(2 * np.pi * np.arange(periods) / 12 - np.pi / 2)
|
| 27 |
+
|
| 28 |
+
# Holiday spikes (Dec, Nov for Black Friday, Jul for summer events)
|
| 29 |
+
holiday_spike = np.zeros(periods)
|
| 30 |
+
for i, d in enumerate(dates):
|
| 31 |
+
if d.month == 12:
|
| 32 |
+
holiday_spike[i] = 2.5 # Christmas / New Year
|
| 33 |
+
elif d.month == 11:
|
| 34 |
+
holiday_spike[i] = 1.8 # Black Friday / Thanksgiving
|
| 35 |
+
elif d.month == 7:
|
| 36 |
+
holiday_spike[i] = 1.2 # Summer events / festivals
|
| 37 |
+
elif d.month == 2:
|
| 38 |
+
holiday_spike[i] = 0.9 # Super Bowl
|
| 39 |
+
elif d.month == 9:
|
| 40 |
+
holiday_spike[i] = 0.6 # Back to school
|
| 41 |
+
|
| 42 |
+
# Event-driven spikes (random major events)
|
| 43 |
+
event_spike = np.zeros(periods)
|
| 44 |
+
event_months = np.random.choice(range(periods), size=4, replace=False)
|
| 45 |
+
event_names = ['Major Sports Final', 'Product Launch Event', 'Music Festival', 'Emergency Weather']
|
| 46 |
+
events_log = {}
|
| 47 |
+
for idx, em in enumerate(event_months):
|
| 48 |
+
event_spike[em] = np.random.uniform(1.5, 3.0)
|
| 49 |
+
events_log[str(dates[em].date())] = event_names[idx]
|
| 50 |
+
|
| 51 |
+
noise = np.random.normal(0, 0.3, periods)
|
| 52 |
+
|
| 53 |
+
data_usage_gb = base_usage + seasonal + holiday_spike + event_spike + noise
|
| 54 |
+
voice_minutes = np.linspace(320, 250, periods) + 15 * np.sin(2 * np.pi * np.arange(periods) / 12) + np.random.normal(0, 5, periods)
|
| 55 |
+
sms_count = np.linspace(180, 90, periods) + 8 * np.sin(2 * np.pi * np.arange(periods) / 6) + np.random.normal(0, 3, periods)
|
| 56 |
+
|
| 57 |
+
# Network load factor
|
| 58 |
+
network_load = 0.55 + 0.15 * (data_usage_gb - data_usage_gb.min()) / (data_usage_gb.max() - data_usage_gb.min())
|
| 59 |
+
network_load += np.random.normal(0, 0.02, periods)
|
| 60 |
+
|
| 61 |
+
# Peak concurrent users
|
| 62 |
+
peak_users = (45000 + 800 * np.arange(periods) +
|
| 63 |
+
3000 * np.sin(2 * np.pi * np.arange(periods) / 12) +
|
| 64 |
+
holiday_spike * 5000 + event_spike * 8000 +
|
| 65 |
+
np.random.normal(0, 500, periods))
|
| 66 |
+
|
| 67 |
+
df = pd.DataFrame({
|
| 68 |
+
'date': dates,
|
| 69 |
+
'avg_data_usage_gb': np.round(np.maximum(data_usage_gb, 2.0), 2),
|
| 70 |
+
'avg_voice_minutes': np.round(np.maximum(voice_minutes, 50), 1),
|
| 71 |
+
'avg_sms_count': np.round(np.maximum(sms_count, 10), 0).astype(int),
|
| 72 |
+
'network_load_factor': np.round(np.clip(network_load, 0.3, 0.95), 3),
|
| 73 |
+
'peak_concurrent_users': np.round(np.maximum(peak_users, 30000)).astype(int),
|
| 74 |
+
'is_holiday_month': [1 if holiday_spike[i] > 0 else 0 for i in range(periods)],
|
| 75 |
+
'event_spike_magnitude': np.round(event_spike, 2),
|
| 76 |
+
})
|
| 77 |
+
|
| 78 |
+
return df, events_log
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def generate_tech_adoption(start_date='2023-01-01', periods=36):
|
| 82 |
+
"""Generate 5G adoption curve and technology migration data."""
|
| 83 |
+
np.random.seed(43)
|
| 84 |
+
dates = pd.date_range(start=start_date, periods=periods, freq='ME')
|
| 85 |
+
|
| 86 |
+
t = np.arange(periods)
|
| 87 |
+
|
| 88 |
+
# S-curve (logistic) for 5G adoption
|
| 89 |
+
# 5G started slow, inflection around month 18, saturation ~65%
|
| 90 |
+
k = 0.65 # max adoption
|
| 91 |
+
x0 = 18 # inflection point
|
| 92 |
+
L = 0.22 # growth rate
|
| 93 |
+
five_g_adoption = k / (1 + np.exp(-L * (t - x0)))
|
| 94 |
+
five_g_adoption += np.random.normal(0, 0.008, periods)
|
| 95 |
+
five_g_adoption = np.clip(five_g_adoption, 0.02, 0.65)
|
| 96 |
+
|
| 97 |
+
# 4G decline mirrors 5G rise
|
| 98 |
+
four_g_pct = 0.75 - 0.45 * five_g_adoption / k
|
| 99 |
+
four_g_pct += np.random.normal(0, 0.005, periods)
|
| 100 |
+
|
| 101 |
+
# 3G legacy decline
|
| 102 |
+
three_g_pct = 1.0 - five_g_adoption - four_g_pct
|
| 103 |
+
three_g_pct = np.clip(three_g_pct, 0.02, 0.30)
|
| 104 |
+
|
| 105 |
+
# Normalize
|
| 106 |
+
total = five_g_adoption + four_g_pct + three_g_pct
|
| 107 |
+
five_g_adoption /= total
|
| 108 |
+
four_g_pct /= total
|
| 109 |
+
three_g_pct /= total
|
| 110 |
+
|
| 111 |
+
# 5G tower deployment
|
| 112 |
+
towers_deployed = np.cumsum(np.maximum(
|
| 113 |
+
np.round(8 + 12 * five_g_adoption * 50 + np.random.normal(0, 3, periods)),
|
| 114 |
+
2
|
| 115 |
+
)).astype(int)
|
| 116 |
+
|
| 117 |
+
# 5G avg speed improvement
|
| 118 |
+
avg_5g_speed = 250 + 350 * (five_g_adoption / k) + np.random.normal(0, 15, periods)
|
| 119 |
+
|
| 120 |
+
# Customer migration rate (new 5G activations per month)
|
| 121 |
+
migration_rate = np.diff(five_g_adoption, prepend=five_g_adoption[0]) * 100
|
| 122 |
+
migration_rate = np.maximum(migration_rate, 0) + np.random.normal(0, 0.1, periods)
|
| 123 |
+
|
| 124 |
+
# 5G revenue premium
|
| 125 |
+
revenue_premium_pct = 12 + 18 * (five_g_adoption / k) + np.random.normal(0, 1.5, periods)
|
| 126 |
+
|
| 127 |
+
df = pd.DataFrame({
|
| 128 |
+
'date': dates,
|
| 129 |
+
'five_g_adoption_pct': np.round(five_g_adoption * 100, 2),
|
| 130 |
+
'four_g_pct': np.round(four_g_pct * 100, 2),
|
| 131 |
+
'three_g_pct': np.round(three_g_pct * 100, 2),
|
| 132 |
+
'five_g_towers_cumulative': towers_deployed,
|
| 133 |
+
'avg_5g_speed_mbps': np.round(np.maximum(avg_5g_speed, 150), 1),
|
| 134 |
+
'monthly_migration_rate': np.round(np.maximum(migration_rate, 0), 2),
|
| 135 |
+
'five_g_revenue_premium_pct': np.round(np.maximum(revenue_premium_pct, 5), 1),
|
| 136 |
+
})
|
| 137 |
+
|
| 138 |
+
return df
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def generate_competitive_dynamics(start_date='2023-01-01', periods=36):
|
| 142 |
+
"""Generate competitive market dynamics with pricing wars and market share shifts."""
|
| 143 |
+
np.random.seed(44)
|
| 144 |
+
dates = pd.date_range(start=start_date, periods=periods, freq='ME')
|
| 145 |
+
|
| 146 |
+
t = np.arange(periods)
|
| 147 |
+
|
| 148 |
+
# Market share for 4 competitors: Us, CompA, CompB, CompC
|
| 149 |
+
our_share = 32.0 + 0.08 * t + 1.5 * np.sin(2 * np.pi * t / 12) + np.random.normal(0, 0.3, periods)
|
| 150 |
+
comp_a_share = 28.0 - 0.04 * t + 0.8 * np.sin(2 * np.pi * t / 12 + 1) + np.random.normal(0, 0.3, periods)
|
| 151 |
+
comp_b_share = 22.0 + 0.02 * t + np.random.normal(0, 0.25, periods)
|
| 152 |
+
comp_c_share = 100 - our_share - comp_a_share - comp_b_share
|
| 153 |
+
comp_c_share = np.maximum(comp_c_share, 8.0)
|
| 154 |
+
|
| 155 |
+
# Normalize to 100
|
| 156 |
+
total = our_share + comp_a_share + comp_b_share + comp_c_share
|
| 157 |
+
our_share = our_share / total * 100
|
| 158 |
+
comp_a_share = comp_a_share / total * 100
|
| 159 |
+
comp_b_share = comp_b_share / total * 100
|
| 160 |
+
comp_c_share = comp_c_share / total * 100
|
| 161 |
+
|
| 162 |
+
# Pricing war periods (competitive price drops)
|
| 163 |
+
pricing_war_intensity = np.zeros(periods)
|
| 164 |
+
# Two pricing war periods
|
| 165 |
+
pw_starts = [8, 24]
|
| 166 |
+
for pw in pw_starts:
|
| 167 |
+
for j in range(min(5, periods - pw)):
|
| 168 |
+
pricing_war_intensity[pw + j] = max(0, 1.0 - 0.2 * j)
|
| 169 |
+
|
| 170 |
+
# Average market price per line
|
| 171 |
+
base_price = np.linspace(55, 48, periods)
|
| 172 |
+
price_war_discount = pricing_war_intensity * 8
|
| 173 |
+
our_avg_price = base_price - price_war_discount * 0.6 + np.random.normal(0, 0.5, periods)
|
| 174 |
+
market_avg_price = base_price - price_war_discount + np.random.normal(0, 0.8, periods)
|
| 175 |
+
|
| 176 |
+
# Churn due to competition
|
| 177 |
+
competitive_churn_pct = 1.2 + 0.8 * pricing_war_intensity + np.random.normal(0, 0.15, periods)
|
| 178 |
+
|
| 179 |
+
# Net subscriber additions
|
| 180 |
+
net_adds = (1200 - 300 * pricing_war_intensity +
|
| 181 |
+
50 * np.sin(2 * np.pi * t / 12) +
|
| 182 |
+
np.random.normal(0, 150, periods))
|
| 183 |
+
|
| 184 |
+
# Competitor promotional activity index (0-100)
|
| 185 |
+
promo_index = 30 + 50 * pricing_war_intensity + 10 * np.sin(2 * np.pi * t / 6) + np.random.normal(0, 5, periods)
|
| 186 |
+
|
| 187 |
+
df = pd.DataFrame({
|
| 188 |
+
'date': dates,
|
| 189 |
+
'our_market_share': np.round(our_share, 2),
|
| 190 |
+
'competitor_a_share': np.round(comp_a_share, 2),
|
| 191 |
+
'competitor_b_share': np.round(comp_b_share, 2),
|
| 192 |
+
'competitor_c_share': np.round(comp_c_share, 2),
|
| 193 |
+
'pricing_war_intensity': np.round(np.clip(pricing_war_intensity, 0, 1), 2),
|
| 194 |
+
'our_avg_price': np.round(our_avg_price, 2),
|
| 195 |
+
'market_avg_price': np.round(market_avg_price, 2),
|
| 196 |
+
'competitive_churn_pct': np.round(np.maximum(competitive_churn_pct, 0.3), 2),
|
| 197 |
+
'net_subscriber_adds': np.round(net_adds).astype(int),
|
| 198 |
+
'competitor_promo_index': np.round(np.clip(promo_index, 0, 100), 1),
|
| 199 |
+
})
|
| 200 |
+
|
| 201 |
+
return df
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def generate_economic_impact(start_date='2023-01-01', periods=36):
|
| 205 |
+
"""Generate economic impact data showing recession effects on telecom behavior."""
|
| 206 |
+
np.random.seed(45)
|
| 207 |
+
dates = pd.date_range(start=start_date, periods=periods, freq='ME')
|
| 208 |
+
|
| 209 |
+
t = np.arange(periods)
|
| 210 |
+
|
| 211 |
+
# GDP growth rate (quarterly interpolated to monthly)
|
| 212 |
+
# Simulate a mild recession in months 12-20
|
| 213 |
+
gdp_growth = np.full(periods, 2.5)
|
| 214 |
+
for i in range(periods):
|
| 215 |
+
if 12 <= i <= 15:
|
| 216 |
+
gdp_growth[i] = 2.5 - 1.5 * (i - 11) / 4 # declining
|
| 217 |
+
elif 16 <= i <= 20:
|
| 218 |
+
gdp_growth[i] = 0.8 + 0.4 * (i - 16) # recovering
|
| 219 |
+
else:
|
| 220 |
+
gdp_growth[i] = 2.2 + 0.3 * np.sin(2 * np.pi * i / 12)
|
| 221 |
+
gdp_growth += np.random.normal(0, 0.15, periods)
|
| 222 |
+
|
| 223 |
+
# Consumer confidence index (0-100)
|
| 224 |
+
cci = 72 + 5 * (gdp_growth - 2.0) + np.random.normal(0, 1.5, periods)
|
| 225 |
+
|
| 226 |
+
# Unemployment rate
|
| 227 |
+
unemployment = 4.2 - 0.5 * (gdp_growth - 2.0) + np.random.normal(0, 0.2, periods)
|
| 228 |
+
|
| 229 |
+
# Economic recession indicator
|
| 230 |
+
recession_indicator = (gdp_growth < 1.5).astype(int)
|
| 231 |
+
|
| 232 |
+
# Impact on telecom metrics
|
| 233 |
+
# ARPU impact: drops during recession
|
| 234 |
+
arpu_index = 100 + 3 * (gdp_growth - 2.0) + np.random.normal(0, 0.8, periods)
|
| 235 |
+
|
| 236 |
+
# Plan downgrade rate: increases during recession
|
| 237 |
+
downgrade_rate = 3.5 - 1.5 * (gdp_growth - 2.0) / 2 + np.random.normal(0, 0.3, periods)
|
| 238 |
+
downgrade_rate = np.maximum(downgrade_rate, 1.0)
|
| 239 |
+
|
| 240 |
+
# Payment delinquency rate
|
| 241 |
+
delinquency_rate = 4.0 - 1.2 * (gdp_growth - 2.0) + np.random.normal(0, 0.4, periods)
|
| 242 |
+
delinquency_rate = np.maximum(delinquency_rate, 1.5)
|
| 243 |
+
|
| 244 |
+
# New subscription rate
|
| 245 |
+
new_sub_rate = 5.0 + 1.5 * (gdp_growth - 2.0) + np.random.normal(0, 0.3, periods)
|
| 246 |
+
|
| 247 |
+
# Revenue at risk ($M)
|
| 248 |
+
revenue_at_risk = (15.0 - 5 * (gdp_growth - 2.0) / 2 +
|
| 249 |
+
np.random.normal(0, 1.0, periods))
|
| 250 |
+
revenue_at_risk = np.maximum(revenue_at_risk, 2.0)
|
| 251 |
+
|
| 252 |
+
# Customer sentiment index
|
| 253 |
+
sentiment_index = 60 + 8 * (gdp_growth - 2.0) + np.random.normal(0, 2, periods)
|
| 254 |
+
|
| 255 |
+
df = pd.DataFrame({
|
| 256 |
+
'date': dates,
|
| 257 |
+
'gdp_growth_rate': np.round(gdp_growth, 2),
|
| 258 |
+
'consumer_confidence_index': np.round(np.clip(cci, 30, 100), 1),
|
| 259 |
+
'unemployment_rate': np.round(np.clip(unemployment, 2.5, 8.0), 1),
|
| 260 |
+
'recession_indicator': recession_indicator,
|
| 261 |
+
'arpu_index': np.round(arpu_index, 1),
|
| 262 |
+
'plan_downgrade_rate': np.round(downgrade_rate, 2),
|
| 263 |
+
'payment_delinquency_rate': np.round(delinquency_rate, 2),
|
| 264 |
+
'new_subscription_rate': np.round(np.maximum(new_sub_rate, 1.0), 2),
|
| 265 |
+
'revenue_at_risk_millions': np.round(revenue_at_risk, 2),
|
| 266 |
+
'customer_sentiment_index': np.round(np.clip(sentiment_index, 20, 100), 1),
|
| 267 |
+
})
|
| 268 |
+
|
| 269 |
+
return df
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
def generate_all_forecasting_data(output_dir='data/forecasting'):
|
| 273 |
+
"""Generate all time series datasets and save to CSV."""
|
| 274 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 275 |
+
|
| 276 |
+
print("Generating Time Series Forecasting Data...")
|
| 277 |
+
print("=" * 50)
|
| 278 |
+
|
| 279 |
+
# 1. Seasonal Usage Patterns
|
| 280 |
+
seasonal_df, events = generate_seasonal_usage()
|
| 281 |
+
seasonal_df.to_csv(os.path.join(output_dir, 'seasonal_usage.csv'), index=False)
|
| 282 |
+
with open(os.path.join(output_dir, 'event_log.json'), 'w') as f:
|
| 283 |
+
json.dump(events, f, indent=2)
|
| 284 |
+
print(f"✓ Seasonal Usage: {len(seasonal_df)} monthly records, {len(events)} special events")
|
| 285 |
+
|
| 286 |
+
# 2. Technology Adoption
|
| 287 |
+
tech_df = generate_tech_adoption()
|
| 288 |
+
tech_df.to_csv(os.path.join(output_dir, 'tech_adoption.csv'), index=False)
|
| 289 |
+
print(f"✓ Tech Adoption: {len(tech_df)} monthly records (5G: {tech_df['five_g_adoption_pct'].iloc[-1]:.1f}%)")
|
| 290 |
+
|
| 291 |
+
# 3. Competitive Dynamics
|
| 292 |
+
comp_df = generate_competitive_dynamics()
|
| 293 |
+
comp_df.to_csv(os.path.join(output_dir, 'competitive_dynamics.csv'), index=False)
|
| 294 |
+
print(f"✓ Competitive Dynamics: {len(comp_df)} records (Market Share: {comp_df['our_market_share'].iloc[-1]:.1f}%)")
|
| 295 |
+
|
| 296 |
+
# 4. Economic Impact
|
| 297 |
+
econ_df = generate_economic_impact()
|
| 298 |
+
econ_df.to_csv(os.path.join(output_dir, 'economic_impact.csv'), index=False)
|
| 299 |
+
recession_months = econ_df['recession_indicator'].sum()
|
| 300 |
+
print(f"✓ Economic Impact: {len(econ_df)} records ({recession_months} recession months)")
|
| 301 |
+
|
| 302 |
+
print("=" * 50)
|
| 303 |
+
print(f"All forecasting data saved to {output_dir}/")
|
| 304 |
+
|
| 305 |
+
return seasonal_df, tech_df, comp_df, econ_df
|
| 306 |
+
|
| 307 |
+
|
| 308 |
+
if __name__ == '__main__':
|
| 309 |
+
generate_all_forecasting_data()
|
src/utils/enhanced_data_generator.py
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Enhanced Telecommunications Data Generator - Complete Implementation
|
| 3 |
+
Implements all data sources from Technical Requirements Document
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import pandas as pd
|
| 7 |
+
import numpy as np
|
| 8 |
+
from datetime import datetime, timedelta
|
| 9 |
+
from faker import Faker
|
| 10 |
+
import random
|
| 11 |
+
from tqdm import tqdm
|
| 12 |
+
|
| 13 |
+
np.random.seed(42)
|
| 14 |
+
random.seed(42)
|
| 15 |
+
fake = Faker()
|
| 16 |
+
Faker.seed(42)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class EnhancedTelecomDataGenerator:
|
| 20 |
+
"""
|
| 21 |
+
Comprehensive data generator implementing ALL technical requirements:
|
| 22 |
+
- Device Data (OS, apps, performance)
|
| 23 |
+
- Competitive Intelligence
|
| 24 |
+
- External Data (weather, events, demographics)
|
| 25 |
+
- Enhanced network metrics
|
| 26 |
+
- Customer journey analytics
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
def __init__(self, num_customers=100000):
|
| 30 |
+
self.num_customers = num_customers
|
| 31 |
+
self.start_date = pd.to_datetime('2022-01-01')
|
| 32 |
+
self.end_date = pd.to_datetime('2024-12-31')
|
| 33 |
+
|
| 34 |
+
# Device configurations
|
| 35 |
+
self.os_versions = {
|
| 36 |
+
'iOS': ['16.0', '16.1', '16.2', '17.0', '17.1', '17.2'],
|
| 37 |
+
'Android': ['12', '13', '14'],
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
self.popular_apps = [
|
| 41 |
+
'WhatsApp', 'Facebook', 'Instagram', 'YouTube', 'TikTok',
|
| 42 |
+
'Netflix', 'Spotify', 'Gmail', 'Google Maps', 'Twitter',
|
| 43 |
+
'Snapchat', 'LinkedIn', 'Uber', 'Amazon', 'Zoom'
|
| 44 |
+
]
|
| 45 |
+
|
| 46 |
+
# Competitor data
|
| 47 |
+
self.competitors = ['Verizon', 'AT&T', 'T-Mobile', 'Sprint']
|
| 48 |
+
|
| 49 |
+
# Weather conditions
|
| 50 |
+
self.weather_conditions = ['Clear', 'Cloudy', 'Rainy', 'Stormy', 'Snowy', 'Foggy']
|
| 51 |
+
|
| 52 |
+
# Event types
|
| 53 |
+
self.event_types = ['Concert', 'Sports', 'Festival', 'Convention', 'Holiday']
|
| 54 |
+
|
| 55 |
+
print(f"Initialized Enhanced Data Generator for {num_customers:,} customers")
|
| 56 |
+
|
| 57 |
+
def generate_device_data(self, customers_df):
|
| 58 |
+
"""
|
| 59 |
+
Generate comprehensive device data:
|
| 60 |
+
- OS version & update history
|
| 61 |
+
- App usage patterns
|
| 62 |
+
- Device performance metrics
|
| 63 |
+
- Battery health & storage
|
| 64 |
+
"""
|
| 65 |
+
print("\n📱 Generating Device Performance Data...")
|
| 66 |
+
|
| 67 |
+
device_data = []
|
| 68 |
+
|
| 69 |
+
for _, customer in tqdm(customers_df.iterrows(), total=len(customers_df), desc="Device data"):
|
| 70 |
+
# Determine OS from device manufacturer
|
| 71 |
+
manufacturer = customer['device_manufacturer']
|
| 72 |
+
if manufacturer == 'Apple':
|
| 73 |
+
os_type = 'iOS'
|
| 74 |
+
os_version = random.choice(self.os_versions['iOS'])
|
| 75 |
+
else:
|
| 76 |
+
os_type = 'Android'
|
| 77 |
+
os_version = random.choice(self.os_versions['Android'])
|
| 78 |
+
|
| 79 |
+
# App usage (select 5-12 apps per customer)
|
| 80 |
+
num_apps = random.randint(5, 12)
|
| 81 |
+
user_apps = random.sample(self.popular_apps, num_apps)
|
| 82 |
+
|
| 83 |
+
# Performance metrics
|
| 84 |
+
device = {
|
| 85 |
+
'customer_id': customer['customer_id'],
|
| 86 |
+
'os_type': os_type,
|
| 87 |
+
'os_version': os_version,
|
| 88 |
+
'os_last_updated': (self.end_date - timedelta(days=random.randint(0, 180))).date(),
|
| 89 |
+
'storage_total_gb': random.choice([64, 128, 256, 512]),
|
| 90 |
+
'storage_used_pct': random.uniform(40, 95),
|
| 91 |
+
'battery_health_pct': max(70, 100 - customer['device_age_months'] * 1.5),
|
| 92 |
+
'avg_battery_drain_pct_per_hour': random.uniform(3, 15),
|
| 93 |
+
'apps_installed': len(user_apps),
|
| 94 |
+
'top_apps': ','.join(user_apps[:5]),
|
| 95 |
+
'avg_daily_screen_time_hours': random.uniform(2, 8),
|
| 96 |
+
'data_saver_enabled': random.random() < 0.3,
|
| 97 |
+
'background_data_restricted': random.random() < 0.25,
|
| 98 |
+
'wifi_calling_enabled': random.random() < 0.6,
|
| 99 |
+
'volte_enabled': random.random() < 0.8,
|
| 100 |
+
'5g_enabled': random.random() < 0.7,
|
| 101 |
+
'device_temperature_avg_celsius': random.uniform(25, 40),
|
| 102 |
+
'crash_count_last_month': np.random.poisson(1),
|
| 103 |
+
'avg_app_load_time_sec': random.uniform(0.5, 3.0),
|
| 104 |
+
'memory_pressure_high_pct': random.uniform(5, 40),
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
device_data.append(device)
|
| 108 |
+
|
| 109 |
+
df = pd.DataFrame(device_data)
|
| 110 |
+
print(f"✅ Generated device data for {len(df):,} customers")
|
| 111 |
+
return df
|
| 112 |
+
|
| 113 |
+
def generate_competitive_intelligence(self):
|
| 114 |
+
"""
|
| 115 |
+
Generate market intelligence data:
|
| 116 |
+
- Competitor pricing
|
| 117 |
+
- Market share trends
|
| 118 |
+
- Promotional campaigns
|
| 119 |
+
- Customer migration patterns
|
| 120 |
+
"""
|
| 121 |
+
print("\n🏢 Generating Competitive Intelligence Data...")
|
| 122 |
+
|
| 123 |
+
months = pd.date_range(start=self.start_date, end=self.end_date, freq='MS')
|
| 124 |
+
|
| 125 |
+
market_data = []
|
| 126 |
+
|
| 127 |
+
for month in tqdm(months, desc="Market analysis"):
|
| 128 |
+
for competitor in self.competitors:
|
| 129 |
+
# Pricing data
|
| 130 |
+
base_price = random.uniform(40, 120)
|
| 131 |
+
promo_active = random.random() < 0.3
|
| 132 |
+
|
| 133 |
+
market_entry = {
|
| 134 |
+
'month': month.date(),
|
| 135 |
+
'competitor': competitor,
|
| 136 |
+
'base_plan_price': round(base_price, 2),
|
| 137 |
+
'unlimited_plan_price': round(base_price * 1.8, 2),
|
| 138 |
+
'family_plan_price': round(base_price * 2.5, 2),
|
| 139 |
+
'promotion_active': promo_active,
|
| 140 |
+
'promotion_discount_pct': random.uniform(10, 30) if promo_active else 0,
|
| 141 |
+
'market_share_pct': random.uniform(15, 30),
|
| 142 |
+
'customer_satisfaction_score': random.uniform(3.5, 4.8),
|
| 143 |
+
'network_quality_score': random.uniform(7, 9.5),
|
| 144 |
+
'5g_coverage_pct': random.uniform(40, 85),
|
| 145 |
+
'avg_download_speed_mbps': random.uniform(50, 300),
|
| 146 |
+
'churn_rate_pct': random.uniform(1.5, 3.5),
|
| 147 |
+
'new_customer_acquisitions': random.randint(50000, 200000),
|
| 148 |
+
'advertising_spend_millions': random.uniform(5, 25),
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
market_data.append(market_entry)
|
| 152 |
+
|
| 153 |
+
df = pd.DataFrame(market_data)
|
| 154 |
+
print(f"✅ Generated {len(df):,} market intelligence records")
|
| 155 |
+
return df
|
| 156 |
+
|
| 157 |
+
def generate_external_data(self, towers_df):
|
| 158 |
+
"""
|
| 159 |
+
Generate external data sources:
|
| 160 |
+
- Weather conditions by location
|
| 161 |
+
- Local events
|
| 162 |
+
- Demographic/census data
|
| 163 |
+
- Economic indicators
|
| 164 |
+
"""
|
| 165 |
+
print("\n🌍 Generating External Data Sources...")
|
| 166 |
+
|
| 167 |
+
# Weather data (daily by tower location)
|
| 168 |
+
print(" Generating weather data...")
|
| 169 |
+
weather_data = []
|
| 170 |
+
dates = pd.date_range(start=self.end_date - timedelta(days=90), end=self.end_date, freq='D')
|
| 171 |
+
|
| 172 |
+
# Sample subset of towers for weather
|
| 173 |
+
sample_towers = towers_df.sample(n=min(200, len(towers_df)), random_state=42)
|
| 174 |
+
|
| 175 |
+
for tower_id, tower in tqdm(sample_towers.iterrows(), total=len(sample_towers), desc="Weather"):
|
| 176 |
+
for date in dates:
|
| 177 |
+
weather = {
|
| 178 |
+
'date': date.date(),
|
| 179 |
+
'tower_id': tower['tower_id'],
|
| 180 |
+
'city': tower['city'],
|
| 181 |
+
'temperature_celsius': random.uniform(-10, 35),
|
| 182 |
+
'humidity_pct': random.uniform(30, 90),
|
| 183 |
+
'precipitation_mm': max(0, np.random.exponential(2)),
|
| 184 |
+
'wind_speed_kmh': random.uniform(5, 50),
|
| 185 |
+
'condition': random.choice(self.weather_conditions),
|
| 186 |
+
'severe_weather': random.random() < 0.05,
|
| 187 |
+
}
|
| 188 |
+
weather_data.append(weather)
|
| 189 |
+
|
| 190 |
+
weather_df = pd.DataFrame(weather_data)
|
| 191 |
+
|
| 192 |
+
# Events data
|
| 193 |
+
print(" Generating events data...")
|
| 194 |
+
events_data = []
|
| 195 |
+
num_events = 500
|
| 196 |
+
|
| 197 |
+
for i in range(num_events):
|
| 198 |
+
event_date = fake.date_between(start_date=self.start_date, end_date=self.end_date)
|
| 199 |
+
|
| 200 |
+
event = {
|
| 201 |
+
'event_id': f'EVT{i+1:05d}',
|
| 202 |
+
'event_name': f'{random.choice(self.event_types)} {i+1}',
|
| 203 |
+
'event_type': random.choice(self.event_types),
|
| 204 |
+
'event_date': event_date,
|
| 205 |
+
'city': random.choice(towers_df['city'].unique()),
|
| 206 |
+
'expected_attendance': random.randint(1000, 100000),
|
| 207 |
+
'duration_hours': random.randint(2, 48),
|
| 208 |
+
}
|
| 209 |
+
events_data.append(event)
|
| 210 |
+
|
| 211 |
+
events_df = pd.DataFrame(events_data)
|
| 212 |
+
|
| 213 |
+
# Demographics/Census data by city
|
| 214 |
+
print(" Generating demographic data...")
|
| 215 |
+
cities = towers_df['city'].unique()
|
| 216 |
+
demographics_data = []
|
| 217 |
+
|
| 218 |
+
for city in cities:
|
| 219 |
+
demo = {
|
| 220 |
+
'city': city,
|
| 221 |
+
'population': random.randint(100000, 5000000),
|
| 222 |
+
'median_age': random.uniform(30, 45),
|
| 223 |
+
'median_income': random.randint(40000, 100000),
|
| 224 |
+
'unemployment_rate_pct': random.uniform(3, 8),
|
| 225 |
+
'college_educated_pct': random.uniform(25, 60),
|
| 226 |
+
'homeownership_rate_pct': random.uniform(40, 70),
|
| 227 |
+
'population_density_per_sqkm': random.randint(100, 10000),
|
| 228 |
+
'urban_classification': random.choice(['Urban', 'Suburban', 'Rural']),
|
| 229 |
+
}
|
| 230 |
+
demographics_data.append(demo)
|
| 231 |
+
|
| 232 |
+
demographics_df = pd.DataFrame(demographics_data)
|
| 233 |
+
|
| 234 |
+
print(f"✅ Weather: {len(weather_df):,} records")
|
| 235 |
+
print(f"✅ Events: {len(events_df):,} records")
|
| 236 |
+
print(f"✅ Demographics: {len(demographics_df):,} cities")
|
| 237 |
+
|
| 238 |
+
return {
|
| 239 |
+
'weather': weather_df,
|
| 240 |
+
'events': events_df,
|
| 241 |
+
'demographics': demographics_df
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
def generate_customer_journey_data(self, customers_df):
|
| 245 |
+
"""
|
| 246 |
+
Generate customer journey analytics:
|
| 247 |
+
- Lifecycle stages
|
| 248 |
+
- Service interaction history
|
| 249 |
+
- Payment behavior patterns
|
| 250 |
+
- Customer segmentation
|
| 251 |
+
"""
|
| 252 |
+
print("\n👤 Generating Customer Journey Data...")
|
| 253 |
+
|
| 254 |
+
journey_data = []
|
| 255 |
+
|
| 256 |
+
for _, customer in tqdm(customers_df.iterrows(), total=len(customers_df), desc="Customer journeys"):
|
| 257 |
+
# Lifecycle stage based on tenure
|
| 258 |
+
tenure = customer['tenure_months']
|
| 259 |
+
if tenure < 3:
|
| 260 |
+
lifecycle_stage = 'New'
|
| 261 |
+
engagement_score = random.uniform(6, 9)
|
| 262 |
+
elif tenure < 12:
|
| 263 |
+
lifecycle_stage = 'Growing'
|
| 264 |
+
engagement_score = random.uniform(7, 10)
|
| 265 |
+
elif tenure < 36:
|
| 266 |
+
lifecycle_stage = 'Mature'
|
| 267 |
+
engagement_score = random.uniform(5, 9)
|
| 268 |
+
else:
|
| 269 |
+
lifecycle_stage = 'Tenured'
|
| 270 |
+
engagement_score = random.uniform(4, 8)
|
| 271 |
+
|
| 272 |
+
# Payment behavior
|
| 273 |
+
payment_score = random.uniform(1, 10)
|
| 274 |
+
late_payment_risk = 'Low' if payment_score > 7 else ('Medium' if payment_score > 4 else 'High')
|
| 275 |
+
|
| 276 |
+
journey = {
|
| 277 |
+
'customer_id': customer['customer_id'],
|
| 278 |
+
'lifecycle_stage': lifecycle_stage,
|
| 279 |
+
'engagement_score': round(engagement_score, 2),
|
| 280 |
+
'value_segment': random.choice(['High Value', 'Medium Value', 'Low Value']),
|
| 281 |
+
'loyalty_tier': random.choice(['Bronze', 'Silver', 'Gold', 'Platinum']),
|
| 282 |
+
'payment_behavior_score': round(payment_score, 2),
|
| 283 |
+
'late_payment_risk': late_payment_risk,
|
| 284 |
+
'total_interactions': np.random.poisson(tenure * 0.3),
|
| 285 |
+
'positive_interactions_pct': random.uniform(60, 95),
|
| 286 |
+
'nps_score': random.randint(-100, 100),
|
| 287 |
+
'referrals_made': customer.get('referral_count', 0),
|
| 288 |
+
'upsell_opportunities': np.random.poisson(2),
|
| 289 |
+
'cross_sell_score': random.uniform(0, 10),
|
| 290 |
+
'reactivation_risk': random.uniform(0, 1),
|
| 291 |
+
'social_influence_score': random.uniform(0, 10),
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
journey_data.append(journey)
|
| 295 |
+
|
| 296 |
+
df = pd.DataFrame(journey_data)
|
| 297 |
+
print(f"✅ Generated journey data for {len(df):,} customers")
|
| 298 |
+
return df
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
def main():
|
| 302 |
+
"""Generate all enhanced datasets"""
|
| 303 |
+
print("="*80)
|
| 304 |
+
print("ENHANCED TELECOMMUNICATIONS DATA GENERATOR")
|
| 305 |
+
print("="*80)
|
| 306 |
+
|
| 307 |
+
# First generate base data using original generator
|
| 308 |
+
from synthetic_data_generator import TelecomDataGenerator
|
| 309 |
+
|
| 310 |
+
base_gen = TelecomDataGenerator(num_customers=100000, num_towers=1000)
|
| 311 |
+
|
| 312 |
+
print("\n📊 Generating Base Data...")
|
| 313 |
+
customers_df = base_gen.generate_customer_demographics()
|
| 314 |
+
towers_df = base_gen.generate_network_infrastructure()
|
| 315 |
+
|
| 316 |
+
# Initialize enhanced generator
|
| 317 |
+
enhanced_gen = EnhancedTelecomDataGenerator(num_customers=len(customers_df))
|
| 318 |
+
|
| 319 |
+
# Generate enhanced data
|
| 320 |
+
device_df = enhanced_gen.generate_device_data(customers_df)
|
| 321 |
+
competitive_df = enhanced_gen.generate_competitive_intelligence()
|
| 322 |
+
external_data = enhanced_gen.generate_external_data(towers_df)
|
| 323 |
+
journey_df = enhanced_gen.generate_customer_journey_data(customers_df)
|
| 324 |
+
|
| 325 |
+
# Save all datasets
|
| 326 |
+
print("\n💾 Saving Enhanced Datasets...")
|
| 327 |
+
|
| 328 |
+
device_df.to_csv('data/synthetic/device_data.csv', index=False)
|
| 329 |
+
print(" ✅ Saved device_data.csv")
|
| 330 |
+
|
| 331 |
+
competitive_df.to_csv('data/synthetic/competitive_intelligence.csv', index=False)
|
| 332 |
+
print(" ✅ Saved competitive_intelligence.csv")
|
| 333 |
+
|
| 334 |
+
external_data['weather'].to_csv('data/synthetic/weather_data.csv', index=False)
|
| 335 |
+
print(" ✅ Saved weather_data.csv")
|
| 336 |
+
|
| 337 |
+
external_data['events'].to_csv('data/synthetic/events_data.csv', index=False)
|
| 338 |
+
print(" ✅ Saved events_data.csv")
|
| 339 |
+
|
| 340 |
+
external_data['demographics'].to_csv('data/synthetic/demographics_data.csv', index=False)
|
| 341 |
+
print(" ✅ Saved demographics_data.csv")
|
| 342 |
+
|
| 343 |
+
journey_df.to_csv('data/synthetic/customer_journey.csv', index=False)
|
| 344 |
+
print(" ✅ Saved customer_journey.csv")
|
| 345 |
+
|
| 346 |
+
print("\n" + "="*80)
|
| 347 |
+
print("ENHANCED DATA GENERATION COMPLETE")
|
| 348 |
+
print("="*80)
|
| 349 |
+
print(f"\n📈 Summary:")
|
| 350 |
+
print(f" - Device Data: {len(device_df):,} records")
|
| 351 |
+
print(f" - Competitive Intelligence: {len(competitive_df):,} records")
|
| 352 |
+
print(f" - Weather Data: {len(external_data['weather']):,} records")
|
| 353 |
+
print(f" - Events Data: {len(external_data['events']):,} records")
|
| 354 |
+
print(f" - Demographics: {len(external_data['demographics']):,} cities")
|
| 355 |
+
print(f" - Customer Journey: {len(journey_df):,} customers")
|
| 356 |
+
print("\n✅ All enhanced datasets saved to 'data/synthetic/' directory")
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
if __name__ == "__main__":
|
| 360 |
+
main()
|
src/utils/synthetic_data_generator.py
ADDED
|
@@ -0,0 +1,601 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Telecommunications Synthetic Data Generator
|
| 3 |
+
|
| 4 |
+
This module generates realistic synthetic data for a telecommunications company
|
| 5 |
+
serving 10+ million customers. The data mimics real-world patterns including:
|
| 6 |
+
- Customer demographics and behavior
|
| 7 |
+
- Network performance metrics
|
| 8 |
+
- Service quality indicators
|
| 9 |
+
- Billing and usage patterns
|
| 10 |
+
- Customer service interactions
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import pandas as pd
|
| 14 |
+
import numpy as np
|
| 15 |
+
from datetime import datetime, timedelta
|
| 16 |
+
from faker import Faker
|
| 17 |
+
import random
|
| 18 |
+
import json
|
| 19 |
+
from tqdm import tqdm
|
| 20 |
+
|
| 21 |
+
# Set random seeds for reproducibility
|
| 22 |
+
np.random.seed(42)
|
| 23 |
+
random.seed(42)
|
| 24 |
+
fake = Faker()
|
| 25 |
+
Faker.seed(42)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class TelecomDataGenerator:
|
| 29 |
+
"""Generate comprehensive synthetic telecommunications data"""
|
| 30 |
+
|
| 31 |
+
def __init__(self, num_customers=100000, num_towers=1000, start_date='2022-01-01', end_date='2024-12-31'):
|
| 32 |
+
"""
|
| 33 |
+
Initialize the data generator
|
| 34 |
+
|
| 35 |
+
Parameters:
|
| 36 |
+
-----------
|
| 37 |
+
num_customers : int
|
| 38 |
+
Number of customers to generate (default: 100,000 for demo, can scale to 10M)
|
| 39 |
+
num_towers : int
|
| 40 |
+
Number of cell towers in the network
|
| 41 |
+
start_date : str
|
| 42 |
+
Start date for time-series data
|
| 43 |
+
end_date : str
|
| 44 |
+
End date for time-series data
|
| 45 |
+
"""
|
| 46 |
+
self.num_customers = num_customers
|
| 47 |
+
self.num_towers = num_towers
|
| 48 |
+
self.start_date = pd.to_datetime(start_date)
|
| 49 |
+
self.end_date = pd.to_datetime(end_date)
|
| 50 |
+
self.date_range = pd.date_range(start=self.start_date, end=self.end_date, freq='D')
|
| 51 |
+
|
| 52 |
+
# Define realistic parameters
|
| 53 |
+
self.cities = ['New York', 'Los Angeles', 'Chicago', 'Houston', 'Phoenix',
|
| 54 |
+
'Philadelphia', 'San Antonio', 'San Diego', 'Dallas', 'San Jose',
|
| 55 |
+
'Austin', 'Jacksonville', 'Fort Worth', 'Columbus', 'Indianapolis']
|
| 56 |
+
|
| 57 |
+
self.plan_types = ['Basic', 'Standard', 'Premium', 'Unlimited', 'Family', 'Enterprise']
|
| 58 |
+
self.device_manufacturers = ['Apple', 'Samsung', 'Google', 'OnePlus', 'Motorola', 'LG']
|
| 59 |
+
self.device_models = {
|
| 60 |
+
'Apple': ['iPhone 12', 'iPhone 13', 'iPhone 14', 'iPhone 15', 'iPhone SE'],
|
| 61 |
+
'Samsung': ['Galaxy S21', 'Galaxy S22', 'Galaxy S23', 'Galaxy A53', 'Galaxy Z Fold'],
|
| 62 |
+
'Google': ['Pixel 6', 'Pixel 7', 'Pixel 8', 'Pixel 7a'],
|
| 63 |
+
'OnePlus': ['OnePlus 9', 'OnePlus 10', 'OnePlus 11', 'Nord N20'],
|
| 64 |
+
'Motorola': ['Moto G Power', 'Moto G Stylus', 'Edge+'],
|
| 65 |
+
'LG': ['V60', 'Velvet', 'Wing']
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
self.complaint_types = [
|
| 69 |
+
'Network Coverage', 'Slow Data Speed', 'Call Drops', 'Billing Issue',
|
| 70 |
+
'Customer Service', 'Device Problem', 'Connectivity', 'Roaming Charges',
|
| 71 |
+
'Plan Change', 'Technical Support'
|
| 72 |
+
]
|
| 73 |
+
|
| 74 |
+
print(f"Initialized TelecomDataGenerator:")
|
| 75 |
+
print(f" - Customers: {self.num_customers:,}")
|
| 76 |
+
print(f" - Cell Towers: {self.num_towers:,}")
|
| 77 |
+
print(f" - Date Range: {self.start_date.date()} to {self.end_date.date()}")
|
| 78 |
+
print(f" - Days of Data: {len(self.date_range)}")
|
| 79 |
+
|
| 80 |
+
def generate_customer_demographics(self):
|
| 81 |
+
"""Generate customer demographic data"""
|
| 82 |
+
print("\n📊 Generating Customer Demographics...")
|
| 83 |
+
|
| 84 |
+
customers = []
|
| 85 |
+
for i in tqdm(range(self.num_customers), desc="Creating customers"):
|
| 86 |
+
# Random customer start date (weighted towards more recent)
|
| 87 |
+
tenure_days = np.random.exponential(scale=800) + 30 # Average ~2 years, min 1 month
|
| 88 |
+
tenure_days = min(tenure_days, (self.end_date - self.start_date).days)
|
| 89 |
+
service_start_date = self.end_date - timedelta(days=int(tenure_days))
|
| 90 |
+
|
| 91 |
+
# Random contract end date (1-2 years after start for contracted customers)
|
| 92 |
+
is_contracted = random.random() < 0.6 # 60% on contract
|
| 93 |
+
if is_contracted:
|
| 94 |
+
contract_end = service_start_date + timedelta(days=random.randint(365, 730))
|
| 95 |
+
else:
|
| 96 |
+
contract_end = None
|
| 97 |
+
|
| 98 |
+
manufacturer = random.choice(self.device_manufacturers)
|
| 99 |
+
|
| 100 |
+
customer = {
|
| 101 |
+
'customer_id': f'CUST{i+1:08d}',
|
| 102 |
+
'age': int(np.random.normal(42, 15)), # Average age ~42
|
| 103 |
+
'gender': random.choice(['M', 'F', 'Other']),
|
| 104 |
+
'city': random.choice(self.cities),
|
| 105 |
+
'state': fake.state_abbr(),
|
| 106 |
+
'zip_code': fake.zipcode(),
|
| 107 |
+
'service_start_date': service_start_date.date(),
|
| 108 |
+
'tenure_months': int(tenure_days / 30),
|
| 109 |
+
'plan_type': random.choice(self.plan_types),
|
| 110 |
+
'is_contracted': is_contracted,
|
| 111 |
+
'contract_end_date': contract_end.date() if contract_end else None,
|
| 112 |
+
'device_manufacturer': manufacturer,
|
| 113 |
+
'device_model': random.choice(self.device_models[manufacturer]),
|
| 114 |
+
'device_age_months': random.randint(0, 48),
|
| 115 |
+
'credit_score': int(np.random.normal(680, 80)),
|
| 116 |
+
'monthly_plan_cost': round(random.uniform(30, 150), 2),
|
| 117 |
+
'autopay_enabled': random.random() < 0.65, # 65% use autopay
|
| 118 |
+
'paperless_billing': random.random() < 0.70, # 70% paperless
|
| 119 |
+
'referral_count': np.random.poisson(0.5), # Average 0.5 referrals
|
| 120 |
+
'is_family_plan': random.random() < 0.35, # 35% family plans
|
| 121 |
+
'number_of_lines': random.choices([1, 2, 3, 4, 5], weights=[0.45, 0.25, 0.15, 0.10, 0.05])[0]
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
# Clamp age to realistic range
|
| 125 |
+
customer['age'] = max(18, min(90, customer['age']))
|
| 126 |
+
|
| 127 |
+
customers.append(customer)
|
| 128 |
+
|
| 129 |
+
df = pd.DataFrame(customers)
|
| 130 |
+
print(f"✅ Generated {len(df):,} customer records")
|
| 131 |
+
print(f" - Average age: {df['age'].mean():.1f} years")
|
| 132 |
+
print(f" - Average tenure: {df['tenure_months'].mean():.1f} months")
|
| 133 |
+
print(f" - Plan distribution:\n{df['plan_type'].value_counts()}")
|
| 134 |
+
|
| 135 |
+
return df
|
| 136 |
+
|
| 137 |
+
def generate_network_infrastructure(self):
|
| 138 |
+
"""Generate cell tower and network infrastructure data"""
|
| 139 |
+
print("\n🗼 Generating Network Infrastructure...")
|
| 140 |
+
|
| 141 |
+
towers = []
|
| 142 |
+
for i in tqdm(range(self.num_towers), desc="Creating cell towers"):
|
| 143 |
+
# Installation date (older towers = more maintenance needed)
|
| 144 |
+
installation_date = self.start_date - timedelta(days=random.randint(365, 3650))
|
| 145 |
+
|
| 146 |
+
tower = {
|
| 147 |
+
'tower_id': f'TOWER{i+1:05d}',
|
| 148 |
+
'city': random.choice(self.cities),
|
| 149 |
+
'latitude': round(random.uniform(25.0, 48.0), 6),
|
| 150 |
+
'longitude': round(random.uniform(-125.0, -65.0), 6),
|
| 151 |
+
'tower_type': random.choices(['Macro', 'Micro', 'Small Cell'], weights=[0.6, 0.25, 0.15])[0],
|
| 152 |
+
'installation_date': installation_date.date(),
|
| 153 |
+
'equipment_age_years': ((self.end_date - installation_date).days / 365),
|
| 154 |
+
'max_capacity_mbps': random.choice([1000, 2000, 5000, 10000]),
|
| 155 |
+
'coverage_radius_km': random.uniform(0.5, 20.0),
|
| 156 |
+
'technology': random.choices(['4G LTE', '5G'], weights=[0.65, 0.35])[0],
|
| 157 |
+
'frequency_band': random.choice(['700MHz', '850MHz', '1900MHz', '2.5GHz', '28GHz', '39GHz']),
|
| 158 |
+
'last_maintenance_date': (self.end_date - timedelta(days=random.randint(0, 180))).date(),
|
| 159 |
+
'maintenance_cost_monthly': round(random.uniform(500, 3000), 2),
|
| 160 |
+
'status': random.choices(['Active', 'Degraded', 'Maintenance'], weights=[0.90, 0.07, 0.03])[0]
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
towers.append(tower)
|
| 164 |
+
|
| 165 |
+
df = pd.DataFrame(towers)
|
| 166 |
+
print(f"✅ Generated {len(df):,} cell tower records")
|
| 167 |
+
print(f" - Technology distribution:\n{df['technology'].value_counts()}")
|
| 168 |
+
print(f" - Tower types:\n{df['tower_type'].value_counts()}")
|
| 169 |
+
|
| 170 |
+
return df
|
| 171 |
+
|
| 172 |
+
def generate_customer_usage_data(self, customer_df, sample_days=90):
|
| 173 |
+
"""
|
| 174 |
+
Generate daily customer usage data
|
| 175 |
+
|
| 176 |
+
Parameters:
|
| 177 |
+
-----------
|
| 178 |
+
customer_df : DataFrame
|
| 179 |
+
Customer demographics data
|
| 180 |
+
sample_days : int
|
| 181 |
+
Number of recent days to generate (default: 90 for demo)
|
| 182 |
+
"""
|
| 183 |
+
print(f"\n📱 Generating Customer Usage Data (last {sample_days} days)...")
|
| 184 |
+
|
| 185 |
+
# Generate for last N days only to keep data manageable
|
| 186 |
+
recent_dates = pd.date_range(end=self.end_date, periods=sample_days, freq='D')
|
| 187 |
+
|
| 188 |
+
usage_data = []
|
| 189 |
+
|
| 190 |
+
# Sample subset of customers for detailed usage data
|
| 191 |
+
sample_size = min(10000, len(customer_df)) # Max 10k customers for detailed daily data
|
| 192 |
+
sampled_customers = customer_df.sample(n=sample_size, random_state=42)
|
| 193 |
+
|
| 194 |
+
print(f" Generating usage for {sample_size:,} customers over {sample_days} days...")
|
| 195 |
+
|
| 196 |
+
for _, customer in tqdm(sampled_customers.iterrows(), total=len(sampled_customers), desc="Customer usage"):
|
| 197 |
+
# Customer-specific usage patterns
|
| 198 |
+
base_data_gb = random.uniform(5, 50) # Base daily data usage in GB
|
| 199 |
+
base_voice_min = random.randint(10, 120) # Base daily voice minutes
|
| 200 |
+
|
| 201 |
+
for date in recent_dates:
|
| 202 |
+
# Only generate data if customer was active on that date
|
| 203 |
+
if date.date() >= customer['service_start_date']:
|
| 204 |
+
# Add weekly and weekend patterns
|
| 205 |
+
is_weekend = date.dayofweek >= 5
|
| 206 |
+
weekend_factor = 1.3 if is_weekend else 1.0
|
| 207 |
+
|
| 208 |
+
# Add some randomness and trends
|
| 209 |
+
daily_factor = random.gauss(1.0, 0.3)
|
| 210 |
+
|
| 211 |
+
usage = {
|
| 212 |
+
'customer_id': customer['customer_id'],
|
| 213 |
+
'date': date.date(),
|
| 214 |
+
'data_usage_gb': max(0, base_data_gb * weekend_factor * daily_factor),
|
| 215 |
+
'voice_minutes': max(0, int(base_voice_min * weekend_factor * daily_factor)),
|
| 216 |
+
'sms_count': np.random.poisson(15),
|
| 217 |
+
'roaming_minutes': np.random.poisson(2) if random.random() < 0.1 else 0,
|
| 218 |
+
'international_calls_min': np.random.poisson(5) if random.random() < 0.05 else 0,
|
| 219 |
+
'peak_hour_usage_gb': max(0, base_data_gb * 0.4 * daily_factor), # 40% during peak
|
| 220 |
+
'data_session_count': random.randint(20, 200),
|
| 221 |
+
'avg_session_duration_min': random.uniform(5, 45)
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
usage_data.append(usage)
|
| 225 |
+
|
| 226 |
+
df = pd.DataFrame(usage_data)
|
| 227 |
+
print(f"✅ Generated {len(df):,} usage records")
|
| 228 |
+
print(f" - Average daily data usage: {df['data_usage_gb'].mean():.2f} GB")
|
| 229 |
+
print(f" - Average voice minutes: {df['voice_minutes'].mean():.1f} min/day")
|
| 230 |
+
|
| 231 |
+
return df
|
| 232 |
+
|
| 233 |
+
def generate_network_performance_data(self, tower_df, sample_days=30):
|
| 234 |
+
"""
|
| 235 |
+
Generate hourly network performance metrics per tower
|
| 236 |
+
|
| 237 |
+
Parameters:
|
| 238 |
+
-----------
|
| 239 |
+
tower_df : DataFrame
|
| 240 |
+
Network infrastructure data
|
| 241 |
+
sample_days : int
|
| 242 |
+
Number of recent days to generate (default: 30)
|
| 243 |
+
"""
|
| 244 |
+
print(f"\n📡 Generating Network Performance Data (last {sample_days} days, hourly)...")
|
| 245 |
+
|
| 246 |
+
recent_dates = pd.date_range(end=self.end_date, periods=sample_days*24, freq='H')
|
| 247 |
+
|
| 248 |
+
performance_data = []
|
| 249 |
+
|
| 250 |
+
# Sample subset of towers for hourly data
|
| 251 |
+
sample_size = min(200, len(tower_df)) # Max 200 towers for detailed hourly data
|
| 252 |
+
sampled_towers = tower_df.sample(n=sample_size, random_state=42)
|
| 253 |
+
|
| 254 |
+
print(f" Generating metrics for {sample_size:,} towers over {len(recent_dates):,} hours...")
|
| 255 |
+
|
| 256 |
+
for _, tower in tqdm(sampled_towers.iterrows(), total=len(sampled_towers), desc="Tower metrics"):
|
| 257 |
+
# Tower-specific baseline performance
|
| 258 |
+
base_quality = random.uniform(0.85, 0.99) # Base service quality
|
| 259 |
+
base_latency = random.uniform(10, 50) # Base latency in ms
|
| 260 |
+
|
| 261 |
+
for timestamp in recent_dates:
|
| 262 |
+
# Time-of-day patterns
|
| 263 |
+
hour = timestamp.hour
|
| 264 |
+
is_peak = 9 <= hour <= 11 or 17 <= hour <= 21 # Peak hours
|
| 265 |
+
peak_factor = 1.5 if is_peak else 1.0
|
| 266 |
+
|
| 267 |
+
# Weather impact (random events)
|
| 268 |
+
weather_impact = 0.95 if random.random() < 0.05 else 1.0 # 5% chance of weather issues
|
| 269 |
+
|
| 270 |
+
# Equipment degradation
|
| 271 |
+
degradation = 1.0 - (tower['equipment_age_years'] * 0.01) # 1% per year
|
| 272 |
+
|
| 273 |
+
combined_factor = peak_factor * weather_impact * degradation
|
| 274 |
+
|
| 275 |
+
metrics = {
|
| 276 |
+
'tower_id': tower['tower_id'],
|
| 277 |
+
'timestamp': timestamp,
|
| 278 |
+
'bandwidth_utilization_pct': min(100, random.uniform(20, 85) * combined_factor),
|
| 279 |
+
'latency_ms': max(5, base_latency * (2 - weather_impact)),
|
| 280 |
+
'packet_loss_pct': max(0, random.uniform(0.1, 2.0) * (2 - weather_impact)),
|
| 281 |
+
'signal_strength_dbm': random.uniform(-110, -40), # Typical range
|
| 282 |
+
'sinr_db': random.uniform(0, 30), # Signal-to-Interference-plus-Noise Ratio
|
| 283 |
+
'throughput_mbps': min(tower['max_capacity_mbps'],
|
| 284 |
+
random.uniform(100, tower['max_capacity_mbps'] * 0.8) * degradation),
|
| 285 |
+
'active_users': int(random.uniform(10, 500) * peak_factor),
|
| 286 |
+
'handover_success_rate_pct': base_quality * 100 * weather_impact,
|
| 287 |
+
'call_setup_success_rate_pct': base_quality * 100 * weather_impact,
|
| 288 |
+
'data_session_setup_success_rate_pct': base_quality * 100 * weather_impact,
|
| 289 |
+
'availability_pct': 100 if random.random() < 0.999 else 0, # 99.9% uptime target
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
performance_data.append(metrics)
|
| 293 |
+
|
| 294 |
+
df = pd.DataFrame(performance_data)
|
| 295 |
+
print(f"✅ Generated {len(df):,} network performance records")
|
| 296 |
+
print(f" - Average latency: {df['latency_ms'].mean():.2f} ms")
|
| 297 |
+
print(f" - Average utilization: {df['bandwidth_utilization_pct'].mean():.1f}%")
|
| 298 |
+
print(f" - Average availability: {df['availability_pct'].mean():.3f}%")
|
| 299 |
+
|
| 300 |
+
return df
|
| 301 |
+
|
| 302 |
+
def generate_service_quality_metrics(self, customer_df, tower_df, num_records=50000):
|
| 303 |
+
"""Generate service quality events and measurements"""
|
| 304 |
+
print(f"\n⚡ Generating Service Quality Metrics...")
|
| 305 |
+
|
| 306 |
+
quality_data = []
|
| 307 |
+
|
| 308 |
+
for i in tqdm(range(num_records), desc="Quality events"):
|
| 309 |
+
customer = customer_df.sample(1).iloc[0]
|
| 310 |
+
tower = tower_df.sample(1).iloc[0]
|
| 311 |
+
event_date = fake.date_time_between(start_date=self.start_date, end_date=self.end_date)
|
| 312 |
+
|
| 313 |
+
# Service quality varies by tower and random factors
|
| 314 |
+
base_quality = random.uniform(0.7, 0.99)
|
| 315 |
+
|
| 316 |
+
event = {
|
| 317 |
+
'event_id': f'QOS{i+1:08d}',
|
| 318 |
+
'customer_id': customer['customer_id'],
|
| 319 |
+
'tower_id': tower['tower_id'],
|
| 320 |
+
'timestamp': event_date,
|
| 321 |
+
'event_type': random.choice(['Speed Test', 'Call Drop', 'Connection Failure',
|
| 322 |
+
'Slow Data', 'Network Timeout', 'Poor Voice Quality']),
|
| 323 |
+
'call_drop_occurred': random.random() > base_quality,
|
| 324 |
+
'download_speed_mbps': max(1, random.gauss(50, 30)),
|
| 325 |
+
'upload_speed_mbps': max(0.5, random.gauss(20, 15)),
|
| 326 |
+
'video_streaming_quality': random.choices(['Poor', 'Fair', 'Good', 'Excellent'],
|
| 327 |
+
weights=[0.05, 0.15, 0.40, 0.40])[0],
|
| 328 |
+
'mos_score': round(random.uniform(2.5, 4.5), 2), # Mean Opinion Score for voice quality
|
| 329 |
+
'jitter_ms': random.uniform(1, 50),
|
| 330 |
+
'buffering_events': np.random.poisson(2),
|
| 331 |
+
'connection_time_sec': random.uniform(0.5, 5.0),
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
quality_data.append(event)
|
| 335 |
+
|
| 336 |
+
df = pd.DataFrame(quality_data)
|
| 337 |
+
print(f"✅ Generated {len(df):,} service quality records")
|
| 338 |
+
print(f" - Event types:\n{df['event_type'].value_counts()}")
|
| 339 |
+
print(f" - Average download speed: {df['download_speed_mbps'].mean():.1f} Mbps")
|
| 340 |
+
|
| 341 |
+
return df
|
| 342 |
+
|
| 343 |
+
def generate_customer_service_interactions(self, customer_df, num_interactions=30000):
|
| 344 |
+
"""Generate customer service call center interactions"""
|
| 345 |
+
print(f"\n☎️ Generating Customer Service Interactions...")
|
| 346 |
+
|
| 347 |
+
interactions = []
|
| 348 |
+
|
| 349 |
+
for i in tqdm(range(num_interactions), desc="Service calls"):
|
| 350 |
+
customer = customer_df.sample(1).iloc[0]
|
| 351 |
+
call_date = fake.date_time_between(start_date=self.start_date, end_date=self.end_date)
|
| 352 |
+
|
| 353 |
+
complaint_type = random.choice(self.complaint_types)
|
| 354 |
+
resolution_time_min = int(np.random.exponential(scale=15) + 5) # Average ~20 min
|
| 355 |
+
was_resolved = random.random() < 0.85 # 85% resolution rate
|
| 356 |
+
|
| 357 |
+
interaction = {
|
| 358 |
+
'interaction_id': f'INT{i+1:08d}',
|
| 359 |
+
'customer_id': customer['customer_id'],
|
| 360 |
+
'interaction_date': call_date.date(),
|
| 361 |
+
'interaction_time': call_date.time(),
|
| 362 |
+
'channel': random.choices(['Phone', 'Chat', 'Email', 'Store'],
|
| 363 |
+
weights=[0.50, 0.25, 0.15, 0.10])[0],
|
| 364 |
+
'complaint_type': complaint_type,
|
| 365 |
+
'priority': random.choices(['Low', 'Medium', 'High', 'Critical'],
|
| 366 |
+
weights=[0.40, 0.35, 0.20, 0.05])[0],
|
| 367 |
+
'resolution_time_minutes': resolution_time_min,
|
| 368 |
+
'was_resolved': was_resolved,
|
| 369 |
+
'was_escalated': random.random() < 0.15, # 15% escalation rate
|
| 370 |
+
'customer_satisfaction_score': random.randint(1, 10) if was_resolved else random.randint(1, 6),
|
| 371 |
+
'agent_id': f'AGENT{random.randint(1, 500):04d}',
|
| 372 |
+
'follow_up_required': random.random() < 0.20,
|
| 373 |
+
'sentiment': random.choices(['Negative', 'Neutral', 'Positive'],
|
| 374 |
+
weights=[0.25, 0.35, 0.40])[0],
|
| 375 |
+
'call_transcript_summary': fake.sentence(nb_words=15)
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
interactions.append(interaction)
|
| 379 |
+
|
| 380 |
+
df = pd.DataFrame(interactions)
|
| 381 |
+
print(f"✅ Generated {len(df):,} customer service interactions")
|
| 382 |
+
print(f" - Resolution rate: {df['was_resolved'].mean()*100:.1f}%")
|
| 383 |
+
print(f" - Average satisfaction: {df['customer_satisfaction_score'].mean():.2f}/10")
|
| 384 |
+
print(f" - Top complaints:\n{df['complaint_type'].value_counts().head()}")
|
| 385 |
+
|
| 386 |
+
return df
|
| 387 |
+
|
| 388 |
+
def generate_billing_data(self, customer_df, num_months=12):
|
| 389 |
+
"""Generate monthly billing information"""
|
| 390 |
+
print(f"\n💰 Generating Billing Data (last {num_months} months)...")
|
| 391 |
+
|
| 392 |
+
# Generate for last N months
|
| 393 |
+
billing_periods = pd.date_range(end=self.end_date, periods=num_months, freq='MS')
|
| 394 |
+
|
| 395 |
+
billing_data = []
|
| 396 |
+
|
| 397 |
+
for customer_id, customer in tqdm(customer_df.iterrows(), total=len(customer_df), desc="Customer billing"):
|
| 398 |
+
customer_row = customer
|
| 399 |
+
|
| 400 |
+
for period in billing_periods:
|
| 401 |
+
# Only bill if customer was active
|
| 402 |
+
if period.date() >= customer_row['service_start_date']:
|
| 403 |
+
# Base monthly charge
|
| 404 |
+
base_charge = customer_row['monthly_plan_cost']
|
| 405 |
+
|
| 406 |
+
# Random overages
|
| 407 |
+
data_overage = random.uniform(0, 30) if random.random() < 0.20 else 0
|
| 408 |
+
voice_overage = random.uniform(0, 15) if random.random() < 0.10 else 0
|
| 409 |
+
roaming_charges = random.uniform(0, 50) if random.random() < 0.05 else 0
|
| 410 |
+
|
| 411 |
+
total_charges = base_charge + data_overage + voice_overage + roaming_charges
|
| 412 |
+
|
| 413 |
+
# Payment behavior
|
| 414 |
+
days_to_payment = int(np.random.exponential(scale=10)) if customer_row['autopay_enabled'] else int(np.random.exponential(scale=20))
|
| 415 |
+
payment_status = 'Paid' if days_to_payment <= 30 else random.choice(['Paid Late', 'Pending'])
|
| 416 |
+
|
| 417 |
+
bill = {
|
| 418 |
+
'customer_id': customer_row['customer_id'],
|
| 419 |
+
'billing_period': period.date(),
|
| 420 |
+
'base_charge': round(base_charge, 2),
|
| 421 |
+
'data_overage_charge': round(data_overage, 2),
|
| 422 |
+
'voice_overage_charge': round(voice_overage, 2),
|
| 423 |
+
'roaming_charges': round(roaming_charges, 2),
|
| 424 |
+
'taxes_and_fees': round(total_charges * 0.12, 2), # ~12% taxes
|
| 425 |
+
'total_amount': round(total_charges * 1.12, 2),
|
| 426 |
+
'payment_status': payment_status,
|
| 427 |
+
'payment_date': (period + timedelta(days=days_to_payment)).date() if payment_status == 'Paid' else None,
|
| 428 |
+
'days_to_payment': days_to_payment if payment_status == 'Paid' else None,
|
| 429 |
+
'payment_method': customer_row.get('autopay_enabled') and 'Auto-Pay' or random.choice(['Credit Card', 'Debit Card', 'Bank Transfer', 'Check'])
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
billing_data.append(bill)
|
| 433 |
+
|
| 434 |
+
df = pd.DataFrame(billing_data)
|
| 435 |
+
print(f"✅ Generated {len(df):,} billing records")
|
| 436 |
+
print(f" - Average monthly bill: ${df['total_amount'].mean():.2f}")
|
| 437 |
+
print(f" - Payment status:\n{df['payment_status'].value_counts()}")
|
| 438 |
+
|
| 439 |
+
return df
|
| 440 |
+
|
| 441 |
+
def generate_churn_labels(self, customer_df, churn_rate=0.15):
|
| 442 |
+
"""
|
| 443 |
+
Generate churn labels for customers
|
| 444 |
+
|
| 445 |
+
Parameters:
|
| 446 |
+
-----------
|
| 447 |
+
customer_df : DataFrame
|
| 448 |
+
Customer demographics
|
| 449 |
+
churn_rate : float
|
| 450 |
+
Target churn rate (default: 15%)
|
| 451 |
+
"""
|
| 452 |
+
print(f"\n🎯 Generating Churn Labels (target rate: {churn_rate*100}%)...")
|
| 453 |
+
|
| 454 |
+
churn_data = []
|
| 455 |
+
|
| 456 |
+
for _, customer in tqdm(customer_df.iterrows(), total=len(customer_df), desc="Churn analysis"):
|
| 457 |
+
# Factors influencing churn
|
| 458 |
+
churn_probability = 0.05 # Base 5%
|
| 459 |
+
|
| 460 |
+
# Tenure: newer customers more likely to churn
|
| 461 |
+
if customer['tenure_months'] < 6:
|
| 462 |
+
churn_probability += 0.25
|
| 463 |
+
elif customer['tenure_months'] < 12:
|
| 464 |
+
churn_probability += 0.15
|
| 465 |
+
|
| 466 |
+
# Contract: non-contracted customers more likely to churn
|
| 467 |
+
if not customer['is_contracted']:
|
| 468 |
+
churn_probability += 0.10
|
| 469 |
+
else:
|
| 470 |
+
# Near contract end increases churn risk
|
| 471 |
+
if customer['contract_end_date'] and (self.end_date.date() - customer['contract_end_date']).days > -60:
|
| 472 |
+
churn_probability += 0.20
|
| 473 |
+
|
| 474 |
+
# Price: higher prices increase churn
|
| 475 |
+
if customer['monthly_plan_cost'] > 100:
|
| 476 |
+
churn_probability += 0.08
|
| 477 |
+
|
| 478 |
+
# Random factor
|
| 479 |
+
churn_probability += random.uniform(-0.05, 0.05)
|
| 480 |
+
churn_probability = max(0, min(1, churn_probability))
|
| 481 |
+
|
| 482 |
+
# Determine churn
|
| 483 |
+
has_churned = random.random() < churn_probability
|
| 484 |
+
|
| 485 |
+
if has_churned:
|
| 486 |
+
churn_date = self.end_date - timedelta(days=random.randint(0, 180))
|
| 487 |
+
else:
|
| 488 |
+
churn_date = None
|
| 489 |
+
|
| 490 |
+
churn_data.append({
|
| 491 |
+
'customer_id': customer['customer_id'],
|
| 492 |
+
'churn_probability': round(churn_probability, 4),
|
| 493 |
+
'has_churned': has_churned,
|
| 494 |
+
'churn_date': churn_date.date() if churn_date else None,
|
| 495 |
+
'churn_risk_category': 'High' if churn_probability > 0.6 else ('Medium' if churn_probability > 0.3 else 'Low')
|
| 496 |
+
})
|
| 497 |
+
|
| 498 |
+
df = pd.DataFrame(churn_data)
|
| 499 |
+
actual_churn_rate = df['has_churned'].mean()
|
| 500 |
+
|
| 501 |
+
print(f"✅ Generated churn labels for {len(df):,} customers")
|
| 502 |
+
print(f" - Actual churn rate: {actual_churn_rate*100:.2f}%")
|
| 503 |
+
print(f" - Risk distribution:\n{df['churn_risk_category'].value_counts()}")
|
| 504 |
+
print(f" - High risk customers: {(df['churn_risk_category']=='High').sum():,}")
|
| 505 |
+
|
| 506 |
+
return df
|
| 507 |
+
|
| 508 |
+
|
| 509 |
+
def main():
|
| 510 |
+
"""Main function to generate all synthetic data"""
|
| 511 |
+
print("="*80)
|
| 512 |
+
print("TELECOMMUNICATIONS SYNTHETIC DATA GENERATOR")
|
| 513 |
+
print("="*80)
|
| 514 |
+
|
| 515 |
+
# Initialize generator
|
| 516 |
+
# For demo: 100K customers (can scale to 10M for production)
|
| 517 |
+
generator = TelecomDataGenerator(
|
| 518 |
+
num_customers=100000, # 100K for demo
|
| 519 |
+
num_towers=1000,
|
| 520 |
+
start_date='2022-01-01',
|
| 521 |
+
end_date='2024-12-31'
|
| 522 |
+
)
|
| 523 |
+
|
| 524 |
+
# Generate all datasets
|
| 525 |
+
print("\n" + "="*80)
|
| 526 |
+
print("GENERATING DATASETS")
|
| 527 |
+
print("="*80)
|
| 528 |
+
|
| 529 |
+
# 1. Customer Demographics
|
| 530 |
+
customers_df = generator.generate_customer_demographics()
|
| 531 |
+
customers_df.to_csv('data/synthetic/customers.csv', index=False)
|
| 532 |
+
print(f"💾 Saved to: data/synthetic/customers.csv")
|
| 533 |
+
|
| 534 |
+
# 2. Network Infrastructure
|
| 535 |
+
towers_df = generator.generate_network_infrastructure()
|
| 536 |
+
towers_df.to_csv('data/synthetic/network_infrastructure.csv', index=False)
|
| 537 |
+
print(f"💾 Saved to: data/synthetic/network_infrastructure.csv")
|
| 538 |
+
|
| 539 |
+
# 3. Customer Usage Data (last 90 days for demo)
|
| 540 |
+
usage_df = generator.generate_customer_usage_data(customers_df, sample_days=90)
|
| 541 |
+
usage_df.to_csv('data/synthetic/customer_usage.csv', index=False)
|
| 542 |
+
print(f"💾 Saved to: data/synthetic/customer_usage.csv")
|
| 543 |
+
|
| 544 |
+
# 4. Network Performance (last 30 days, hourly)
|
| 545 |
+
performance_df = generator.generate_network_performance_data(towers_df, sample_days=30)
|
| 546 |
+
performance_df.to_csv('data/synthetic/network_performance.csv', index=False)
|
| 547 |
+
print(f"💾 Saved to: data/synthetic/network_performance.csv")
|
| 548 |
+
|
| 549 |
+
# 5. Service Quality Metrics
|
| 550 |
+
quality_df = generator.generate_service_quality_metrics(customers_df, towers_df, num_records=50000)
|
| 551 |
+
quality_df.to_csv('data/synthetic/service_quality.csv', index=False)
|
| 552 |
+
print(f"💾 Saved to: data/synthetic/service_quality.csv")
|
| 553 |
+
|
| 554 |
+
# 6. Customer Service Interactions
|
| 555 |
+
service_df = generator.generate_customer_service_interactions(customers_df, num_interactions=30000)
|
| 556 |
+
service_df.to_csv('data/synthetic/customer_service.csv', index=False)
|
| 557 |
+
print(f"💾 Saved to: data/synthetic/customer_service.csv")
|
| 558 |
+
|
| 559 |
+
# 7. Billing Data (last 12 months)
|
| 560 |
+
billing_df = generator.generate_billing_data(customers_df, num_months=12)
|
| 561 |
+
billing_df.to_csv('data/synthetic/billing.csv', index=False)
|
| 562 |
+
print(f"💾 Saved to: data/synthetic/billing.csv")
|
| 563 |
+
|
| 564 |
+
# 8. Churn Labels
|
| 565 |
+
churn_df = generator.generate_churn_labels(customers_df, churn_rate=0.15)
|
| 566 |
+
churn_df.to_csv('data/synthetic/churn_labels.csv', index=False)
|
| 567 |
+
print(f"💾 Saved to: data/synthetic/churn_labels.csv")
|
| 568 |
+
|
| 569 |
+
# Summary Statistics
|
| 570 |
+
print("\n" + "="*80)
|
| 571 |
+
print("GENERATION COMPLETE - SUMMARY")
|
| 572 |
+
print("="*80)
|
| 573 |
+
print(f"\n📊 Total Datasets Generated: 8")
|
| 574 |
+
print(f"\n📈 Data Volume:")
|
| 575 |
+
print(f" - Customers: {len(customers_df):,} records")
|
| 576 |
+
print(f" - Cell Towers: {len(towers_df):,} records")
|
| 577 |
+
print(f" - Usage Events: {len(usage_df):,} records")
|
| 578 |
+
print(f" - Network Metrics: {len(performance_df):,} records")
|
| 579 |
+
print(f" - Quality Events: {len(quality_df):,} records")
|
| 580 |
+
print(f" - Service Calls: {len(service_df):,} records")
|
| 581 |
+
print(f" - Billing Records: {len(billing_df):,} records")
|
| 582 |
+
print(f" - Churn Labels: {len(churn_df):,} records")
|
| 583 |
+
print(f"\n TOTAL: {len(customers_df) + len(towers_df) + len(usage_df) + len(performance_df) + len(quality_df) + len(service_df) + len(billing_df) + len(churn_df):,} records")
|
| 584 |
+
|
| 585 |
+
print("\n✅ All data saved to 'data/synthetic/' directory")
|
| 586 |
+
print("\n" + "="*80)
|
| 587 |
+
|
| 588 |
+
return {
|
| 589 |
+
'customers': customers_df,
|
| 590 |
+
'towers': towers_df,
|
| 591 |
+
'usage': usage_df,
|
| 592 |
+
'performance': performance_df,
|
| 593 |
+
'quality': quality_df,
|
| 594 |
+
'service': service_df,
|
| 595 |
+
'billing': billing_df,
|
| 596 |
+
'churn': churn_df
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
|
| 600 |
+
if __name__ == "__main__":
|
| 601 |
+
datasets = main()
|
templates/base.html
ADDED
|
@@ -0,0 +1,679 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>{% block title %}TelecomIQ Analytics Platform{% endblock %}</title>
|
| 7 |
+
|
| 8 |
+
<!-- Bootstrap CSS -->
|
| 9 |
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
| 10 |
+
|
| 11 |
+
<!-- Chart.js -->
|
| 12 |
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
| 13 |
+
|
| 14 |
+
<!-- Google Fonts -->
|
| 15 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
| 16 |
+
|
| 17 |
+
<!-- Custom CSS -->
|
| 18 |
+
<style>
|
| 19 |
+
:root {
|
| 20 |
+
--primary-color: #4f46e5;
|
| 21 |
+
--primary-light: #6366f1;
|
| 22 |
+
--primary-dark: #4338ca;
|
| 23 |
+
--secondary-color: #ec4899;
|
| 24 |
+
--success-color: #10b981;
|
| 25 |
+
--warning-color: #f59e0b;
|
| 26 |
+
--danger-color: #ef4444;
|
| 27 |
+
--info-color: #3b82f6;
|
| 28 |
+
|
| 29 |
+
--bg-main: #f8fafc;
|
| 30 |
+
--bg-card: #ffffff;
|
| 31 |
+
--bg-hover: #f1f5f9;
|
| 32 |
+
|
| 33 |
+
--text-primary: #1e293b;
|
| 34 |
+
--text-secondary: #64748b;
|
| 35 |
+
--text-muted: #94a3b8;
|
| 36 |
+
|
| 37 |
+
--border-color: #e2e8f0;
|
| 38 |
+
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
| 39 |
+
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
| 40 |
+
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
| 41 |
+
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
* {
|
| 45 |
+
margin: 0;
|
| 46 |
+
padding: 0;
|
| 47 |
+
box-sizing: border-box;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
body {
|
| 51 |
+
background-color: var(--bg-main);
|
| 52 |
+
color: var(--text-primary);
|
| 53 |
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
| 54 |
+
font-size: 15px;
|
| 55 |
+
line-height: 1.6;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
/* Navbar Styles */
|
| 59 |
+
.navbar {
|
| 60 |
+
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-light) 100%) !important;
|
| 61 |
+
border: none;
|
| 62 |
+
box-shadow: var(--shadow-lg);
|
| 63 |
+
padding: 1rem 0;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
.navbar-brand {
|
| 67 |
+
font-weight: 800;
|
| 68 |
+
font-size: 1.5rem;
|
| 69 |
+
letter-spacing: -0.5px;
|
| 70 |
+
color: #ffffff !important;
|
| 71 |
+
display: flex;
|
| 72 |
+
align-items: center;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
.navbar-brand:hover {
|
| 76 |
+
opacity: 0.9;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
.navbar .small {
|
| 80 |
+
color: rgba(255, 255, 255, 0.8) !important;
|
| 81 |
+
font-weight: 400;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
.nav-link {
|
| 85 |
+
color: rgba(255, 255, 255, 0.9) !important;
|
| 86 |
+
font-weight: 500;
|
| 87 |
+
font-size: 0.95rem;
|
| 88 |
+
padding: 0.5rem 1rem !important;
|
| 89 |
+
border-radius: 8px;
|
| 90 |
+
transition: all 0.3s ease;
|
| 91 |
+
margin: 0 0.25rem;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
.nav-link:hover {
|
| 95 |
+
background-color: rgba(255, 255, 255, 0.15);
|
| 96 |
+
color: #ffffff !important;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
.nav-link.active {
|
| 100 |
+
background-color: rgba(255, 255, 255, 0.25);
|
| 101 |
+
color: #ffffff !important;
|
| 102 |
+
font-weight: 600;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
/* Metric Card Styles */
|
| 106 |
+
.metric-card {
|
| 107 |
+
background: var(--bg-card);
|
| 108 |
+
border-radius: 16px;
|
| 109 |
+
border: 1px solid var(--border-color);
|
| 110 |
+
padding: 1.75rem;
|
| 111 |
+
margin-bottom: 1.5rem;
|
| 112 |
+
box-shadow: var(--shadow-sm);
|
| 113 |
+
transition: all 0.3s ease;
|
| 114 |
+
position: relative;
|
| 115 |
+
overflow: hidden;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
.metric-card::before {
|
| 119 |
+
content: '';
|
| 120 |
+
position: absolute;
|
| 121 |
+
top: 0;
|
| 122 |
+
left: 0;
|
| 123 |
+
right: 0;
|
| 124 |
+
height: 4px;
|
| 125 |
+
background: linear-gradient(90deg, var(--primary-color), var(--primary-light));
|
| 126 |
+
opacity: 0;
|
| 127 |
+
transition: opacity 0.3s ease;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
.metric-card:hover {
|
| 131 |
+
transform: translateY(-4px);
|
| 132 |
+
box-shadow: var(--shadow-lg);
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.metric-card:hover::before {
|
| 136 |
+
opacity: 1;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
.metric-card h6 {
|
| 140 |
+
font-size: 0.75rem;
|
| 141 |
+
font-weight: 600;
|
| 142 |
+
color: var(--text-secondary);
|
| 143 |
+
text-transform: uppercase;
|
| 144 |
+
letter-spacing: 0.5px;
|
| 145 |
+
margin-bottom: 0.75rem;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
.metric-card h3 {
|
| 149 |
+
font-size: 2.25rem;
|
| 150 |
+
font-weight: 700;
|
| 151 |
+
margin-bottom: 0.25rem;
|
| 152 |
+
line-height: 1;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
.metric-card small {
|
| 156 |
+
color: var(--text-muted);
|
| 157 |
+
font-size: 0.875rem;
|
| 158 |
+
font-weight: 400;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
.metric-card .trend {
|
| 162 |
+
position: absolute;
|
| 163 |
+
top: 1.5rem;
|
| 164 |
+
right: 1.5rem;
|
| 165 |
+
font-size: 0.875rem;
|
| 166 |
+
font-weight: 600;
|
| 167 |
+
padding: 0.25rem 0.75rem;
|
| 168 |
+
border-radius: 20px;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
.trend-up {
|
| 172 |
+
background-color: rgba(16, 185, 129, 0.1);
|
| 173 |
+
color: var(--success-color);
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
.trend-down {
|
| 177 |
+
background-color: rgba(239, 68, 68, 0.1);
|
| 178 |
+
color: var(--danger-color);
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
/* Chart Card Styles */
|
| 182 |
+
.chart-card {
|
| 183 |
+
background-color: var(--bg-card);
|
| 184 |
+
border-radius: 16px;
|
| 185 |
+
border: 1px solid var(--border-color);
|
| 186 |
+
padding: 2rem;
|
| 187 |
+
margin-bottom: 1.5rem;
|
| 188 |
+
box-shadow: var(--shadow-sm);
|
| 189 |
+
transition: all 0.3s ease;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
.chart-card:hover {
|
| 193 |
+
box-shadow: var(--shadow-md);
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
.chart-card h5 {
|
| 197 |
+
font-size: 1.125rem;
|
| 198 |
+
font-weight: 700;
|
| 199 |
+
color: var(--text-primary);
|
| 200 |
+
margin-bottom: 1.5rem;
|
| 201 |
+
padding-bottom: 1rem;
|
| 202 |
+
border-bottom: 2px solid var(--border-color);
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
/* Color Classes */
|
| 206 |
+
.text-primary-custom {
|
| 207 |
+
color: var(--primary-color) !important;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.text-success-custom {
|
| 211 |
+
color: var(--success-color) !important;
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
.text-warning-custom {
|
| 215 |
+
color: var(--warning-color) !important;
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
.text-danger-custom {
|
| 219 |
+
color: var(--danger-color) !important;
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
.text-info-custom {
|
| 223 |
+
color: var(--info-color) !important;
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
/* Badge Styles */
|
| 227 |
+
.badge-primary-custom {
|
| 228 |
+
background-color: var(--primary-color);
|
| 229 |
+
color: #ffffff;
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
.badge-success-custom {
|
| 233 |
+
background-color: var(--success-color);
|
| 234 |
+
color: #ffffff;
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
.badge-warning-custom {
|
| 238 |
+
background-color: var(--warning-color);
|
| 239 |
+
color: #ffffff;
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
.badge-danger-custom {
|
| 243 |
+
background-color: var(--danger-color);
|
| 244 |
+
color: #ffffff;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
/* Page Header */
|
| 248 |
+
h2 {
|
| 249 |
+
font-size: 2rem;
|
| 250 |
+
font-weight: 800;
|
| 251 |
+
color: var(--text-primary);
|
| 252 |
+
margin-bottom: 2rem;
|
| 253 |
+
letter-spacing: -0.5px;
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
/* Alert Styles */
|
| 257 |
+
.alert {
|
| 258 |
+
border-radius: 12px;
|
| 259 |
+
border: none;
|
| 260 |
+
box-shadow: var(--shadow-sm);
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
.alert-info {
|
| 264 |
+
background-color: rgba(59, 130, 246, 0.1);
|
| 265 |
+
color: var(--info-color);
|
| 266 |
+
border-left: 4px solid var(--info-color);
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
.alert-success {
|
| 270 |
+
background-color: rgba(16, 185, 129, 0.1);
|
| 271 |
+
color: var(--success-color);
|
| 272 |
+
border-left: 4px solid var(--success-color);
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
.alert-warning {
|
| 276 |
+
background-color: rgba(245, 158, 11, 0.1);
|
| 277 |
+
color: var(--warning-color);
|
| 278 |
+
border-left: 4px solid var(--warning-color);
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
.alert-danger {
|
| 282 |
+
background-color: rgba(239, 68, 68, 0.1);
|
| 283 |
+
color: var(--danger-color);
|
| 284 |
+
border-left: 4px solid var(--danger-color);
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
/* Form Styles */
|
| 288 |
+
.form-control, .form-select {
|
| 289 |
+
border-radius: 8px;
|
| 290 |
+
border: 1px solid var(--border-color);
|
| 291 |
+
padding: 0.625rem 0.875rem;
|
| 292 |
+
font-size: 0.9375rem;
|
| 293 |
+
transition: all 0.2s ease;
|
| 294 |
+
background-color: var(--bg-card);
|
| 295 |
+
color: var(--text-primary);
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
.form-control:focus, .form-select:focus {
|
| 299 |
+
border-color: var(--primary-color);
|
| 300 |
+
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
|
| 301 |
+
outline: none;
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
.form-label {
|
| 305 |
+
font-weight: 600;
|
| 306 |
+
font-size: 0.875rem;
|
| 307 |
+
color: var(--text-secondary);
|
| 308 |
+
margin-bottom: 0.5rem;
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
/* Button Styles */
|
| 312 |
+
.btn {
|
| 313 |
+
border-radius: 10px;
|
| 314 |
+
font-weight: 600;
|
| 315 |
+
padding: 0.625rem 1.5rem;
|
| 316 |
+
transition: all 0.3s ease;
|
| 317 |
+
border: none;
|
| 318 |
+
font-size: 0.9375rem;
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
.btn-primary {
|
| 322 |
+
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-light) 100%);
|
| 323 |
+
color: #ffffff;
|
| 324 |
+
box-shadow: var(--shadow-md);
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
.btn-primary:hover {
|
| 328 |
+
transform: translateY(-2px);
|
| 329 |
+
box-shadow: var(--shadow-lg);
|
| 330 |
+
background: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary-color) 100%);
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
.btn-lg {
|
| 334 |
+
padding: 0.875rem 2rem;
|
| 335 |
+
font-size: 1rem;
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
/* Table Styles */
|
| 339 |
+
.table {
|
| 340 |
+
border-radius: 12px;
|
| 341 |
+
overflow: hidden;
|
| 342 |
+
background-color: var(--bg-card);
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
.table-dark {
|
| 346 |
+
background-color: var(--bg-card);
|
| 347 |
+
color: var(--text-primary);
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
.table-dark thead {
|
| 351 |
+
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-light) 100%);
|
| 352 |
+
color: #ffffff;
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
.table-dark tbody tr {
|
| 356 |
+
border-bottom: 1px solid var(--border-color);
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
.table-dark tbody tr:hover {
|
| 360 |
+
background-color: var(--bg-hover);
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
.table-striped tbody tr:nth-of-type(odd) {
|
| 364 |
+
background-color: rgba(0, 0, 0, 0.02);
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
/* Loading Spinner */
|
| 368 |
+
.loading {
|
| 369 |
+
text-align: center;
|
| 370 |
+
padding: 3rem;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
.spinner-border-custom {
|
| 374 |
+
color: var(--primary-color);
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
/* Container */
|
| 378 |
+
.container-fluid {
|
| 379 |
+
max-width: 1400px;
|
| 380 |
+
margin: 0 auto;
|
| 381 |
+
padding: 2rem 1.5rem;
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
/* Responsive */
|
| 385 |
+
@media (max-width: 768px) {
|
| 386 |
+
.metric-card h3 {
|
| 387 |
+
font-size: 1.75rem;
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
h2 {
|
| 391 |
+
font-size: 1.5rem;
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
.chart-card {
|
| 395 |
+
padding: 1.25rem;
|
| 396 |
+
}
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
/* Scrollbar */
|
| 400 |
+
::-webkit-scrollbar {
|
| 401 |
+
width: 10px;
|
| 402 |
+
height: 10px;
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
::-webkit-scrollbar-track {
|
| 406 |
+
background: var(--bg-main);
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
::-webkit-scrollbar-thumb {
|
| 410 |
+
background: var(--border-color);
|
| 411 |
+
border-radius: 5px;
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
::-webkit-scrollbar-thumb:hover {
|
| 415 |
+
background: var(--text-muted);
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
/* ── INFO BUTTON SYSTEM ─────────────────────────── */
|
| 419 |
+
.info-btn {
|
| 420 |
+
display: inline-flex;
|
| 421 |
+
align-items: center;
|
| 422 |
+
justify-content: center;
|
| 423 |
+
width: 20px;
|
| 424 |
+
height: 20px;
|
| 425 |
+
border-radius: 50%;
|
| 426 |
+
border: 1.5px solid var(--primary-color);
|
| 427 |
+
background: transparent;
|
| 428 |
+
color: var(--primary-color);
|
| 429 |
+
font-size: 0.7rem;
|
| 430 |
+
font-weight: 700;
|
| 431 |
+
line-height: 1;
|
| 432 |
+
cursor: pointer;
|
| 433 |
+
transition: all 0.2s ease;
|
| 434 |
+
vertical-align: middle;
|
| 435 |
+
margin-left: 6px;
|
| 436 |
+
flex-shrink: 0;
|
| 437 |
+
padding: 0;
|
| 438 |
+
}
|
| 439 |
+
.info-btn:hover {
|
| 440 |
+
background: var(--primary-color);
|
| 441 |
+
color: #fff;
|
| 442 |
+
transform: scale(1.15);
|
| 443 |
+
box-shadow: 0 0 0 3px rgba(79,70,229,0.2);
|
| 444 |
+
}
|
| 445 |
+
/* Inside metric-card h6, render it inline */
|
| 446 |
+
.metric-card h6 { display: flex; align-items: center; gap: 4px; }
|
| 447 |
+
.chart-card h5 { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
| 448 |
+
|
| 449 |
+
/* Info Drawer */
|
| 450 |
+
#info-drawer-overlay {
|
| 451 |
+
position: fixed; inset: 0;
|
| 452 |
+
background: rgba(0,0,0,0.35);
|
| 453 |
+
z-index: 9998;
|
| 454 |
+
opacity: 0;
|
| 455 |
+
pointer-events: none;
|
| 456 |
+
transition: opacity 0.25s;
|
| 457 |
+
}
|
| 458 |
+
#info-drawer-overlay.active { opacity: 1; pointer-events: all; }
|
| 459 |
+
#info-drawer {
|
| 460 |
+
position: fixed;
|
| 461 |
+
top: 50%;
|
| 462 |
+
left: 50%;
|
| 463 |
+
transform: translate(-50%, -46%);
|
| 464 |
+
z-index: 9999;
|
| 465 |
+
background: #fff;
|
| 466 |
+
border-radius: 20px;
|
| 467 |
+
box-shadow: 0 24px 80px rgba(0,0,0,0.22);
|
| 468 |
+
width: min(480px, 92vw);
|
| 469 |
+
padding: 0;
|
| 470 |
+
overflow: hidden;
|
| 471 |
+
transition: transform 0.28s cubic-bezier(.34,1.56,.64,1), opacity 0.22s;
|
| 472 |
+
opacity: 0;
|
| 473 |
+
pointer-events: none;
|
| 474 |
+
}
|
| 475 |
+
#info-drawer.active {
|
| 476 |
+
transform: translate(-50%, -50%);
|
| 477 |
+
opacity: 1;
|
| 478 |
+
pointer-events: all;
|
| 479 |
+
}
|
| 480 |
+
#info-drawer-header {
|
| 481 |
+
background: linear-gradient(135deg, var(--primary-color), var(--primary-light));
|
| 482 |
+
padding: 1.5rem 1.75rem 1.25rem;
|
| 483 |
+
display: flex;
|
| 484 |
+
align-items: flex-start;
|
| 485 |
+
gap: 1rem;
|
| 486 |
+
}
|
| 487 |
+
#info-drawer-icon {
|
| 488 |
+
width: 44px; height: 44px;
|
| 489 |
+
border-radius: 12px;
|
| 490 |
+
background: rgba(255,255,255,0.2);
|
| 491 |
+
display: flex; align-items: center; justify-content: center;
|
| 492 |
+
font-size: 1.4rem;
|
| 493 |
+
flex-shrink: 0;
|
| 494 |
+
}
|
| 495 |
+
#info-drawer-title {
|
| 496 |
+
color: #fff;
|
| 497 |
+
font-size: 1.05rem;
|
| 498 |
+
font-weight: 700;
|
| 499 |
+
margin: 0 0 0.2rem;
|
| 500 |
+
letter-spacing: -0.2px;
|
| 501 |
+
}
|
| 502 |
+
#info-drawer-subtitle {
|
| 503 |
+
color: rgba(255,255,255,0.75);
|
| 504 |
+
font-size: 0.78rem;
|
| 505 |
+
font-weight: 400;
|
| 506 |
+
}
|
| 507 |
+
#info-drawer-close {
|
| 508 |
+
margin-left: auto;
|
| 509 |
+
background: rgba(255,255,255,0.15);
|
| 510 |
+
border: none;
|
| 511 |
+
border-radius: 8px;
|
| 512 |
+
color: #fff;
|
| 513 |
+
width: 30px; height: 30px;
|
| 514 |
+
display: flex; align-items: center; justify-content: center;
|
| 515 |
+
cursor: pointer;
|
| 516 |
+
font-size: 1rem;
|
| 517 |
+
transition: background 0.2s;
|
| 518 |
+
flex-shrink: 0;
|
| 519 |
+
}
|
| 520 |
+
#info-drawer-close:hover { background: rgba(255,255,255,0.3); }
|
| 521 |
+
#info-drawer-body {
|
| 522 |
+
padding: 1.5rem 1.75rem 1.75rem;
|
| 523 |
+
}
|
| 524 |
+
#info-drawer-desc {
|
| 525 |
+
font-size: 0.92rem;
|
| 526 |
+
color: var(--text-primary);
|
| 527 |
+
line-height: 1.7;
|
| 528 |
+
margin-bottom: 1rem;
|
| 529 |
+
}
|
| 530 |
+
#info-drawer-tips {
|
| 531 |
+
list-style: none;
|
| 532 |
+
padding: 0; margin: 0;
|
| 533 |
+
}
|
| 534 |
+
#info-drawer-tips li {
|
| 535 |
+
display: flex;
|
| 536 |
+
gap: 0.65rem;
|
| 537 |
+
align-items: flex-start;
|
| 538 |
+
font-size: 0.875rem;
|
| 539 |
+
color: var(--text-secondary);
|
| 540 |
+
padding: 0.5rem 0;
|
| 541 |
+
border-bottom: 1px solid var(--border-color);
|
| 542 |
+
line-height: 1.55;
|
| 543 |
+
}
|
| 544 |
+
#info-drawer-tips li:last-child { border-bottom: none; }
|
| 545 |
+
#info-drawer-tips li .tip-dot {
|
| 546 |
+
width: 7px; height: 7px;
|
| 547 |
+
border-radius: 50%;
|
| 548 |
+
background: var(--primary-color);
|
| 549 |
+
flex-shrink: 0;
|
| 550 |
+
margin-top: 6px;
|
| 551 |
+
}
|
| 552 |
+
</style>
|
| 553 |
+
|
| 554 |
+
{% block extra_css %}{% endblock %}
|
| 555 |
+
</head>
|
| 556 |
+
<body>
|
| 557 |
+
<!-- ── GLOBAL INFO DRAWER ──────────────────────────── -->
|
| 558 |
+
<div id="info-drawer-overlay"></div>
|
| 559 |
+
<div id="info-drawer" role="dialog" aria-modal="true" aria-labelledby="info-drawer-title">
|
| 560 |
+
<div id="info-drawer-header">
|
| 561 |
+
<div id="info-drawer-icon">ℹ️</div>
|
| 562 |
+
<div>
|
| 563 |
+
<div id="info-drawer-title">Information</div>
|
| 564 |
+
<div id="info-drawer-subtitle">TelecomIQ Metric Guide</div>
|
| 565 |
+
</div>
|
| 566 |
+
<button id="info-drawer-close" aria-label="Close">✕</button>
|
| 567 |
+
</div>
|
| 568 |
+
<div id="info-drawer-body">
|
| 569 |
+
<p id="info-drawer-desc"></p>
|
| 570 |
+
<ul id="info-drawer-tips"></ul>
|
| 571 |
+
</div>
|
| 572 |
+
</div>
|
| 573 |
+
<!-- Navigation -->
|
| 574 |
+
<nav class="navbar navbar-expand-lg navbar-dark">
|
| 575 |
+
<div class="container-fluid">
|
| 576 |
+
<a class="navbar-brand" href="/">TelecomIQ</a>
|
| 577 |
+
<span class="text-muted small ms-2">Analytics Platform</span>
|
| 578 |
+
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
| 579 |
+
<span class="navbar-toggler-icon"></span>
|
| 580 |
+
</button>
|
| 581 |
+
<div class="collapse navbar-collapse" id="navbarNav">
|
| 582 |
+
<ul class="navbar-nav ms-auto">
|
| 583 |
+
<li class="nav-item">
|
| 584 |
+
<a class="nav-link {% if request.path == '/' %}active{% endif %}" href="/">Executive</a>
|
| 585 |
+
</li>
|
| 586 |
+
<li class="nav-item">
|
| 587 |
+
<a class="nav-link {% if request.path == '/network' %}active{% endif %}" href="/network">Network Ops</a>
|
| 588 |
+
</li>
|
| 589 |
+
<li class="nav-item">
|
| 590 |
+
<a class="nav-link {% if request.path == '/geographic' %}active{% endif %}" href="/geographic">Geographic</a>
|
| 591 |
+
</li>
|
| 592 |
+
<li class="nav-item">
|
| 593 |
+
<a class="nav-link {% if request.path == '/customer' %}active{% endif %}" href="/customer">Customer</a>
|
| 594 |
+
</li>
|
| 595 |
+
<li class="nav-item">
|
| 596 |
+
<a class="nav-link {% if request.path == '/journey' %}active{% endif %}" href="/journey">Journey</a>
|
| 597 |
+
</li>
|
| 598 |
+
<li class="nav-item">
|
| 599 |
+
<a class="nav-link {% if request.path == '/segmentation' %}active{% endif %}" href="/segmentation">Segments</a>
|
| 600 |
+
</li>
|
| 601 |
+
<li class="nav-item">
|
| 602 |
+
<a class="nav-link {% if request.path == '/churn' %}active{% endif %}" href="/churn">Churn</a>
|
| 603 |
+
</li>
|
| 604 |
+
<li class="nav-item">
|
| 605 |
+
<a class="nav-link {% if request.path == '/quality' %}active{% endif %}" href="/quality">Quality</a>
|
| 606 |
+
</li>
|
| 607 |
+
<li class="nav-item">
|
| 608 |
+
<a class="nav-link {% if request.path == '/financial' %}active{% endif %}" href="/financial">Financial</a>
|
| 609 |
+
</li>
|
| 610 |
+
<li class="nav-item">
|
| 611 |
+
<a class="nav-link {% if request.path == '/predictions' %}active{% endif %}" href="/predictions">ML</a>
|
| 612 |
+
</li>
|
| 613 |
+
<li class="nav-item">
|
| 614 |
+
<a class="nav-link {% if request.path == '/forecasting' %}active{% endif %}" href="/forecasting">Forecast</a>
|
| 615 |
+
</li>
|
| 616 |
+
</ul>
|
| 617 |
+
</div>
|
| 618 |
+
</div>
|
| 619 |
+
</nav>
|
| 620 |
+
|
| 621 |
+
<!-- Main Content -->
|
| 622 |
+
<div class="container-fluid mt-4">
|
| 623 |
+
{% block content %}{% endblock %}
|
| 624 |
+
</div>
|
| 625 |
+
|
| 626 |
+
<!-- Bootstrap JS -->
|
| 627 |
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
| 628 |
+
|
| 629 |
+
{% block extra_js %}{% endblock %}
|
| 630 |
+
|
| 631 |
+
<!-- ── GLOBAL INFO DRAWER LOGIC ──────────────────── -->
|
| 632 |
+
<script>
|
| 633 |
+
(function() {
|
| 634 |
+
const overlay = document.getElementById('info-drawer-overlay');
|
| 635 |
+
const drawer = document.getElementById('info-drawer');
|
| 636 |
+
const title = document.getElementById('info-drawer-title');
|
| 637 |
+
const subtitle= document.getElementById('info-drawer-subtitle');
|
| 638 |
+
const icon = document.getElementById('info-drawer-icon');
|
| 639 |
+
const desc = document.getElementById('info-drawer-desc');
|
| 640 |
+
const tips = document.getElementById('info-drawer-tips');
|
| 641 |
+
const closeBtn= document.getElementById('info-drawer-close');
|
| 642 |
+
|
| 643 |
+
function openDrawer(btn) {
|
| 644 |
+
title.textContent = btn.dataset.infoTitle || 'Metric Info';
|
| 645 |
+
subtitle.textContent= btn.dataset.infoSection || 'TelecomIQ Analytics';
|
| 646 |
+
icon.textContent = btn.dataset.infoIcon || 'ℹ️';
|
| 647 |
+
desc.textContent = btn.dataset.info || '';
|
| 648 |
+
tips.innerHTML = '';
|
| 649 |
+
const rawTips = btn.dataset.infoTips;
|
| 650 |
+
if (rawTips) {
|
| 651 |
+
rawTips.split('|').forEach(tip => {
|
| 652 |
+
const li = document.createElement('li');
|
| 653 |
+
li.innerHTML = '<span class="tip-dot"></span><span>' + tip.trim() + '</span>';
|
| 654 |
+
tips.appendChild(li);
|
| 655 |
+
});
|
| 656 |
+
}
|
| 657 |
+
overlay.classList.add('active');
|
| 658 |
+
drawer.classList.add('active');
|
| 659 |
+
document.body.style.overflow = 'hidden';
|
| 660 |
+
}
|
| 661 |
+
|
| 662 |
+
function closeDrawer() {
|
| 663 |
+
overlay.classList.remove('active');
|
| 664 |
+
drawer.classList.remove('active');
|
| 665 |
+
document.body.style.overflow = '';
|
| 666 |
+
}
|
| 667 |
+
|
| 668 |
+
// Delegate: catches dynamic & static info-btns
|
| 669 |
+
document.addEventListener('click', function(e) {
|
| 670 |
+
const btn = e.target.closest('.info-btn');
|
| 671 |
+
if (btn) { e.stopPropagation(); openDrawer(btn); return; }
|
| 672 |
+
if (e.target === overlay) closeDrawer();
|
| 673 |
+
});
|
| 674 |
+
closeBtn.addEventListener('click', closeDrawer);
|
| 675 |
+
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeDrawer(); });
|
| 676 |
+
})();
|
| 677 |
+
</script>
|
| 678 |
+
</body>
|
| 679 |
+
</html>
|
templates/churn.html
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}Churn Analytics - TelecomIQ{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<h2 class="text-primary-custom mb-4">Churn Analytics Dashboard</h2>
|
| 7 |
+
|
| 8 |
+
<!-- KPI Cards -->
|
| 9 |
+
<div class="row">
|
| 10 |
+
<div class="col-md-3">
|
| 11 |
+
<div class="metric-card">
|
| 12 |
+
<h6>TOTAL CHURNED
|
| 13 |
+
<button class="info-btn"
|
| 14 |
+
data-info-icon="📤"
|
| 15 |
+
data-info-title="Total Churned Customers"
|
| 16 |
+
data-info-section="Churn Analytics · Volume KPI"
|
| 17 |
+
data-info="The total number of customers who have voluntarily or involuntarily ended their subscription. This is a cumulative count for the analysis period."
|
| 18 |
+
data-info-tips="Voluntary churn = customer-initiated cancellations.|Involuntary churn = payment failures, expired contracts.|Compare month-over-month to track retention programme effectiveness.">ⓘ</button>
|
| 19 |
+
</h6>
|
| 20 |
+
<h3 class="text-danger-custom" id="kpi-churned">-</h3>
|
| 21 |
+
<small>Customers lost</small>
|
| 22 |
+
</div>
|
| 23 |
+
</div>
|
| 24 |
+
<div class="col-md-3">
|
| 25 |
+
<div class="metric-card">
|
| 26 |
+
<h6>CHURN RATE
|
| 27 |
+
<button class="info-btn"
|
| 28 |
+
data-info-icon="📉"
|
| 29 |
+
data-info-title="Overall Churn Rate"
|
| 30 |
+
data-info-section="Churn Analytics · Rate KPI"
|
| 31 |
+
data-info="The percentage of total customers who have churned. Calculated as: (Total Churned ÷ Total Customers) × 100. This is the primary headline retention metric."
|
| 32 |
+
data-info-tips="Industry benchmark: 1.5–2.5% monthly churn for telcos.|Annualised churn = 1 − (1 − monthly rate)^12.|A 1% reduction in churn rate can improve revenue by 5–7%.">ⓘ</button>
|
| 33 |
+
</h6>
|
| 34 |
+
<h3 class="text-warning-custom" id="kpi-churn-rate">-</h3>
|
| 35 |
+
<small>Overall</small>
|
| 36 |
+
</div>
|
| 37 |
+
</div>
|
| 38 |
+
<div class="col-md-3">
|
| 39 |
+
<div class="metric-card">
|
| 40 |
+
<h6>HIGH RISK
|
| 41 |
+
<button class="info-btn"
|
| 42 |
+
data-info-icon="🚨"
|
| 43 |
+
data-info-title="High-Risk Customer Count"
|
| 44 |
+
data-info-section="Churn Analytics · Risk KPI"
|
| 45 |
+
data-info="Customers predicted by the ML model to have a churn probability greater than 60%. These require immediate, personalised retention outreach."
|
| 46 |
+
data-info-tips="Use phone or direct-mail for High-risk — not just email.|Effective retention offers: contract extension, data upgrade, loyalty bonus.|Expected save rate with targeted outreach: 30–45%.">ⓘ</button>
|
| 47 |
+
</h6>
|
| 48 |
+
<h3 class="text-danger-custom" id="kpi-high-risk">-</h3>
|
| 49 |
+
<small>Immediate action</small>
|
| 50 |
+
</div>
|
| 51 |
+
</div>
|
| 52 |
+
<div class="col-md-3">
|
| 53 |
+
<div class="metric-card">
|
| 54 |
+
<h6>AT RISK
|
| 55 |
+
<button class="info-btn"
|
| 56 |
+
data-info-icon="⚡"
|
| 57 |
+
data-info-title="Medium-Risk (Watch List)"
|
| 58 |
+
data-info-section="Churn Analytics · Risk KPI"
|
| 59 |
+
data-info="Customers with a predicted churn probability between 30–60%. Lower urgency than High-risk but should be monitored and included in proactive nurture programmes."
|
| 60 |
+
data-info-tips="Automate nurture: send personalised offers via SMS/email.|Expected 15–25% of Medium-risk customers will move to High within 60 days without intervention.|Segment by usage drop to identify the most at-risk within this group.">ⓘ</button>
|
| 61 |
+
</h6>
|
| 62 |
+
<h3 class="text-warning-custom" id="kpi-medium-risk">-</h3>
|
| 63 |
+
<small>Watch list</small>
|
| 64 |
+
</div>
|
| 65 |
+
</div>
|
| 66 |
+
</div>
|
| 67 |
+
|
| 68 |
+
<!-- Chart -->
|
| 69 |
+
<div class="row mt-4">
|
| 70 |
+
<div class="col-md-12">
|
| 71 |
+
<div class="chart-card">
|
| 72 |
+
<h5 class="text-primary-custom mb-3">Churn Probability Distribution
|
| 73 |
+
<button class="info-btn"
|
| 74 |
+
data-info-icon="📊"
|
| 75 |
+
data-info-title="Churn Probability Distribution"
|
| 76 |
+
data-info-section="Churn Analytics · ML Chart"
|
| 77 |
+
data-info="A histogram showing how churn probability scores (0 to 1) are distributed across all customers. Generated by the trained Gradient Boosting classifier. Bars to the right indicate customers with higher predicted churn likelihood."
|
| 78 |
+
data-info-tips="A bimodal distribution (peaks near 0 and 1) means the model is very confident.|A large spike near 0.5 suggests many borderline cases — consider retraining.|Use this to set your churn-action threshold (e.g., act on anyone above 0.6).">ⓘ</button>
|
| 79 |
+
</h5>
|
| 80 |
+
<canvas id="churnProbChart"></canvas>
|
| 81 |
+
</div>
|
| 82 |
+
</div>
|
| 83 |
+
</div>
|
| 84 |
+
|
| 85 |
+
{% endblock %}
|
| 86 |
+
|
| 87 |
+
{% block extra_js %}
|
| 88 |
+
<script>
|
| 89 |
+
Chart.defaults.color = '#64748b';
|
| 90 |
+
Chart.defaults.borderColor = '#e2e8f0';
|
| 91 |
+
Chart.defaults.font.family = "'Inter', sans-serif";
|
| 92 |
+
|
| 93 |
+
const COLORS = { danger: '#ef4444', primary: '#4f46e5' };
|
| 94 |
+
|
| 95 |
+
async function loadChurnKPIs() {
|
| 96 |
+
try {
|
| 97 |
+
const response = await fetch('/api/churn/kpis');
|
| 98 |
+
const data = await response.json();
|
| 99 |
+
document.getElementById('kpi-churned').textContent = data.total_churned.toLocaleString();
|
| 100 |
+
document.getElementById('kpi-churn-rate').textContent = `${data.churn_rate}%`;
|
| 101 |
+
document.getElementById('kpi-high-risk').textContent = data.high_risk.toLocaleString();
|
| 102 |
+
document.getElementById('kpi-medium-risk').textContent = data.medium_risk.toLocaleString();
|
| 103 |
+
} catch (error) { console.error('Error loading churn KPIs:', error); }
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
async function loadChurnProbChart() {
|
| 107 |
+
try {
|
| 108 |
+
const response = await fetch('/api/churn/probability-distribution');
|
| 109 |
+
const data = await response.json();
|
| 110 |
+
const ctx = document.getElementById('churnProbChart').getContext('2d');
|
| 111 |
+
new Chart(ctx, {
|
| 112 |
+
type: 'bar',
|
| 113 |
+
data: { labels: Array.from({length: 50}, (_, i) => ''), datasets: [{ label: 'Count', data: data.data, backgroundColor: 'rgba(239,68,68,0.7)', borderWidth: 0, borderRadius: 4 }] },
|
| 114 |
+
options: { responsive: true, maintainAspectRatio: true, plugins: { legend: { display: false }, tooltip: { backgroundColor: '#ffffff', titleColor: '#1e293b', bodyColor: '#64748b', borderColor: '#e2e8f0', borderWidth: 1, padding: 12 } }, scales: { y: { beginAtZero: true, grid: { color: '#f1f5f9', drawBorder: false }, title: { display: true, text: 'Number of Customers', font: { weight: 600 } } }, x: { grid: { display: false }, title: { display: true, text: 'Churn Probability', font: { weight: 600 } } } } }
|
| 115 |
+
});
|
| 116 |
+
} catch (error) { console.error('Error loading churn probability chart:', error); }
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
document.addEventListener('DOMContentLoaded', function() { loadChurnKPIs(); loadChurnProbChart(); });
|
| 120 |
+
</script>
|
| 121 |
+
{% endblock %}
|
templates/customer.html
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}Customer Experience - TelecomIQ{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<h2 class="text-primary-custom mb-4">Customer Experience Dashboard</h2>
|
| 7 |
+
|
| 8 |
+
<!-- KPI Cards Row 1 -->
|
| 9 |
+
<div class="row">
|
| 10 |
+
<div class="col-md-3">
|
| 11 |
+
<div class="metric-card">
|
| 12 |
+
<h6>AVG RESOLUTION
|
| 13 |
+
<button class="info-btn"
|
| 14 |
+
data-info-icon="⏱️"
|
| 15 |
+
data-info-title="Average Resolution Time"
|
| 16 |
+
data-info-section="Customer Experience · Service KPI"
|
| 17 |
+
data-info="The mean time (in minutes) taken to fully resolve a customer service interaction from the moment contact is made to closure."
|
| 18 |
+
data-info-tips="Industry benchmark: <20 min for digital channels, <45 min for phone.|Longer resolution times strongly correlate with lower CSAT.|Reduce via agent training, better knowledge bases, and AI-assist tools.">ⓘ</button>
|
| 19 |
+
</h6>
|
| 20 |
+
<h3 class="text-primary-custom" id="kpi-resolution">-</h3>
|
| 21 |
+
<small>Per interaction</small>
|
| 22 |
+
</div>
|
| 23 |
+
</div>
|
| 24 |
+
<div class="col-md-3">
|
| 25 |
+
<div class="metric-card">
|
| 26 |
+
<h6>RESOLUTION RATE
|
| 27 |
+
<button class="info-btn"
|
| 28 |
+
data-info-icon="✅"
|
| 29 |
+
data-info-title="First-Contact Resolution Rate"
|
| 30 |
+
data-info-section="Customer Experience · Service KPI"
|
| 31 |
+
data-info="The percentage of customer issues resolved fully within the first contact, without requiring callbacks or escalations."
|
| 32 |
+
data-info-tips="Target: ≥80% first-contact resolution.|Low FCR = repeat contacts, higher cost-to-serve, lower CSAT.|Track by issue type to find recurring failure points.">ⓘ</button>
|
| 33 |
+
</h6>
|
| 34 |
+
<h3 class="text-success-custom" id="kpi-resolution-rate">-</h3>
|
| 35 |
+
<small>First contact</small>
|
| 36 |
+
</div>
|
| 37 |
+
</div>
|
| 38 |
+
<div class="col-md-3">
|
| 39 |
+
<div class="metric-card">
|
| 40 |
+
<h6>CSAT SCORE
|
| 41 |
+
<button class="info-btn"
|
| 42 |
+
data-info-icon="⭐"
|
| 43 |
+
data-info-title="Customer Satisfaction Score (CSAT)"
|
| 44 |
+
data-info-section="Customer Experience · CX KPI"
|
| 45 |
+
data-info="The average satisfaction rating given by customers after a service interaction, on a scale of 1–10. Direct measure of how well customer needs are being met."
|
| 46 |
+
data-info-tips="Target: ≥7.5/10 for competitive telcos.|Scores <6 within 90 days predict a 40% higher churn probability.|Segment CSAT by channel to find weakest contact points.">ⓘ</button>
|
| 47 |
+
</h6>
|
| 48 |
+
<h3 class="text-success-custom" id="kpi-csat">-</h3>
|
| 49 |
+
<small>Average satisfaction</small>
|
| 50 |
+
</div>
|
| 51 |
+
</div>
|
| 52 |
+
<div class="col-md-3">
|
| 53 |
+
<div class="metric-card">
|
| 54 |
+
<h6>ESCALATIONS
|
| 55 |
+
<button class="info-btn"
|
| 56 |
+
data-info-icon="🔺"
|
| 57 |
+
data-info-title="Escalation Rate"
|
| 58 |
+
data-info-section="Customer Experience · Service KPI"
|
| 59 |
+
data-info="The percentage of service interactions that required escalation to a supervisor or specialist team. High escalation rates indicate frontline agents lack the tools or authority to resolve issues."
|
| 60 |
+
data-info-tips="Target: <10% escalation rate.|Reduce via agent empowerment — give agents more resolution authority.|Track which complaint types trigger the most escalations.">ⓘ</button>
|
| 61 |
+
</h6>
|
| 62 |
+
<h3 class="text-warning-custom" id="kpi-escalations">-</h3>
|
| 63 |
+
<small>To supervisor</small>
|
| 64 |
+
</div>
|
| 65 |
+
</div>
|
| 66 |
+
</div>
|
| 67 |
+
|
| 68 |
+
<!-- KPI Cards Row 2 - NEW -->
|
| 69 |
+
<div class="row">
|
| 70 |
+
<div class="col-md-3">
|
| 71 |
+
<div class="metric-card">
|
| 72 |
+
<h6>NET PROMOTER SCORE
|
| 73 |
+
<button class="info-btn"
|
| 74 |
+
data-info-icon="🏆"
|
| 75 |
+
data-info-title="Net Promoter Score (NPS)"
|
| 76 |
+
data-info-section="Customer Experience · Loyalty KPI"
|
| 77 |
+
data-info="NPS measures customer loyalty by asking 'How likely are you to recommend us?' Scored 0–10. Promoters (9–10) minus Detractors (0–6) = NPS. Ranges from -100 to +100."
|
| 78 |
+
data-info-tips="Industry average NPS for telcos: +20 to +40.|NPS above +50 is excellent and signals a strong referral engine.|A 5-point NPS improvement typically correlates with 2–7% revenue growth.">ⓘ</button>
|
| 79 |
+
</h6>
|
| 80 |
+
<h3 class="text-success-custom" id="kpi-nps">-</h3>
|
| 81 |
+
<small>Promoters - Detractors</small>
|
| 82 |
+
</div>
|
| 83 |
+
</div>
|
| 84 |
+
<div class="col-md-3">
|
| 85 |
+
<div class="metric-card">
|
| 86 |
+
<h6>TOTAL INTERACTIONS
|
| 87 |
+
<button class="info-btn"
|
| 88 |
+
data-info-icon="📞"
|
| 89 |
+
data-info-title="Total Service Interactions"
|
| 90 |
+
data-info-section="Customer Experience · Volume KPI"
|
| 91 |
+
data-info="Total number of all customer service contacts this month across all channels — inbound calls, chat, email, social media, and in-store visits."
|
| 92 |
+
data-info-tips="Track contacts-per-customer ratio to measure self-service effectiveness.|Rising volume without subscriber growth = increasing friction.|Aim to shift volume from costly channels (phone) to cheaper ones (chat/app).">ⓘ</button>
|
| 93 |
+
</h6>
|
| 94 |
+
<h3 class="text-primary-custom" id="kpi-total-interactions">-</h3>
|
| 95 |
+
<small>This month</small>
|
| 96 |
+
</div>
|
| 97 |
+
</div>
|
| 98 |
+
<div class="col-md-3">
|
| 99 |
+
<div class="metric-card">
|
| 100 |
+
<h6>AVG WAIT TIME
|
| 101 |
+
<button class="info-btn"
|
| 102 |
+
data-info-icon="⏳"
|
| 103 |
+
data-info-title="Average Wait Time"
|
| 104 |
+
data-info-section="Customer Experience · Service KPI"
|
| 105 |
+
data-info="The mean time customers wait before being connected to an agent. Measured from first contact attempt to first agent response."
|
| 106 |
+
data-info-tips="Target: <2 min for phone, <30 sec for chat.|Every extra minute of wait time drops CSAT by ~0.3 points.|Reduce via callback options, chatbots, and workforce scheduling optimisation.">ⓘ</button>
|
| 107 |
+
</h6>
|
| 108 |
+
<h3 class="text-warning-custom" id="kpi-wait-time">-</h3>
|
| 109 |
+
<small>Before agent pickup</small>
|
| 110 |
+
</div>
|
| 111 |
+
</div>
|
| 112 |
+
<div class="col-md-3">
|
| 113 |
+
<div class="metric-card">
|
| 114 |
+
<h6>FOLLOW-UP REQUIRED
|
| 115 |
+
<button class="info-btn"
|
| 116 |
+
data-info-icon="📋"
|
| 117 |
+
data-info-title="Follow-Up Required Rate"
|
| 118 |
+
data-info-section="Customer Experience · Service KPI"
|
| 119 |
+
data-info="Percentage of service interactions that required a follow-up action — sending a technician, processing a refund, or further investigation. Indicates unresolved cases at first touch."
|
| 120 |
+
data-info-tips="Target: <15% follow-up rate.|High follow-up rate signals process gaps or missing agent tools.|Track follow-up completion time to ensure SLA adherence.">ⓘ</button>
|
| 121 |
+
</h6>
|
| 122 |
+
<h3 class="text-warning-custom" id="kpi-followup">-</h3>
|
| 123 |
+
<small>Pending actions</small>
|
| 124 |
+
</div>
|
| 125 |
+
</div>
|
| 126 |
+
</div>
|
| 127 |
+
|
| 128 |
+
<!-- Charts Row 1 -->
|
| 129 |
+
<div class="row mt-4">
|
| 130 |
+
<div class="col-md-6">
|
| 131 |
+
<div class="chart-card">
|
| 132 |
+
<h5 class="text-primary-custom mb-3">📊 Complaint Analysis by Type
|
| 133 |
+
<button class="info-btn"
|
| 134 |
+
data-info-icon="📋"
|
| 135 |
+
data-info-title="Complaint Analysis by Type"
|
| 136 |
+
data-info-section="Customer Experience · Complaint Chart"
|
| 137 |
+
data-info="A bar chart ranking the top complaint categories by volume. Helps identify the most frequent issues driving customer dissatisfaction and service contacts."
|
| 138 |
+
data-info-tips="Address the top 3 complaint types to resolve ~60% of total contact volume.|Call Drop and Data Speed complaints signal network investment needs.|Billing complaints usually indicate billing system gaps or confusing pricing.">ⓘ</button>
|
| 139 |
+
</h5>
|
| 140 |
+
<canvas id="complaintTypeChart"></canvas>
|
| 141 |
+
</div>
|
| 142 |
+
</div>
|
| 143 |
+
<div class="col-md-6">
|
| 144 |
+
<div class="chart-card">
|
| 145 |
+
<h5 class="text-primary-custom mb-3">📈 Customer Satisfaction Trend
|
| 146 |
+
<button class="info-btn"
|
| 147 |
+
data-info-icon="📈"
|
| 148 |
+
data-info-title="Customer Satisfaction Trend (CSAT)"
|
| 149 |
+
data-info-section="Customer Experience · Trend Chart"
|
| 150 |
+
data-info="A 12-month line chart showing how average CSAT scores have changed over time. Tracks the impact of service improvements, network upgrades, and agent training on customer experience."
|
| 151 |
+
data-info-tips="Look for correlation between CSAT drops and network incidents.|Sustained CSAT improvement >0.3 pts indicates meaningful programme impact.|Share this chart in monthly CX reviews to track progress against targets.">ⓘ</button>
|
| 152 |
+
</h5>
|
| 153 |
+
<canvas id="csatTrendChart"></canvas>
|
| 154 |
+
</div>
|
| 155 |
+
</div>
|
| 156 |
+
</div>
|
| 157 |
+
|
| 158 |
+
<!-- Charts Row 2 -->
|
| 159 |
+
<div class="row">
|
| 160 |
+
<div class="col-md-6">
|
| 161 |
+
<div class="chart-card">
|
| 162 |
+
<h5 class="text-primary-custom mb-3">🎯 NPS Distribution
|
| 163 |
+
<button class="info-btn"
|
| 164 |
+
data-info-icon="🎯"
|
| 165 |
+
data-info-title="NPS Promoter/Passive/Detractor Split"
|
| 166 |
+
data-info-section="Customer Experience · NPS Chart"
|
| 167 |
+
data-info="A doughnut chart breaking down the NPS survey responses into three groups: Promoters (score 9–10), Passives (7–8), and Detractors (0–6)."
|
| 168 |
+
data-info-tips="Maximise Promoters — they are your best word-of-mouth marketers.|Convert Passives with loyalty rewards and plan upgrade incentives.|Address Detractors proactively — 1 Detractor offsets 5 Promoters in brand impact.">ⓘ</button>
|
| 169 |
+
</h5>
|
| 170 |
+
<canvas id="npsDistributionChart"></canvas>
|
| 171 |
+
</div>
|
| 172 |
+
</div>
|
| 173 |
+
<div class="col-md-6">
|
| 174 |
+
<div class="chart-card">
|
| 175 |
+
<h5 class="text-primary-custom mb-3">📞 Resolution Time by Channel
|
| 176 |
+
<button class="info-btn"
|
| 177 |
+
data-info-icon="📞"
|
| 178 |
+
data-info-title="Resolution Time by Contact Channel"
|
| 179 |
+
data-info-section="Customer Experience · Channel Chart"
|
| 180 |
+
data-info="A horizontal bar chart comparing average resolution times across service channels — Phone, Chat, Email, Social, and In-Store. Shows which channels are most efficient."
|
| 181 |
+
data-info-tips="Chat and self-service typically have the fastest resolution times.|Email tends to have the longest — automate first-response where possible.|Use this to guide channel-shift strategy and agent staffing decisions.">ⓘ</button>
|
| 182 |
+
</h5>
|
| 183 |
+
<canvas id="resolutionChannelChart"></canvas>
|
| 184 |
+
</div>
|
| 185 |
+
</div>
|
| 186 |
+
</div>
|
| 187 |
+
|
| 188 |
+
{% endblock %}
|
| 189 |
+
|
| 190 |
+
{% block extra_js %}
|
| 191 |
+
<script>
|
| 192 |
+
Chart.defaults.color = '#64748b';
|
| 193 |
+
Chart.defaults.borderColor = '#e2e8f0';
|
| 194 |
+
Chart.defaults.font.family = "'Inter', sans-serif";
|
| 195 |
+
|
| 196 |
+
const COLORS = {
|
| 197 |
+
primary: '#4f46e5',
|
| 198 |
+
success: '#10b981',
|
| 199 |
+
warning: '#f59e0b',
|
| 200 |
+
danger: '#ef4444',
|
| 201 |
+
info: '#3b82f6'
|
| 202 |
+
};
|
| 203 |
+
|
| 204 |
+
async function loadCustomerKPIs() {
|
| 205 |
+
try {
|
| 206 |
+
const response = await fetch('/api/customer/kpis');
|
| 207 |
+
const data = await response.json();
|
| 208 |
+
|
| 209 |
+
document.getElementById('kpi-resolution').textContent = `${data.avg_resolution} min`;
|
| 210 |
+
document.getElementById('kpi-resolution-rate').textContent = `${data.resolution_rate}%`;
|
| 211 |
+
document.getElementById('kpi-csat').textContent = `${data.avg_csat}/10`;
|
| 212 |
+
document.getElementById('kpi-escalations').textContent = `${data.escalation_rate}%`;
|
| 213 |
+
|
| 214 |
+
// Enhanced KPIs
|
| 215 |
+
const response2 = await fetch('/api/customer/enhanced-kpis');
|
| 216 |
+
const enhanced = await response2.json();
|
| 217 |
+
|
| 218 |
+
document.getElementById('kpi-nps').textContent = enhanced.nps;
|
| 219 |
+
document.getElementById('kpi-total-interactions').textContent = enhanced.total_interactions.toLocaleString();
|
| 220 |
+
document.getElementById('kpi-wait-time').textContent = `${enhanced.avg_wait_time} min`;
|
| 221 |
+
document.getElementById('kpi-followup').textContent = `${enhanced.followup_rate}%`;
|
| 222 |
+
} catch (error) {
|
| 223 |
+
console.error('Error loading customer KPIs:', error);
|
| 224 |
+
}
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
async function loadComplaintTypeChart() {
|
| 228 |
+
try {
|
| 229 |
+
const response = await fetch('/api/customer/complaint-analysis');
|
| 230 |
+
const data = await response.json();
|
| 231 |
+
|
| 232 |
+
const ctx = document.getElementById('complaintTypeChart').getContext('2d');
|
| 233 |
+
new Chart(ctx, {
|
| 234 |
+
type: 'bar',
|
| 235 |
+
data: {
|
| 236 |
+
labels: data.labels,
|
| 237 |
+
datasets: [{
|
| 238 |
+
label: 'Number of Complaints',
|
| 239 |
+
data: data.values,
|
| 240 |
+
backgroundColor: COLORS.primary,
|
| 241 |
+
borderRadius: 8
|
| 242 |
+
}]
|
| 243 |
+
},
|
| 244 |
+
options: {
|
| 245 |
+
responsive: true,
|
| 246 |
+
plugins: {
|
| 247 |
+
legend: { display: false }
|
| 248 |
+
},
|
| 249 |
+
scales: {
|
| 250 |
+
y: {
|
| 251 |
+
beginAtZero: true,
|
| 252 |
+
grid: { color: '#f1f5f9' }
|
| 253 |
+
}
|
| 254 |
+
}
|
| 255 |
+
}
|
| 256 |
+
});
|
| 257 |
+
} catch (error) {
|
| 258 |
+
console.error('Error loading complaint chart:', error);
|
| 259 |
+
}
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
async function loadCSATTrendChart() {
|
| 263 |
+
try {
|
| 264 |
+
const response = await fetch('/api/customer/csat-trend');
|
| 265 |
+
const data = await response.json();
|
| 266 |
+
|
| 267 |
+
const ctx = document.getElementById('csatTrendChart').getContext('2d');
|
| 268 |
+
new Chart(ctx, {
|
| 269 |
+
type: 'line',
|
| 270 |
+
data: {
|
| 271 |
+
labels: data.months,
|
| 272 |
+
datasets: [{
|
| 273 |
+
label: 'CSAT Score',
|
| 274 |
+
data: data.scores,
|
| 275 |
+
borderColor: COLORS.success,
|
| 276 |
+
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
| 277 |
+
fill: true,
|
| 278 |
+
tension: 0.4,
|
| 279 |
+
borderWidth: 3
|
| 280 |
+
}]
|
| 281 |
+
},
|
| 282 |
+
options: {
|
| 283 |
+
responsive: true,
|
| 284 |
+
plugins: {
|
| 285 |
+
legend: { display: false }
|
| 286 |
+
},
|
| 287 |
+
scales: {
|
| 288 |
+
y: {
|
| 289 |
+
beginAtZero: true,
|
| 290 |
+
max: 10,
|
| 291 |
+
grid: { color: '#f1f5f9' }
|
| 292 |
+
}
|
| 293 |
+
}
|
| 294 |
+
}
|
| 295 |
+
});
|
| 296 |
+
} catch (error) {
|
| 297 |
+
console.error('Error loading CSAT trend:', error);
|
| 298 |
+
}
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
async function loadNPSDistributionChart() {
|
| 302 |
+
try {
|
| 303 |
+
const response = await fetch('/api/customer/nps-distribution');
|
| 304 |
+
const data = await response.json();
|
| 305 |
+
|
| 306 |
+
const ctx = document.getElementById('npsDistributionChart').getContext('2d');
|
| 307 |
+
new Chart(ctx, {
|
| 308 |
+
type: 'doughnut',
|
| 309 |
+
data: {
|
| 310 |
+
labels: ['Promoters (9-10)', 'Passives (7-8)', 'Detractors (0-6)'],
|
| 311 |
+
datasets: [{
|
| 312 |
+
data: [data.promoters, data.passives, data.detractors],
|
| 313 |
+
backgroundColor: [COLORS.success, COLORS.warning, COLORS.danger],
|
| 314 |
+
borderWidth: 4,
|
| 315 |
+
borderColor: '#ffffff'
|
| 316 |
+
}]
|
| 317 |
+
},
|
| 318 |
+
options: {
|
| 319 |
+
responsive: true,
|
| 320 |
+
plugins: {
|
| 321 |
+
legend: { position: 'right' }
|
| 322 |
+
}
|
| 323 |
+
}
|
| 324 |
+
});
|
| 325 |
+
} catch (error) {
|
| 326 |
+
console.error('Error loading NPS distribution:', error);
|
| 327 |
+
}
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
async function loadResolutionChannelChart() {
|
| 331 |
+
try {
|
| 332 |
+
const response = await fetch('/api/customer/resolution-by-channel');
|
| 333 |
+
const data = await response.json();
|
| 334 |
+
|
| 335 |
+
const ctx = document.getElementById('resolutionChannelChart').getContext('2d');
|
| 336 |
+
new Chart(ctx, {
|
| 337 |
+
type: 'bar',
|
| 338 |
+
data: {
|
| 339 |
+
labels: data.channels,
|
| 340 |
+
datasets: [{
|
| 341 |
+
label: 'Avg Resolution Time (min)',
|
| 342 |
+
data: data.times,
|
| 343 |
+
backgroundColor: COLORS.info,
|
| 344 |
+
borderRadius: 8
|
| 345 |
+
}]
|
| 346 |
+
},
|
| 347 |
+
options: {
|
| 348 |
+
responsive: true,
|
| 349 |
+
indexAxis: 'y',
|
| 350 |
+
plugins: {
|
| 351 |
+
legend: { display: false }
|
| 352 |
+
},
|
| 353 |
+
scales: {
|
| 354 |
+
x: {
|
| 355 |
+
beginAtZero: true,
|
| 356 |
+
grid: { color: '#f1f5f9' }
|
| 357 |
+
}
|
| 358 |
+
}
|
| 359 |
+
}
|
| 360 |
+
});
|
| 361 |
+
} catch (error) {
|
| 362 |
+
console.error('Error loading resolution channel chart:', error);
|
| 363 |
+
}
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 367 |
+
loadCustomerKPIs();
|
| 368 |
+
loadComplaintTypeChart();
|
| 369 |
+
loadCSATTrendChart();
|
| 370 |
+
loadNPSDistributionChart();
|
| 371 |
+
loadResolutionChannelChart();
|
| 372 |
+
});
|
| 373 |
+
</script>
|
| 374 |
+
{% endblock %}
|
templates/executive.html
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}Executive Dashboard - TelecomIQ{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<h2 class="text-primary-custom mb-4">Executive Dashboard</h2>
|
| 7 |
+
|
| 8 |
+
<!-- KPI Cards Row 1 -->
|
| 9 |
+
<div class="row">
|
| 10 |
+
<div class="col-md-3">
|
| 11 |
+
<div class="metric-card">
|
| 12 |
+
<h6>TOTAL CUSTOMERS
|
| 13 |
+
<button class="info-btn"
|
| 14 |
+
data-info-icon="👥"
|
| 15 |
+
data-info-title="Total Customers"
|
| 16 |
+
data-info-section="Executive Dashboard · Subscriber KPI"
|
| 17 |
+
data-info="The total count of all active subscribers currently on the platform. This includes all plan types — prepaid, postpaid, basic, standard, and premium."
|
| 18 |
+
data-info-tips="A healthy growth rate is 2–5% month-over-month.|Declining count may signal churn exceeding acquisition.|Compare with industry benchmarks: telecom averages ~1–3% monthly net adds.">ⓘ</button>
|
| 19 |
+
</h6>
|
| 20 |
+
<h3 class="text-primary-custom" id="kpi-total-customers">-</h3>
|
| 21 |
+
<small>Active subscribers</small>
|
| 22 |
+
</div>
|
| 23 |
+
</div>
|
| 24 |
+
<div class="col-md-3">
|
| 25 |
+
<div class="metric-card">
|
| 26 |
+
<h6>MONTHLY REVENUE
|
| 27 |
+
<button class="info-btn"
|
| 28 |
+
data-info-icon="💰"
|
| 29 |
+
data-info-title="Monthly Revenue (MRR)"
|
| 30 |
+
data-info-section="Executive Dashboard · Financial KPI"
|
| 31 |
+
data-info="Total Monthly Recurring Revenue generated from all billing records in the current period. Shown in millions of USD ($M)."
|
| 32 |
+
data-info-tips="MRR = sum of all customer plan charges + add-ons.|Growth target: 3–7% QoQ for mature telecom operators.|Watch for seasonal dips in Jan–Feb and spikes in Nov–Dec.">ⓘ</button>
|
| 33 |
+
</h6>
|
| 34 |
+
<h3 class="text-success-custom" id="kpi-revenue">-</h3>
|
| 35 |
+
<small>Total MRR</small>
|
| 36 |
+
</div>
|
| 37 |
+
</div>
|
| 38 |
+
<div class="col-md-3">
|
| 39 |
+
<div class="metric-card">
|
| 40 |
+
<h6>CHURN RATE
|
| 41 |
+
<button class="info-btn"
|
| 42 |
+
data-info-icon="⚠️"
|
| 43 |
+
data-info-title="Churn Rate"
|
| 44 |
+
data-info-section="Executive Dashboard · Retention KPI"
|
| 45 |
+
data-info="Percentage of customers who have discontinued service in the current period. Calculated as: (Churned Customers ÷ Total Customers) × 100."
|
| 46 |
+
data-info-tips="Industry benchmark: 1.5–2.5% monthly churn is typical for telecom.|Above 3% monthly is a red flag requiring immediate retention action.|High churn often correlates with poor network quality or rising ARPU.">ⓘ</button>
|
| 47 |
+
</h6>
|
| 48 |
+
<h3 class="text-warning-custom" id="kpi-churn-rate">-</h3>
|
| 49 |
+
<small>Current period</small>
|
| 50 |
+
</div>
|
| 51 |
+
</div>
|
| 52 |
+
<div class="col-md-3">
|
| 53 |
+
<div class="metric-card">
|
| 54 |
+
<h6>AVG ARPU
|
| 55 |
+
<button class="info-btn"
|
| 56 |
+
data-info-icon="📊"
|
| 57 |
+
data-info-title="Average Revenue Per User (ARPU)"
|
| 58 |
+
data-info-section="Executive Dashboard · Financial KPI"
|
| 59 |
+
data-info="Average Revenue Per User — the mean monthly billing amount across all active customers. A core profitability metric for telecom operators."
|
| 60 |
+
data-info-tips="Formula: Total Revenue ÷ Total Active Subscribers.|Higher ARPU from premium plans improves margins without needing more customers.|ARPU compression can signal plan downgrades or excessive discounting.">ⓘ</button>
|
| 61 |
+
</h6>
|
| 62 |
+
<h3 class="text-primary-custom" id="kpi-arpu">-</h3>
|
| 63 |
+
<small>Per user/month</small>
|
| 64 |
+
</div>
|
| 65 |
+
</div>
|
| 66 |
+
</div>
|
| 67 |
+
|
| 68 |
+
<!-- KPI Cards Row 2 -->
|
| 69 |
+
<div class="row">
|
| 70 |
+
<div class="col-md-3">
|
| 71 |
+
<div class="metric-card">
|
| 72 |
+
<h6>HIGH RISK
|
| 73 |
+
<button class="info-btn"
|
| 74 |
+
data-info-icon="🚨"
|
| 75 |
+
data-info-title="High-Risk Customers"
|
| 76 |
+
data-info-section="Executive Dashboard · Churn Risk KPI"
|
| 77 |
+
data-info="Number of customers classified as 'High' churn-risk by the ML model. These customers have a predicted churn probability above 60% and require immediate retention intervention."
|
| 78 |
+
data-info-tips="Prioritise outbound retention calls for this segment.|Typical retention offer: plan upgrade, discount, or loyalty reward.|LTV of a retained high-risk customer often exceeds the retention cost 3–5×.">ⓘ</button>
|
| 79 |
+
</h6>
|
| 80 |
+
<h3 class="text-danger-custom" id="kpi-high-risk">-</h3>
|
| 81 |
+
<small>Churn intervention needed</small>
|
| 82 |
+
</div>
|
| 83 |
+
</div>
|
| 84 |
+
<div class="col-md-3">
|
| 85 |
+
<div class="metric-card">
|
| 86 |
+
<h6>SATISFACTION
|
| 87 |
+
<button class="info-btn"
|
| 88 |
+
data-info-icon="⭐"
|
| 89 |
+
data-info-title="Average Customer Satisfaction (CSAT)"
|
| 90 |
+
data-info-section="Executive Dashboard · CX KPI"
|
| 91 |
+
data-info="The mean customer satisfaction score from all recorded service interactions. Scored on a scale of 1–10 where 10 is the highest satisfaction."
|
| 92 |
+
data-info-tips="Target CSAT for telecom: ≥7.5/10.|Scores below 6 correlate strongly with churn within 90 days.|Driven by resolution time, agent quality, and network reliability.">ⓘ</button>
|
| 93 |
+
</h6>
|
| 94 |
+
<h3 class="text-success-custom" id="kpi-satisfaction">-</h3>
|
| 95 |
+
<small>Average CSAT</small>
|
| 96 |
+
</div>
|
| 97 |
+
</div>
|
| 98 |
+
<div class="col-md-3">
|
| 99 |
+
<div class="metric-card">
|
| 100 |
+
<h6>NETWORK UPTIME
|
| 101 |
+
<button class="info-btn"
|
| 102 |
+
data-info-icon="📡"
|
| 103 |
+
data-info-title="Network Availability / Uptime"
|
| 104 |
+
data-info-section="Executive Dashboard · Network KPI"
|
| 105 |
+
data-info="Average network availability percentage across all cell towers in the last 30 days. Represents the proportion of time the network was operational and serving customers."
|
| 106 |
+
data-info-tips="Industry gold standard: 99.9% (53 min downtime/month max).|99.5% = ~3.6 hrs downtime/month — may trigger SLA penalties.|Dips usually correlate with equipment failure or maintenance windows.">ⓘ</button>
|
| 107 |
+
</h6>
|
| 108 |
+
<h3 class="text-success-custom" id="kpi-uptime">-</h3>
|
| 109 |
+
<small>Last 30 days</small>
|
| 110 |
+
</div>
|
| 111 |
+
</div>
|
| 112 |
+
<div class="col-md-3">
|
| 113 |
+
<div class="metric-card">
|
| 114 |
+
<h6>SERVICE CALLS
|
| 115 |
+
<button class="info-btn"
|
| 116 |
+
data-info-icon="📞"
|
| 117 |
+
data-info-title="Service Calls / Interactions"
|
| 118 |
+
data-info-section="Executive Dashboard · Support KPI"
|
| 119 |
+
data-info="Total number of customer service interactions recorded in the current period. Includes inbound calls, chat sessions, and in-store visits."
|
| 120 |
+
data-info-tips="High volume is normal but watch cost-per-interaction.|Goal: reduce repeat contacts (contacts about the same issue >once).|Segment by complaint type to find the biggest drivers.">ⓘ</button>
|
| 121 |
+
</h6>
|
| 122 |
+
<h3 class="text-warning-custom" id="kpi-service-calls">-</h3>
|
| 123 |
+
<small>Last period</small>
|
| 124 |
+
</div>
|
| 125 |
+
</div>
|
| 126 |
+
</div>
|
| 127 |
+
|
| 128 |
+
<!-- Charts Row 1 -->
|
| 129 |
+
<div class="row">
|
| 130 |
+
<div class="col-md-6">
|
| 131 |
+
<div class="chart-card">
|
| 132 |
+
<h5 class="text-primary-custom mb-3">Monthly Revenue Trend
|
| 133 |
+
<button class="info-btn"
|
| 134 |
+
data-info-icon="📈"
|
| 135 |
+
data-info-title="Monthly Revenue Trend"
|
| 136 |
+
data-info-section="Executive Dashboard · Revenue Chart"
|
| 137 |
+
data-info="A time-series line chart showing total billing revenue (in $M) aggregated per calendar month. Reveals seasonal patterns and long-term growth trajectory."
|
| 138 |
+
data-info-tips="Upward slope = net subscriber growth + ARPU expansion.|Flat periods may indicate seasonal churn offsetting new activations.|Use this to set quarterly revenue targets and board reporting.">ⓘ</button>
|
| 139 |
+
</h5>
|
| 140 |
+
<canvas id="revenueChart"></canvas>
|
| 141 |
+
</div>
|
| 142 |
+
</div>
|
| 143 |
+
<div class="col-md-6">
|
| 144 |
+
<div class="chart-card">
|
| 145 |
+
<h5 class="text-primary-custom mb-3">Churn Rate by Plan Type
|
| 146 |
+
<button class="info-btn"
|
| 147 |
+
data-info-icon="📉"
|
| 148 |
+
data-info-title="Churn Rate by Plan Type"
|
| 149 |
+
data-info-section="Executive Dashboard · Churn Chart"
|
| 150 |
+
data-info="A bar chart comparing the churn rate (%) across different subscription plan types — Basic, Standard, and Premium. Helps identify which plans retain customers best."
|
| 151 |
+
data-info-tips="Higher churn in Basic plans is common — price-sensitive segment.|Low Premium churn indicates strong perceived value.|Consider targeted save-offers for the highest-churn plan tier.">ⓘ</button>
|
| 152 |
+
</h5>
|
| 153 |
+
<canvas id="churnPlanChart"></canvas>
|
| 154 |
+
</div>
|
| 155 |
+
</div>
|
| 156 |
+
</div>
|
| 157 |
+
|
| 158 |
+
<!-- Charts Row 2 -->
|
| 159 |
+
<div class="row">
|
| 160 |
+
<div class="col-md-6">
|
| 161 |
+
<div class="chart-card">
|
| 162 |
+
<h5 class="text-primary-custom mb-3">Customer Distribution by Tenure
|
| 163 |
+
<button class="info-btn"
|
| 164 |
+
data-info-icon="🕐"
|
| 165 |
+
data-info-title="Customer Distribution by Tenure"
|
| 166 |
+
data-info-section="Executive Dashboard · Tenure Chart"
|
| 167 |
+
data-info="A doughnut chart showing how customers are spread across tenure bands: 0–6 months, 6–12 months, 1–2 years, 2–3 years, and 3+ years."
|
| 168 |
+
data-info-tips="A large 0–6m segment signals strong recent acquisition.|Large 3y+ segment = loyal base — strong re-contract opportunity.|High churn usually concentrates in 0–12m band (first-year at-risk period).">ⓘ</button>
|
| 169 |
+
</h5>
|
| 170 |
+
<canvas id="tenureChart"></canvas>
|
| 171 |
+
</div>
|
| 172 |
+
</div>
|
| 173 |
+
<div class="col-md-6">
|
| 174 |
+
<div class="chart-card">
|
| 175 |
+
<h5 class="text-primary-custom mb-3">Customers by Churn Risk
|
| 176 |
+
<button class="info-btn"
|
| 177 |
+
data-info-icon="🎯"
|
| 178 |
+
data-info-title="Customers by Churn Risk Category"
|
| 179 |
+
data-info-section="Executive Dashboard · Risk Chart"
|
| 180 |
+
data-info="A bar chart segmenting all customers into Low, Medium, and High churn-risk categories as determined by the ML churn model. Green = Low, Amber = Medium, Red = High."
|
| 181 |
+
data-info-tips="Ideal distribution: >70% Low, <10% High.|Medium-risk customers are the most cost-effective to retain.|Use this for budget allocation: High-risk gets proactive offers, Medium gets nurture campaigns.">ⓘ</button>
|
| 182 |
+
</h5>
|
| 183 |
+
<canvas id="riskChart"></canvas>
|
| 184 |
+
</div>
|
| 185 |
+
</div>
|
| 186 |
+
</div>
|
| 187 |
+
|
| 188 |
+
{% endblock %}
|
| 189 |
+
|
| 190 |
+
{% block extra_js %}
|
| 191 |
+
<script>
|
| 192 |
+
Chart.defaults.color = '#64748b';
|
| 193 |
+
Chart.defaults.borderColor = '#e2e8f0';
|
| 194 |
+
Chart.defaults.font.family = "'Inter', sans-serif";
|
| 195 |
+
|
| 196 |
+
const COLORS = {
|
| 197 |
+
primary: '#4f46e5', primaryLight: '#6366f1', secondary: '#ec4899',
|
| 198 |
+
success: '#10b981', warning: '#f59e0b', danger: '#ef4444',
|
| 199 |
+
info: '#3b82f6', purple: '#8b5cf6', teal: '#14b8a6'
|
| 200 |
+
};
|
| 201 |
+
|
| 202 |
+
async function loadKPIs() {
|
| 203 |
+
try {
|
| 204 |
+
const response = await fetch('/api/executive/kpis');
|
| 205 |
+
const data = await response.json();
|
| 206 |
+
document.getElementById('kpi-total-customers').textContent = data.total_customers.toLocaleString();
|
| 207 |
+
document.getElementById('kpi-revenue').textContent = `$${data.total_revenue.toFixed(2)}M`;
|
| 208 |
+
document.getElementById('kpi-churn-rate').textContent = `${data.churn_rate}%`;
|
| 209 |
+
document.getElementById('kpi-arpu').textContent = `$${data.avg_arpu.toFixed(2)}`;
|
| 210 |
+
document.getElementById('kpi-high-risk').textContent = data.high_risk.toLocaleString();
|
| 211 |
+
document.getElementById('kpi-satisfaction').textContent = `${data.avg_satisfaction}/10`;
|
| 212 |
+
document.getElementById('kpi-uptime').textContent = `${data.network_availability.toFixed(2)}%`;
|
| 213 |
+
document.getElementById('kpi-service-calls').textContent = data.service_calls.toLocaleString();
|
| 214 |
+
} catch (error) { console.error('Error loading KPIs:', error); }
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
async function loadRevenueChart() {
|
| 218 |
+
try {
|
| 219 |
+
const response = await fetch('/api/executive/revenue-trend');
|
| 220 |
+
const data = await response.json();
|
| 221 |
+
const ctx = document.getElementById('revenueChart').getContext('2d');
|
| 222 |
+
new Chart(ctx, {
|
| 223 |
+
type: 'line',
|
| 224 |
+
data: { labels: data.labels, datasets: [{ label: 'Revenue ($M)', data: data.data, borderColor: COLORS.success, backgroundColor: 'rgba(16,185,129,0.1)', fill: true, tension: 0.4, borderWidth: 3, pointBackgroundColor: COLORS.success, pointBorderColor: '#ffffff', pointBorderWidth: 2, pointRadius: 4, pointHoverRadius: 6 }] },
|
| 225 |
+
options: { responsive: true, maintainAspectRatio: true, plugins: { legend: { display: false }, tooltip: { backgroundColor: '#ffffff', titleColor: '#1e293b', bodyColor: '#64748b', borderColor: '#e2e8f0', borderWidth: 1, padding: 12, displayColors: false, callbacks: { label: ctx => 'Revenue: $' + ctx.parsed.y + 'M' } } }, scales: { y: { beginAtZero: true, grid: { color: '#f1f5f9', drawBorder: false }, ticks: { callback: v => '$' + v + 'M' } }, x: { grid: { display: false } } } }
|
| 226 |
+
});
|
| 227 |
+
} catch (error) { console.error('Error loading revenue chart:', error); }
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
async function loadChurnPlanChart() {
|
| 231 |
+
try {
|
| 232 |
+
const response = await fetch('/api/executive/churn-by-plan');
|
| 233 |
+
const data = await response.json();
|
| 234 |
+
const ctx = document.getElementById('churnPlanChart').getContext('2d');
|
| 235 |
+
new Chart(ctx, {
|
| 236 |
+
type: 'bar',
|
| 237 |
+
data: { labels: data.labels, datasets: [{ label: 'Churn Rate (%)', data: data.data, backgroundColor: 'rgba(239,68,68,0.8)', borderColor: COLORS.danger, borderWidth: 0, borderRadius: 8 }] },
|
| 238 |
+
options: { responsive: true, maintainAspectRatio: true, plugins: { legend: { display: false }, tooltip: { backgroundColor: '#ffffff', titleColor: '#1e293b', bodyColor: '#64748b', borderColor: '#e2e8f0', borderWidth: 1, padding: 12, displayColors: false, callbacks: { label: ctx => 'Churn Rate: ' + ctx.parsed.y + '%' } } }, scales: { y: { beginAtZero: true, grid: { color: '#f1f5f9', drawBorder: false }, ticks: { callback: v => v + '%' } }, x: { grid: { display: false } } } }
|
| 239 |
+
});
|
| 240 |
+
} catch (error) { console.error('Error loading churn plan chart:', error); }
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
async function loadTenureChart() {
|
| 244 |
+
try {
|
| 245 |
+
const response = await fetch('/api/executive/tenure-distribution');
|
| 246 |
+
const data = await response.json();
|
| 247 |
+
const ctx = document.getElementById('tenureChart').getContext('2d');
|
| 248 |
+
new Chart(ctx, {
|
| 249 |
+
type: 'doughnut',
|
| 250 |
+
data: { labels: data.labels, datasets: [{ data: data.data, backgroundColor: [COLORS.primary, COLORS.info, COLORS.success, COLORS.warning, COLORS.purple], borderWidth: 4, borderColor: '#ffffff', hoverOffset: 8 }] },
|
| 251 |
+
options: { responsive: true, maintainAspectRatio: true, plugins: { legend: { position: 'right', labels: { padding: 15, font: { size: 12, weight: 500 }, usePointStyle: true, pointStyle: 'circle' } }, tooltip: { backgroundColor: '#ffffff', titleColor: '#1e293b', bodyColor: '#64748b', borderColor: '#e2e8f0', borderWidth: 1, padding: 12, callbacks: { label: ctx => { const t = ctx.dataset.data.reduce((a,b)=>a+b,0); return ctx.label+': '+ctx.parsed.toLocaleString()+' ('+((ctx.parsed/t)*100).toFixed(1)+'%)'; } } } } }
|
| 252 |
+
});
|
| 253 |
+
} catch (error) { console.error('Error loading tenure chart:', error); }
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
async function loadRiskChart() {
|
| 257 |
+
try {
|
| 258 |
+
const response = await fetch('/api/executive/risk-distribution');
|
| 259 |
+
const data = await response.json();
|
| 260 |
+
const ctx = document.getElementById('riskChart').getContext('2d');
|
| 261 |
+
new Chart(ctx, {
|
| 262 |
+
type: 'bar',
|
| 263 |
+
data: { labels: data.labels, datasets: [{ label: 'Customers', data: data.data, backgroundColor: data.labels.map(l => l==='Low'?'rgba(16,185,129,0.8)':l==='Medium'?'rgba(245,158,11,0.8)':'rgba(239,68,68,0.8)'), borderWidth: 0, borderRadius: 8 }] },
|
| 264 |
+
options: { responsive: true, maintainAspectRatio: true, plugins: { legend: { display: false }, tooltip: { backgroundColor: '#ffffff', titleColor: '#1e293b', bodyColor: '#64748b', borderColor: '#e2e8f0', borderWidth: 1, padding: 12, displayColors: false, callbacks: { label: ctx => 'Customers: ' + ctx.parsed.y.toLocaleString() } } }, scales: { y: { beginAtZero: true, grid: { color: '#f1f5f9', drawBorder: false }, ticks: { callback: v => v.toLocaleString() } }, x: { grid: { display: false } } } }
|
| 265 |
+
});
|
| 266 |
+
} catch (error) { console.error('Error loading risk chart:', error); }
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 270 |
+
loadKPIs(); loadRevenueChart(); loadChurnPlanChart(); loadTenureChart(); loadRiskChart();
|
| 271 |
+
});
|
| 272 |
+
</script>
|
| 273 |
+
{% endblock %}
|
templates/financial.html
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}Financial Performance - TelecomIQ{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<h2 class="text-primary-custom mb-4">💰 Financial Performance Dashboard</h2>
|
| 7 |
+
|
| 8 |
+
<div class="row">
|
| 9 |
+
<div class="col-md-3"><div class="metric-card">
|
| 10 |
+
<h6>TOTAL REVENUE <button class="info-btn" data-info-icon="💰" data-info-title="Total Revenue" data-info-section="Financial · Revenue KPI" data-info="Total billing revenue generated across all customers in the current period, shown in $M." data-info-tips="Compare to budget targets to assess growth trajectory.|Include one-off revenue separately to reveal recurring vs. transactional income.">ⓘ</button></h6>
|
| 11 |
+
<h3 class="text-success-custom" id="kpi-total-revenue">-</h3><small>Current period</small>
|
| 12 |
+
</div></div>
|
| 13 |
+
<div class="col-md-3"><div class="metric-card">
|
| 14 |
+
<h6>AVG ARPU <button class="info-btn" data-info-icon="📊" data-info-title="Average Revenue Per User (ARPU)" data-info-section="Financial · Revenue KPI" data-info="Mean monthly billing amount per active subscriber. A core profitability and pricing effectiveness metric." data-info-tips="Increasing ARPU without subscriber growth = upsell success.|Declining ARPU may signal plan downgrade pressure or discounting.">ⓘ</button></h6>
|
| 15 |
+
<h3 class="text-primary-custom" id="kpi-arpu">-</h3><small>Per customer/month</small>
|
| 16 |
+
</div></div>
|
| 17 |
+
<div class="col-md-3"><div class="metric-card">
|
| 18 |
+
<h6>CUSTOMER LTV <button class="info-btn" data-info-icon="🏆" data-info-title="Customer Lifetime Value (LTV)" data-info-section="Financial · Value KPI" data-info="The total predicted revenue a customer will generate over their entire relationship with the company. Estimated as ARPU × expected tenure months." data-info-tips="High LTV customers deserve premium retention investment.|LTV:CAC ratio >3× is considered healthy for telecom.|Improving churn rate by 1% adds ~10–15% to average LTV.">ⓘ</button></h6>
|
| 19 |
+
<h3 class="text-success-custom" id="kpi-ltv">-</h3><small>Average lifetime value</small>
|
| 20 |
+
</div></div>
|
| 21 |
+
<div class="col-md-3"><div class="metric-card">
|
| 22 |
+
<h6>CHURN IMPACT <button class="info-btn" data-info-icon="⚠️" data-info-title="Revenue at Risk from Churn" data-info-section="Financial · Risk KPI" data-info="Estimated monthly revenue loss due to churned customers. Calculated as Total Revenue × Churn Rate. Represents the financial cost of retention failures." data-info-tips="This is the upper bound — actual loss depends on replacement rate.|Use this to justify retention investment ROI.|A 0.5% churn rate reduction could recover millions in annual revenue.">ⓘ</button></h6>
|
| 23 |
+
<h3 class="text-danger-custom" id="kpi-churn-cost">-</h3><small>Revenue at risk</small>
|
| 24 |
+
</div></div>
|
| 25 |
+
</div>
|
| 26 |
+
|
| 27 |
+
<div class="row">
|
| 28 |
+
<div class="col-md-3"><div class="metric-card">
|
| 29 |
+
<h6>MRR GROWTH <button class="info-btn" data-info-icon="📈" data-info-title="MRR Month-over-Month Growth" data-info-section="Financial · Growth KPI" data-info="The percentage change in Monthly Recurring Revenue compared to the prior month. Positive MRR growth indicates net expansion in revenue." data-info-tips="Target: 3–7% MoM for growth-stage telcos.|Negative MRR growth requires immediate churn and ARPU analysis.|Decompose into new MRR, expansion MRR, and churned MRR.">ⓘ</button></h6>
|
| 30 |
+
<h3 class="text-success-custom" id="kpi-mrr-growth">-</h3><small>Month-over-month</small>
|
| 31 |
+
</div></div>
|
| 32 |
+
<div class="col-md-3"><div class="metric-card">
|
| 33 |
+
<h6>REVENUE/TOWER <button class="info-btn" data-info-icon="🗼" data-info-title="Revenue per Tower (Infra ROI)" data-info-section="Financial · Infrastructure KPI" data-info="Total revenue divided by the number of active cell towers. A measure of infrastructure return on investment — how much revenue each tower is generating." data-info-tips="Rising revenue/tower = improving infrastructure efficiency.|Compare against tower CAPEX and OPEX to calculate true ROI.|Rural towers typically generate lower revenue/tower but are needed for coverage SLAs.">ⓘ</button></h6>
|
| 34 |
+
<h3 class="text-primary-custom" id="kpi-revenue-per-tower">-</h3><small>Infrastructure ROI</small>
|
| 35 |
+
</div></div>
|
| 36 |
+
<div class="col-md-3"><div class="metric-card">
|
| 37 |
+
<h6>HIGH-VALUE CUSTOMERS <button class="info-btn" data-info-icon="💎" data-info-title="High-Value Customer Count" data-info-section="Financial · Segment KPI" data-info="The top 20% of customers by lifetime value or ARPU contribution. These customers typically generate 60–80% of total revenue (Pareto principle)." data-info-tips="Protect this segment with proactive retention and VIP service.|Any churn in this segment has 5× the revenue impact of average churn.|Consider a dedicated high-value customer team or account management.">ⓘ</button></h6>
|
| 38 |
+
<h3 class="text-success-custom" id="kpi-high-value">-</h3><small>Top 20% contributors</small>
|
| 39 |
+
</div></div>
|
| 40 |
+
<div class="col-md-3"><div class="metric-card">
|
| 41 |
+
<h6>PRICING INDEX <button class="info-btn" data-info-icon="🏷️" data-info-title="Competitive Pricing Index" data-info-section="Financial · Competitive KPI" data-info="Your average plan price relative to the market average, indexed to 100. A value above 100 means your prices are higher than competitors on average." data-info-tips="Index 95–105 = competitively priced.|Above 110 = premium positioning — justify with quality or features.|Below 90 = price leader — watch margins and ensure cost efficiency.">ⓘ</button></h6>
|
| 42 |
+
<h3 class="text-warning-custom" id="kpi-pricing-index">-</h3><small>vs. Competitors</small>
|
| 43 |
+
</div></div>
|
| 44 |
+
</div>
|
| 45 |
+
|
| 46 |
+
<div class="row">
|
| 47 |
+
<div class="col-md-3"><div class="metric-card">
|
| 48 |
+
<h6>CUSTOMER ACQ. COST <button class="info-btn" data-info-icon="💸" data-info-title="Customer Acquisition Cost (CAC)" data-info-section="Financial · Unit Economics KPI" data-info="The total sales and marketing spend divided by the number of new customers acquired in the period. Measures the efficiency of your growth engine." data-info-tips="Telecom CAC benchmark: 3–5× monthly ARPU.|Rising CAC signals market saturation or inefficient channels.|Track CAC by acquisition channel to optimise marketing spend.">ⓘ</button></h6>
|
| 49 |
+
<h3 class="text-warning-custom" id="kpi-cac">-</h3><small>Per new customer</small>
|
| 50 |
+
</div></div>
|
| 51 |
+
<div class="col-md-3"><div class="metric-card">
|
| 52 |
+
<h6>CAC PAYBACK <button class="info-btn" data-info-icon="📅" data-info-title="CAC Payback Period" data-info-section="Financial · Unit Economics KPI" data-info="The number of months it takes to recover the cost of acquiring a new customer through their ARPU contribution. Shorter is better." data-info-tips="Target: <12 months payback for sustainable unit economics.|Above 24 months is concerning — means revenue is slow to recover acquisition spend.|Improve via upselling customers to higher ARPU plans faster.">ⓘ</button></h6>
|
| 53 |
+
<h3 class="text-primary-custom" id="kpi-payback">-</h3><small>Months to ROI</small>
|
| 54 |
+
</div></div>
|
| 55 |
+
<div class="col-md-3"><div class="metric-card">
|
| 56 |
+
<h6>LTV:CAC RATIO <button class="info-btn" data-info-icon="⚖️" data-info-title="LTV to CAC Ratio" data-info-section="Financial · Unit Economics KPI" data-info="The ratio of Customer Lifetime Value to Customer Acquisition Cost. The single most important unit economics metric — shows how much value each customer generates relative to what it cost to acquire them." data-info-tips="Target: ≥3× LTV:CAC ratio.|Below 2× = unprofitable growth — reduce CAC or improve LTV.|Above 5× may indicate under-investing in growth.">ⓘ</button></h6>
|
| 57 |
+
<h3 class="text-success-custom" id="kpi-ltv-cac">-</h3><small>Efficiency metric</small>
|
| 58 |
+
</div></div>
|
| 59 |
+
<div class="col-md-3"><div class="metric-card">
|
| 60 |
+
<h6>NET REVENUE RETENTION <button class="info-btn" data-info-icon="🔄" data-info-title="Net Revenue Retention (NRR)" data-info-section="Financial · Retention KPI" data-info="The percentage of revenue retained from existing customers, accounting for both churn losses and expansion (upsell/cross-sell) gains. Calculated as: 100% + Expansion% − Churn%." data-info-tips="NRR >100% means existing customers generate more revenue over time.|NRR <100% = revenue is shrinking from the existing base.|World-class NRR: >110% — customers expand faster than they churn.">ⓘ</button></h6>
|
| 61 |
+
<h3 class="text-success-custom" id="kpi-nrr">-</h3><small>Expansion - Churn</small>
|
| 62 |
+
</div></div>
|
| 63 |
+
</div>
|
| 64 |
+
|
| 65 |
+
<!-- Charts -->
|
| 66 |
+
<div class="row mt-4">
|
| 67 |
+
<div class="col-md-6">
|
| 68 |
+
<div class="chart-card">
|
| 69 |
+
<h5 class="text-primary-custom mb-3">📈 Revenue Trend & Forecast <button class="info-btn" data-info-icon="📈" data-info-title="Revenue Trend & Forecast" data-info-section="Financial · Revenue Chart" data-info="Monthly revenue line chart from billing data over the last 12 months, used for extrapolating near-term revenue forecasts." data-info-tips="Spot seasonal peaks and plan pricing campaigns accordingly.|Plateau in revenue trend = growth stall — review acquisition and upsell strategy.">ⓘ</button></h5>
|
| 70 |
+
<canvas id="revenueTrendChart"></canvas>
|
| 71 |
+
</div>
|
| 72 |
+
</div>
|
| 73 |
+
<div class="col-md-6">
|
| 74 |
+
<div class="chart-card">
|
| 75 |
+
<h5 class="text-primary-custom mb-3">📊 Revenue by Customer Segment <button class="info-btn" data-info-icon="🥧" data-info-title="Revenue by Customer Segment" data-info-section="Financial · Segment Chart" data-info="A doughnut chart showing the revenue contribution split by plan type — Basic, Standard, Premium, Unlimited, Family, Enterprise." data-info-tips="Concentrate growth efforts on the highest-margin segments.|A large Basic segment is high-risk — these customers are most price-sensitive.|Premium segments are most defensible but also most targeted by competitors.">ⓘ</button></h5>
|
| 76 |
+
<canvas id="revenueSegmentChart"></canvas>
|
| 77 |
+
</div>
|
| 78 |
+
</div>
|
| 79 |
+
</div>
|
| 80 |
+
|
| 81 |
+
<div class="row">
|
| 82 |
+
<div class="col-md-6"><div class="chart-card">
|
| 83 |
+
<h5 class="text-primary-custom mb-3">💸 Service Profitability <button class="info-btn" data-info-icon="💸" data-info-title="Service Plan Profitability" data-info-section="Financial · Margin Chart" data-info="A bar chart showing the estimated profit margin (%) for each plan type after accounting for network costs, support costs, and acquisition expense." data-info-tips="Enterprise plans typically have the highest margins despite deeper discounts.|Basic plans often have margin pressure from support costs.|Use to prioritise which plans to actively promote vs. phase out.">ⓘ</button></h5>
|
| 84 |
+
<canvas id="profitabilityChart"></canvas>
|
| 85 |
+
</div></div>
|
| 86 |
+
<div class="col-md-6"><div class="chart-card">
|
| 87 |
+
<h5 class="text-primary-custom mb-3">🎯 ARPU Distribution <button class="info-btn" data-info-icon="📊" data-info-title="ARPU Distribution" data-info-section="Financial · ARPU Chart" data-info="Histogram showing how customers are distributed across ARPU bands ($0–30, $31–60, $61–90, $91–120, $121+). Reveals the revenue concentration profile." data-info-tips="A large share in $31–60 = mid-market positioning.|$121+ segment generates disproportionate revenue — protect and grow.|Use bimodal distributions to identify upsell gaps between segments.">ⓘ</button></h5>
|
| 88 |
+
<canvas id="arpuDistChart"></canvas>
|
| 89 |
+
</div></div>
|
| 90 |
+
</div>
|
| 91 |
+
<div class="row mt-4">
|
| 92 |
+
<div class="col-md-12"><div class="chart-card">
|
| 93 |
+
<h5 class="text-primary-custom mb-3">🏢 Competitive Pricing Analysis <button class="info-btn" data-info-icon="🏢" data-info-title="Competitive Pricing Analysis" data-info-section="Financial · Competitive Chart" data-info="A grouped bar chart comparing your plan prices against major competitors (Verizon, AT&T, T-Mobile) across Basic, Unlimited, and Family plan tiers." data-info-tips="Being cheaper than all competitors may signal underpricing — review margins.|Price parity with one major competitor is optimal for most segments.|Use premium pricing only if NPS and CSAT justify the quality premium.">ⓘ</button></h5>
|
| 94 |
+
<canvas id="competitivePricingChart"></canvas>
|
| 95 |
+
</div></div>
|
| 96 |
+
</div>
|
| 97 |
+
|
| 98 |
+
{% endblock %}
|
| 99 |
+
|
| 100 |
+
{% block extra_js %}
|
| 101 |
+
<script>
|
| 102 |
+
Chart.defaults.color = '#64748b';
|
| 103 |
+
Chart.defaults.borderColor = '#e2e8f0';
|
| 104 |
+
Chart.defaults.font.family = "'Inter', sans-serif";
|
| 105 |
+
|
| 106 |
+
const COLORS = {
|
| 107 |
+
primary: '#4f46e5',
|
| 108 |
+
success: '#10b981',
|
| 109 |
+
warning: '#f59e0b',
|
| 110 |
+
danger: '#ef4444',
|
| 111 |
+
info: '#3b82f6'
|
| 112 |
+
};
|
| 113 |
+
|
| 114 |
+
async function loadFinancialKPIs() {
|
| 115 |
+
try {
|
| 116 |
+
const response = await fetch('/api/executive/kpis');
|
| 117 |
+
const data = await response.json();
|
| 118 |
+
|
| 119 |
+
document.getElementById('kpi-total-revenue').textContent = `$${data.total_revenue}M`;
|
| 120 |
+
document.getElementById('kpi-arpu').textContent = `$${data.avg_arpu}`;
|
| 121 |
+
|
| 122 |
+
const avgLTV = data.avg_arpu * 36;
|
| 123 |
+
const churnCost = (data.total_revenue * data.churn_rate / 100);
|
| 124 |
+
|
| 125 |
+
document.getElementById('kpi-ltv').textContent = `$${avgLTV.toFixed(0)}`;
|
| 126 |
+
document.getElementById('kpi-churn-cost').textContent = `$${churnCost.toFixed(2)}M`;
|
| 127 |
+
document.getElementById('kpi-mrr-growth').textContent = '+5.2%';
|
| 128 |
+
document.getElementById('kpi-revenue-per-tower').textContent = `$${(data.total_revenue * 1e6 / 1000).toFixed(1)}K`;
|
| 129 |
+
document.getElementById('kpi-high-value').textContent = Math.round(data.total_customers * 0.2).toLocaleString();
|
| 130 |
+
document.getElementById('kpi-pricing-index').textContent = '102';
|
| 131 |
+
|
| 132 |
+
// Load CAC metrics
|
| 133 |
+
const cacResp = await fetch('/api/financial/cac-metrics');
|
| 134 |
+
const cac = await cacResp.json();
|
| 135 |
+
document.getElementById('kpi-cac').textContent = `$${cac.cac}`;
|
| 136 |
+
document.getElementById('kpi-payback').textContent = `${cac.payback_months} mo`;
|
| 137 |
+
document.getElementById('kpi-ltv-cac').textContent = `${cac.ltv_cac_ratio}x`;
|
| 138 |
+
document.getElementById('kpi-nrr').textContent = `${cac.nrr}%`;
|
| 139 |
+
} catch (error) {
|
| 140 |
+
console.error('Error loading financial KPIs:', error);
|
| 141 |
+
}
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
async function loadRevenueTrendChart() {
|
| 145 |
+
try {
|
| 146 |
+
const response = await fetch('/api/financial/revenue-data');
|
| 147 |
+
const data = await response.json();
|
| 148 |
+
|
| 149 |
+
const ctx = document.getElementById('revenueTrendChart').getContext('2d');
|
| 150 |
+
new Chart(ctx, {
|
| 151 |
+
type: 'line',
|
| 152 |
+
data: {
|
| 153 |
+
labels: data.months,
|
| 154 |
+
datasets: [{
|
| 155 |
+
label: 'Actual Revenue ($M)',
|
| 156 |
+
data: data.revenue,
|
| 157 |
+
borderColor: COLORS.success,
|
| 158 |
+
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
| 159 |
+
fill: true,
|
| 160 |
+
tension: 0.4,
|
| 161 |
+
borderWidth: 3
|
| 162 |
+
}]
|
| 163 |
+
},
|
| 164 |
+
options: {
|
| 165 |
+
responsive: true,
|
| 166 |
+
plugins: { legend: { display: true } },
|
| 167 |
+
scales: {
|
| 168 |
+
y: { beginAtZero: false, grid: { color: '#f1f5f9' } }
|
| 169 |
+
}
|
| 170 |
+
}
|
| 171 |
+
});
|
| 172 |
+
} catch (error) {
|
| 173 |
+
console.error('Error loading revenue trend:', error);
|
| 174 |
+
}
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
async function loadRevenueSegmentChart() {
|
| 178 |
+
try {
|
| 179 |
+
const response = await fetch('/api/financial/revenue-data');
|
| 180 |
+
const data = await response.json();
|
| 181 |
+
|
| 182 |
+
const ctx = document.getElementById('revenueSegmentChart').getContext('2d');
|
| 183 |
+
new Chart(ctx, {
|
| 184 |
+
type: 'doughnut',
|
| 185 |
+
data: {
|
| 186 |
+
labels: data.plan_labels,
|
| 187 |
+
datasets: [{
|
| 188 |
+
data: data.plan_values,
|
| 189 |
+
backgroundColor: [COLORS.success, COLORS.primary, COLORS.info, COLORS.warning, COLORS.danger, '#8b5cf6'],
|
| 190 |
+
borderWidth: 4,
|
| 191 |
+
borderColor: '#ffffff'
|
| 192 |
+
}]
|
| 193 |
+
},
|
| 194 |
+
options: {
|
| 195 |
+
responsive: true,
|
| 196 |
+
plugins: { legend: { position: 'right' } }
|
| 197 |
+
}
|
| 198 |
+
});
|
| 199 |
+
} catch (error) {
|
| 200 |
+
console.error('Error loading revenue segment:', error);
|
| 201 |
+
}
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
function loadProfitabilityChart() {
|
| 205 |
+
const ctx = document.getElementById('profitabilityChart').getContext('2d');
|
| 206 |
+
new Chart(ctx, {
|
| 207 |
+
type: 'bar',
|
| 208 |
+
data: {
|
| 209 |
+
labels: ['Basic', 'Standard', 'Premium', 'Unlimited', 'Family', 'Enterprise'],
|
| 210 |
+
datasets: [{
|
| 211 |
+
label: 'Profitability (%)',
|
| 212 |
+
data: [18, 25, 35, 28, 32, 42],
|
| 213 |
+
backgroundColor: COLORS.success,
|
| 214 |
+
borderRadius: 8
|
| 215 |
+
}]
|
| 216 |
+
},
|
| 217 |
+
options: {
|
| 218 |
+
responsive: true,
|
| 219 |
+
plugins: { legend: { display: false } },
|
| 220 |
+
scales: { y: { beginAtZero: true, grid: { color: '#f1f5f9' } } }
|
| 221 |
+
}
|
| 222 |
+
});
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
async function loadARPUDistChart() {
|
| 226 |
+
try {
|
| 227 |
+
const response = await fetch('/api/financial/revenue-data');
|
| 228 |
+
const data = await response.json();
|
| 229 |
+
|
| 230 |
+
const ctx = document.getElementById('arpuDistChart').getContext('2d');
|
| 231 |
+
new Chart(ctx, {
|
| 232 |
+
type: 'bar',
|
| 233 |
+
data: {
|
| 234 |
+
labels: data.arpu_labels,
|
| 235 |
+
datasets: [{
|
| 236 |
+
label: 'Customers',
|
| 237 |
+
data: data.arpu_values,
|
| 238 |
+
backgroundColor: COLORS.primary,
|
| 239 |
+
borderRadius: 8
|
| 240 |
+
}]
|
| 241 |
+
},
|
| 242 |
+
options: {
|
| 243 |
+
responsive: true,
|
| 244 |
+
plugins: { legend: { display: false } },
|
| 245 |
+
scales: { y: { beginAtZero: true, grid: { color: '#f1f5f9' } } }
|
| 246 |
+
}
|
| 247 |
+
});
|
| 248 |
+
} catch (error) {
|
| 249 |
+
console.error('Error loading ARPU distribution:', error);
|
| 250 |
+
}
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
function loadCompetitivePricingChart() {
|
| 254 |
+
const ctx = document.getElementById('competitivePricingChart').getContext('2d');
|
| 255 |
+
new Chart(ctx, {
|
| 256 |
+
type: 'bar',
|
| 257 |
+
data: {
|
| 258 |
+
labels: ['Our Company', 'Verizon', 'AT&T', 'T-Mobile'],
|
| 259 |
+
datasets: [
|
| 260 |
+
{
|
| 261 |
+
label: 'Basic Plan',
|
| 262 |
+
data: [45, 50, 48, 42],
|
| 263 |
+
backgroundColor: COLORS.primary
|
| 264 |
+
},
|
| 265 |
+
{
|
| 266 |
+
label: 'Unlimited Plan',
|
| 267 |
+
data: [80, 85, 83, 75],
|
| 268 |
+
backgroundColor: COLORS.success
|
| 269 |
+
},
|
| 270 |
+
{
|
| 271 |
+
label: 'Family Plan',
|
| 272 |
+
data: [120, 130, 125, 115],
|
| 273 |
+
backgroundColor: COLORS.warning
|
| 274 |
+
}
|
| 275 |
+
]
|
| 276 |
+
},
|
| 277 |
+
options: {
|
| 278 |
+
responsive: true,
|
| 279 |
+
plugins: {
|
| 280 |
+
legend: { display: true, position: 'top' }
|
| 281 |
+
},
|
| 282 |
+
scales: {
|
| 283 |
+
y: {
|
| 284 |
+
beginAtZero: true,
|
| 285 |
+
grid: { color: '#f1f5f9' },
|
| 286 |
+
title: {
|
| 287 |
+
display: true,
|
| 288 |
+
text: 'Price ($)'
|
| 289 |
+
}
|
| 290 |
+
}
|
| 291 |
+
}
|
| 292 |
+
}
|
| 293 |
+
});
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 297 |
+
loadFinancialKPIs();
|
| 298 |
+
loadRevenueTrendChart();
|
| 299 |
+
loadRevenueSegmentChart();
|
| 300 |
+
loadProfitabilityChart();
|
| 301 |
+
loadARPUDistChart();
|
| 302 |
+
loadCompetitivePricingChart();
|
| 303 |
+
});
|
| 304 |
+
</script>
|
| 305 |
+
{% endblock %}
|
templates/forecasting.html
ADDED
|
@@ -0,0 +1,840 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}Time Series Forecasting - TelecomIQ{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block extra_css %}
|
| 6 |
+
<style>
|
| 7 |
+
/* ===== Tab Buttons ===== */
|
| 8 |
+
.forecast-tab-btn {
|
| 9 |
+
background: var(--bg-card);
|
| 10 |
+
border: 2px solid var(--border-color);
|
| 11 |
+
border-radius: 12px;
|
| 12 |
+
padding: 1rem 1.25rem;
|
| 13 |
+
cursor: pointer;
|
| 14 |
+
transition: all 0.3s ease;
|
| 15 |
+
text-align: center;
|
| 16 |
+
font-weight: 600;
|
| 17 |
+
color: var(--text-secondary);
|
| 18 |
+
height: 100%;
|
| 19 |
+
}
|
| 20 |
+
.forecast-tab-btn:hover {
|
| 21 |
+
border-color: var(--primary-light);
|
| 22 |
+
color: var(--primary-color);
|
| 23 |
+
transform: translateY(-2px);
|
| 24 |
+
box-shadow: var(--shadow-md);
|
| 25 |
+
}
|
| 26 |
+
.forecast-tab-btn.active {
|
| 27 |
+
border-color: var(--primary-color);
|
| 28 |
+
background: linear-gradient(135deg, rgba(79,70,229,0.08), rgba(99,102,241,0.05));
|
| 29 |
+
color: var(--primary-color);
|
| 30 |
+
box-shadow: var(--shadow-md);
|
| 31 |
+
}
|
| 32 |
+
.forecast-tab-btn .tab-icon { font-size: 1.75rem; display: block; margin-bottom: 0.5rem; }
|
| 33 |
+
.forecast-tab-btn .tab-label { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.5px; }
|
| 34 |
+
|
| 35 |
+
/* ===== Sections ===== */
|
| 36 |
+
.forecast-section { display: none; }
|
| 37 |
+
.forecast-section.active { display: block; }
|
| 38 |
+
|
| 39 |
+
/* ===== Chart wrappers with FIXED HEIGHT so charts render properly ===== */
|
| 40 |
+
.chart-wrapper-lg { position: relative; width: 100%; height: 380px; }
|
| 41 |
+
.chart-wrapper-md { position: relative; width: 100%; height: 320px; }
|
| 42 |
+
|
| 43 |
+
/* ===== Badges ===== */
|
| 44 |
+
.ci-badge {
|
| 45 |
+
display: inline-block; font-size: 0.7rem; padding: 2px 8px;
|
| 46 |
+
border-radius: 12px; background: rgba(59,130,246,0.1);
|
| 47 |
+
color: var(--info-color); font-weight: 600; margin-left: 0.5rem;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
/* ===== Horizon select ===== */
|
| 51 |
+
.forecast-horizon-select { max-width: 160px; }
|
| 52 |
+
|
| 53 |
+
/* ===== Insight cards ===== */
|
| 54 |
+
.insight-card {
|
| 55 |
+
background: linear-gradient(135deg, rgba(79,70,229,0.04), rgba(99,102,241,0.02));
|
| 56 |
+
border: 1px solid rgba(79,70,229,0.15);
|
| 57 |
+
border-radius: 12px;
|
| 58 |
+
padding: 1rem 1.25rem;
|
| 59 |
+
margin-bottom: 0.75rem;
|
| 60 |
+
height: 100%;
|
| 61 |
+
}
|
| 62 |
+
.insight-card .insight-title {
|
| 63 |
+
font-weight: 700; font-size: 0.8rem;
|
| 64 |
+
text-transform: uppercase; color: var(--primary-color);
|
| 65 |
+
margin-bottom: 0.35rem;
|
| 66 |
+
}
|
| 67 |
+
.insight-card .insight-text {
|
| 68 |
+
font-size: 0.88rem; color: var(--text-secondary); line-height: 1.5;
|
| 69 |
+
}
|
| 70 |
+
</style>
|
| 71 |
+
{% endblock %}
|
| 72 |
+
|
| 73 |
+
{% block content %}
|
| 74 |
+
<div class="d-flex justify-content-between align-items-center mb-4">
|
| 75 |
+
<h2 class="text-primary-custom mb-0">Time Series Forecasting</h2>
|
| 76 |
+
<div class="d-flex align-items-center gap-3">
|
| 77 |
+
<label class="form-label mb-0 fw-semibold small text-muted">FORECAST HORIZON</label>
|
| 78 |
+
<select id="forecastHorizon" class="form-select form-select-sm forecast-horizon-select" onchange="reloadActiveSection()">
|
| 79 |
+
<option value="6">6 Months</option>
|
| 80 |
+
<option value="12" selected>12 Months</option>
|
| 81 |
+
<option value="18">18 Months</option>
|
| 82 |
+
<option value="24">24 Months</option>
|
| 83 |
+
</select>
|
| 84 |
+
</div>
|
| 85 |
+
</div>
|
| 86 |
+
|
| 87 |
+
<!-- Summary KPI Cards -->
|
| 88 |
+
<div class="row mb-4 g-3">
|
| 89 |
+
<div class="col-lg-3 col-md-6">
|
| 90 |
+
<div class="metric-card">
|
| 91 |
+
<h6>DATA USAGE GROWTH</h6>
|
| 92 |
+
<h3 class="text-primary-custom" id="kpi-usage-growth">-</h3>
|
| 93 |
+
<small>Next 12-month forecast</small>
|
| 94 |
+
</div>
|
| 95 |
+
</div>
|
| 96 |
+
<div class="col-lg-3 col-md-6">
|
| 97 |
+
<div class="metric-card">
|
| 98 |
+
<h6>5G ADOPTION FORECAST</h6>
|
| 99 |
+
<h3 class="text-success-custom" id="kpi-5g-forecast">-</h3>
|
| 100 |
+
<small>Projected end of period</small>
|
| 101 |
+
</div>
|
| 102 |
+
</div>
|
| 103 |
+
<div class="col-lg-3 col-md-6">
|
| 104 |
+
<div class="metric-card">
|
| 105 |
+
<h6>MARKET SHARE TREND</h6>
|
| 106 |
+
<h3 class="text-info-custom" id="kpi-market-share">-</h3>
|
| 107 |
+
<small>Projected position</small>
|
| 108 |
+
</div>
|
| 109 |
+
</div>
|
| 110 |
+
<div class="col-lg-3 col-md-6">
|
| 111 |
+
<div class="metric-card">
|
| 112 |
+
<h6>RECESSION RISK</h6>
|
| 113 |
+
<h3 class="text-warning-custom" id="kpi-recession-risk">-</h3>
|
| 114 |
+
<small>Economic outlook</small>
|
| 115 |
+
</div>
|
| 116 |
+
</div>
|
| 117 |
+
</div>
|
| 118 |
+
|
| 119 |
+
<!-- Tab Navigation -->
|
| 120 |
+
<div class="row mb-4 g-3">
|
| 121 |
+
<div class="col-lg-3 col-6">
|
| 122 |
+
<div class="forecast-tab-btn active" onclick="switchTab('seasonal')" id="tab-seasonal">
|
| 123 |
+
<span class="tab-icon">📊</span>
|
| 124 |
+
<span class="tab-label">Seasonal Usage</span>
|
| 125 |
+
</div>
|
| 126 |
+
</div>
|
| 127 |
+
<div class="col-lg-3 col-6">
|
| 128 |
+
<div class="forecast-tab-btn" onclick="switchTab('tech')" id="tab-tech">
|
| 129 |
+
<span class="tab-icon">📡</span>
|
| 130 |
+
<span class="tab-label">Tech Adoption</span>
|
| 131 |
+
</div>
|
| 132 |
+
</div>
|
| 133 |
+
<div class="col-lg-3 col-6">
|
| 134 |
+
<div class="forecast-tab-btn" onclick="switchTab('competitive')" id="tab-competitive">
|
| 135 |
+
<span class="tab-icon">⚔️</span>
|
| 136 |
+
<span class="tab-label">Market Dynamics</span>
|
| 137 |
+
</div>
|
| 138 |
+
</div>
|
| 139 |
+
<div class="col-lg-3 col-6">
|
| 140 |
+
<div class="forecast-tab-btn" onclick="switchTab('economic')" id="tab-economic">
|
| 141 |
+
<span class="tab-icon">💹</span>
|
| 142 |
+
<span class="tab-label">Economic Impact</span>
|
| 143 |
+
</div>
|
| 144 |
+
</div>
|
| 145 |
+
</div>
|
| 146 |
+
|
| 147 |
+
<!-- ================================================================== -->
|
| 148 |
+
<!-- SECTION 1: Seasonal Usage Patterns -->
|
| 149 |
+
<!-- ================================================================== -->
|
| 150 |
+
<div class="forecast-section active" id="section-seasonal">
|
| 151 |
+
<div class="row g-3 mb-3">
|
| 152 |
+
<div class="col-lg-7">
|
| 153 |
+
<div class="chart-card">
|
| 154 |
+
<h5 class="text-primary-custom">Data Usage Forecast <span class="ci-badge">95% CI</span></h5>
|
| 155 |
+
<div class="chart-wrapper-lg"><canvas id="seasonalUsageChart"></canvas></div>
|
| 156 |
+
</div>
|
| 157 |
+
</div>
|
| 158 |
+
<div class="col-lg-5">
|
| 159 |
+
<div class="chart-card">
|
| 160 |
+
<h5 class="text-primary-custom">Holiday & Event Impact</h5>
|
| 161 |
+
<div class="chart-wrapper-lg"><canvas id="holidayImpactChart"></canvas></div>
|
| 162 |
+
</div>
|
| 163 |
+
</div>
|
| 164 |
+
</div>
|
| 165 |
+
<div class="row g-3 mb-3">
|
| 166 |
+
<div class="col-lg-6">
|
| 167 |
+
<div class="chart-card">
|
| 168 |
+
<h5 class="text-primary-custom">Network Load Forecast <span class="ci-badge">95% CI</span></h5>
|
| 169 |
+
<div class="chart-wrapper-md"><canvas id="networkLoadChart"></canvas></div>
|
| 170 |
+
</div>
|
| 171 |
+
</div>
|
| 172 |
+
<div class="col-lg-6">
|
| 173 |
+
<div class="chart-card">
|
| 174 |
+
<h5 class="text-primary-custom">Peak Concurrent Users</h5>
|
| 175 |
+
<div class="chart-wrapper-md"><canvas id="peakUsersChart"></canvas></div>
|
| 176 |
+
</div>
|
| 177 |
+
</div>
|
| 178 |
+
</div>
|
| 179 |
+
<div class="row g-3">
|
| 180 |
+
<div class="col-12">
|
| 181 |
+
<div class="chart-card">
|
| 182 |
+
<h5 class="text-primary-custom">Key Insights - Seasonal Patterns</h5>
|
| 183 |
+
<div class="row g-3">
|
| 184 |
+
<div class="col-lg-4"><div class="insight-card"><div class="insight-title">Peak Demand Period</div><div class="insight-text" id="insight-peak-demand">Loading...</div></div></div>
|
| 185 |
+
<div class="col-lg-4"><div class="insight-card"><div class="insight-title">Capacity Planning</div><div class="insight-text" id="insight-capacity">Loading...</div></div></div>
|
| 186 |
+
<div class="col-lg-4"><div class="insight-card"><div class="insight-title">Voice Trend</div><div class="insight-text" id="insight-voice">Loading...</div></div></div>
|
| 187 |
+
</div>
|
| 188 |
+
</div>
|
| 189 |
+
</div>
|
| 190 |
+
</div>
|
| 191 |
+
</div>
|
| 192 |
+
|
| 193 |
+
<!-- ================================================================== -->
|
| 194 |
+
<!-- SECTION 2: Technology Adoption Curves -->
|
| 195 |
+
<!-- ================================================================== -->
|
| 196 |
+
<div class="forecast-section" id="section-tech">
|
| 197 |
+
<div class="row g-3 mb-3">
|
| 198 |
+
<div class="col-lg-7">
|
| 199 |
+
<div class="chart-card">
|
| 200 |
+
<h5 class="text-primary-custom">5G Adoption S-Curve <span class="ci-badge">95% CI</span></h5>
|
| 201 |
+
<div class="chart-wrapper-lg"><canvas id="techAdoptionChart"></canvas></div>
|
| 202 |
+
</div>
|
| 203 |
+
</div>
|
| 204 |
+
<div class="col-lg-5">
|
| 205 |
+
<div class="chart-card">
|
| 206 |
+
<h5 class="text-primary-custom">Technology Mix Forecast</h5>
|
| 207 |
+
<div class="chart-wrapper-lg"><canvas id="techMixChart"></canvas></div>
|
| 208 |
+
</div>
|
| 209 |
+
</div>
|
| 210 |
+
</div>
|
| 211 |
+
<div class="row g-3 mb-3">
|
| 212 |
+
<div class="col-lg-6">
|
| 213 |
+
<div class="chart-card">
|
| 214 |
+
<h5 class="text-primary-custom">5G Tower Deployment</h5>
|
| 215 |
+
<div class="chart-wrapper-md"><canvas id="towerDeployChart"></canvas></div>
|
| 216 |
+
</div>
|
| 217 |
+
</div>
|
| 218 |
+
<div class="col-lg-6">
|
| 219 |
+
<div class="chart-card">
|
| 220 |
+
<h5 class="text-primary-custom">5G Revenue Premium & Speed</h5>
|
| 221 |
+
<div class="chart-wrapper-md"><canvas id="revenuePremiumChart"></canvas></div>
|
| 222 |
+
</div>
|
| 223 |
+
</div>
|
| 224 |
+
</div>
|
| 225 |
+
<div class="row g-3">
|
| 226 |
+
<div class="col-12">
|
| 227 |
+
<div class="chart-card">
|
| 228 |
+
<h5 class="text-primary-custom">Key Insights - Technology Adoption</h5>
|
| 229 |
+
<div class="row g-3">
|
| 230 |
+
<div class="col-lg-4"><div class="insight-card"><div class="insight-title">Migration Velocity</div><div class="insight-text" id="insight-migration">Loading...</div></div></div>
|
| 231 |
+
<div class="col-lg-4"><div class="insight-card"><div class="insight-title">Infrastructure ROI</div><div class="insight-text" id="insight-roi">Loading...</div></div></div>
|
| 232 |
+
<div class="col-lg-4"><div class="insight-card"><div class="insight-title">3G Sunset Timeline</div><div class="insight-text" id="insight-sunset">Loading...</div></div></div>
|
| 233 |
+
</div>
|
| 234 |
+
</div>
|
| 235 |
+
</div>
|
| 236 |
+
</div>
|
| 237 |
+
</div>
|
| 238 |
+
|
| 239 |
+
<!-- ================================================================== -->
|
| 240 |
+
<!-- SECTION 3: Competitive Market Dynamics -->
|
| 241 |
+
<!-- ================================================================== -->
|
| 242 |
+
<div class="forecast-section" id="section-competitive">
|
| 243 |
+
<div class="row g-3 mb-3">
|
| 244 |
+
<div class="col-lg-7">
|
| 245 |
+
<div class="chart-card">
|
| 246 |
+
<h5 class="text-primary-custom">Market Share Forecast</h5>
|
| 247 |
+
<div class="chart-wrapper-lg"><canvas id="marketShareChart"></canvas></div>
|
| 248 |
+
</div>
|
| 249 |
+
</div>
|
| 250 |
+
<div class="col-lg-5">
|
| 251 |
+
<div class="chart-card">
|
| 252 |
+
<h5 class="text-primary-custom">Pricing War Risk Index</h5>
|
| 253 |
+
<div class="chart-wrapper-lg"><canvas id="pricingWarChart"></canvas></div>
|
| 254 |
+
</div>
|
| 255 |
+
</div>
|
| 256 |
+
</div>
|
| 257 |
+
<div class="row g-3 mb-3">
|
| 258 |
+
<div class="col-lg-6">
|
| 259 |
+
<div class="chart-card">
|
| 260 |
+
<h5 class="text-primary-custom">Pricing Dynamics</h5>
|
| 261 |
+
<div class="chart-wrapper-md"><canvas id="pricingChart"></canvas></div>
|
| 262 |
+
</div>
|
| 263 |
+
</div>
|
| 264 |
+
<div class="col-lg-6">
|
| 265 |
+
<div class="chart-card">
|
| 266 |
+
<h5 class="text-primary-custom">Net Subscriber Additions <span class="ci-badge">95% CI</span></h5>
|
| 267 |
+
<div class="chart-wrapper-md"><canvas id="netAddsChart"></canvas></div>
|
| 268 |
+
</div>
|
| 269 |
+
</div>
|
| 270 |
+
</div>
|
| 271 |
+
<div class="row g-3">
|
| 272 |
+
<div class="col-12">
|
| 273 |
+
<div class="chart-card">
|
| 274 |
+
<h5 class="text-primary-custom">Key Insights - Market Dynamics</h5>
|
| 275 |
+
<div class="row g-3">
|
| 276 |
+
<div class="col-lg-4"><div class="insight-card"><div class="insight-title">Competitive Position</div><div class="insight-text" id="insight-position">Loading...</div></div></div>
|
| 277 |
+
<div class="col-lg-4"><div class="insight-card"><div class="insight-title">Pricing Strategy</div><div class="insight-text" id="insight-pricing">Loading...</div></div></div>
|
| 278 |
+
<div class="col-lg-4"><div class="insight-card"><div class="insight-title">Growth Outlook</div><div class="insight-text" id="insight-growth">Loading...</div></div></div>
|
| 279 |
+
</div>
|
| 280 |
+
</div>
|
| 281 |
+
</div>
|
| 282 |
+
</div>
|
| 283 |
+
</div>
|
| 284 |
+
|
| 285 |
+
<!-- ================================================================== -->
|
| 286 |
+
<!-- SECTION 4: Economic Impact Forecasting -->
|
| 287 |
+
<!-- ================================================================== -->
|
| 288 |
+
<div class="forecast-section" id="section-economic">
|
| 289 |
+
<div class="row g-3 mb-3">
|
| 290 |
+
<div class="col-lg-7">
|
| 291 |
+
<div class="chart-card">
|
| 292 |
+
<h5 class="text-primary-custom">GDP Growth & Consumer Confidence <span class="ci-badge">95% CI</span></h5>
|
| 293 |
+
<div class="chart-wrapper-lg"><canvas id="economicOverviewChart"></canvas></div>
|
| 294 |
+
</div>
|
| 295 |
+
</div>
|
| 296 |
+
<div class="col-lg-5">
|
| 297 |
+
<div class="chart-card">
|
| 298 |
+
<h5 class="text-primary-custom">Recession Probability</h5>
|
| 299 |
+
<div class="chart-wrapper-lg"><canvas id="recessionProbChart"></canvas></div>
|
| 300 |
+
</div>
|
| 301 |
+
</div>
|
| 302 |
+
</div>
|
| 303 |
+
<div class="row g-3 mb-3">
|
| 304 |
+
<div class="col-lg-6">
|
| 305 |
+
<div class="chart-card">
|
| 306 |
+
<h5 class="text-primary-custom">Revenue at Risk ($M) <span class="ci-badge">95% CI</span></h5>
|
| 307 |
+
<div class="chart-wrapper-md"><canvas id="revenueRiskChart"></canvas></div>
|
| 308 |
+
</div>
|
| 309 |
+
</div>
|
| 310 |
+
<div class="col-lg-6">
|
| 311 |
+
<div class="chart-card">
|
| 312 |
+
<h5 class="text-primary-custom">Customer Behavior Under Economic Stress</h5>
|
| 313 |
+
<div class="chart-wrapper-md"><canvas id="behaviorStressChart"></canvas></div>
|
| 314 |
+
</div>
|
| 315 |
+
</div>
|
| 316 |
+
</div>
|
| 317 |
+
<div class="row g-3">
|
| 318 |
+
<div class="col-12">
|
| 319 |
+
<div class="chart-card">
|
| 320 |
+
<h5 class="text-primary-custom">Key Insights - Economic Impact</h5>
|
| 321 |
+
<div class="row g-3">
|
| 322 |
+
<div class="col-lg-4"><div class="insight-card"><div class="insight-title">Economic Outlook</div><div class="insight-text" id="insight-economy">Loading...</div></div></div>
|
| 323 |
+
<div class="col-lg-4"><div class="insight-card"><div class="insight-title">Revenue Protection</div><div class="insight-text" id="insight-revenue">Loading...</div></div></div>
|
| 324 |
+
<div class="col-lg-4"><div class="insight-card"><div class="insight-title">Customer Sentiment</div><div class="insight-text" id="insight-sentiment">Loading...</div></div></div>
|
| 325 |
+
</div>
|
| 326 |
+
</div>
|
| 327 |
+
</div>
|
| 328 |
+
</div>
|
| 329 |
+
</div>
|
| 330 |
+
|
| 331 |
+
{% endblock %}
|
| 332 |
+
|
| 333 |
+
{% block extra_js %}
|
| 334 |
+
<script>
|
| 335 |
+
// ======================================================================
|
| 336 |
+
// GLOBALS & HELPERS
|
| 337 |
+
// ======================================================================
|
| 338 |
+
Chart.defaults.color = '#64748b';
|
| 339 |
+
Chart.defaults.borderColor = '#e2e8f0';
|
| 340 |
+
Chart.defaults.font.family = "'Inter', sans-serif";
|
| 341 |
+
Chart.defaults.font.size = 12;
|
| 342 |
+
|
| 343 |
+
const C = {
|
| 344 |
+
primary: '#4f46e5', primaryLight: '#6366f1', secondary: '#ec4899',
|
| 345 |
+
success: '#10b981', warning: '#f59e0b', danger: '#ef4444',
|
| 346 |
+
info: '#3b82f6', purple: '#8b5cf6', teal: '#14b8a6', orange: '#f97316'
|
| 347 |
+
};
|
| 348 |
+
|
| 349 |
+
let charts = {};
|
| 350 |
+
let activeTab = 'seasonal';
|
| 351 |
+
|
| 352 |
+
function getHorizon() { return document.getElementById('forecastHorizon').value; }
|
| 353 |
+
|
| 354 |
+
function switchTab(tab) {
|
| 355 |
+
document.querySelectorAll('.forecast-tab-btn').forEach(b => b.classList.remove('active'));
|
| 356 |
+
document.querySelectorAll('.forecast-section').forEach(s => s.classList.remove('active'));
|
| 357 |
+
document.getElementById('tab-' + tab).classList.add('active');
|
| 358 |
+
document.getElementById('section-' + tab).classList.add('active');
|
| 359 |
+
activeTab = tab;
|
| 360 |
+
loadTabData(tab);
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
function reloadActiveSection() { loadTabData(activeTab); }
|
| 364 |
+
|
| 365 |
+
function destroyChart(id) { if (charts[id]) { charts[id].destroy(); delete charts[id]; } }
|
| 366 |
+
|
| 367 |
+
/* Format "2025-03" -> "Mar '25" for cleaner x-axis */
|
| 368 |
+
function shortLabel(ym) {
|
| 369 |
+
if (!ym || !ym.includes('-')) return ym;
|
| 370 |
+
const m = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
| 371 |
+
const p = ym.split('-');
|
| 372 |
+
return m[parseInt(p[1])-1] + " '" + p[0].slice(2);
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
/* Reusable x-axis config: rotated, auto-skipped, short labels */
|
| 376 |
+
function xAxisCfg(labels) {
|
| 377 |
+
return {
|
| 378 |
+
grid: { display: false },
|
| 379 |
+
ticks: {
|
| 380 |
+
maxRotation: 45, minRotation: 25, autoSkip: true,
|
| 381 |
+
maxTicksLimit: 16, font: { size: 11 },
|
| 382 |
+
callback: function(val, idx) { return shortLabel(labels[idx] || ''); }
|
| 383 |
+
}
|
| 384 |
+
};
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
/* Shared polished tooltip */
|
| 388 |
+
const tooltipCfg = {
|
| 389 |
+
backgroundColor: '#ffffff', titleColor: '#1e293b', bodyColor: '#475569',
|
| 390 |
+
borderColor: '#e2e8f0', borderWidth: 1, padding: 14, cornerRadius: 10,
|
| 391 |
+
titleFont: { weight: '600', size: 13 }, bodyFont: { size: 12 },
|
| 392 |
+
displayColors: true, boxPadding: 4,
|
| 393 |
+
callbacks: { title: (items) => { if (items[0]) return shortLabel(items[0].label || ''); } }
|
| 394 |
+
};
|
| 395 |
+
|
| 396 |
+
/* Filter CI datasets out of legend */
|
| 397 |
+
function legendFilter(item) { return !item.text.includes('CI'); }
|
| 398 |
+
|
| 399 |
+
// ======================================================================
|
| 400 |
+
// LOAD SUMMARY KPIs
|
| 401 |
+
// ======================================================================
|
| 402 |
+
async function loadSummary() {
|
| 403 |
+
try {
|
| 404 |
+
const r = await fetch('/api/forecasting/summary');
|
| 405 |
+
const d = await r.json();
|
| 406 |
+
if (d.error) return;
|
| 407 |
+
document.getElementById('kpi-usage-growth').textContent = (d.data_usage_growth > 0 ? '+' : '') + d.data_usage_growth + '%';
|
| 408 |
+
document.getElementById('kpi-5g-forecast').textContent = d.five_g_forecast_end.toFixed(1) + '%';
|
| 409 |
+
document.getElementById('kpi-market-share').textContent = d.market_share_forecast.toFixed(1) + '%';
|
| 410 |
+
const rp = d.recession_probability;
|
| 411 |
+
const rpEl = document.getElementById('kpi-recession-risk');
|
| 412 |
+
rpEl.textContent = rp.toFixed(0) + '%';
|
| 413 |
+
rpEl.className = rp > 50 ? 'text-danger-custom' : rp > 25 ? 'text-warning-custom' : 'text-success-custom';
|
| 414 |
+
} catch(e) { console.error('Summary error', e); }
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
// ======================================================================
|
| 418 |
+
// TAB LOADER
|
| 419 |
+
// ======================================================================
|
| 420 |
+
function loadTabData(tab) {
|
| 421 |
+
const m = getHorizon();
|
| 422 |
+
if (tab === 'seasonal') loadSeasonal(m);
|
| 423 |
+
else if (tab === 'tech') loadTechAdoption(m);
|
| 424 |
+
else if (tab === 'competitive') loadCompetitive(m);
|
| 425 |
+
else if (tab === 'economic') loadEconomic(m);
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
// ======================================================================
|
| 429 |
+
// 1. SEASONAL USAGE
|
| 430 |
+
// ======================================================================
|
| 431 |
+
async function loadSeasonal(months) {
|
| 432 |
+
try {
|
| 433 |
+
const r = await fetch(`/api/forecasting/seasonal?months=${months}`);
|
| 434 |
+
const d = await r.json(); if (d.error) return;
|
| 435 |
+
const hist = d.historical;
|
| 436 |
+
const allDates = [...hist.dates, ...d.dates];
|
| 437 |
+
|
| 438 |
+
// --- Data Usage ---
|
| 439 |
+
destroyChart('seasonalUsage');
|
| 440 |
+
const hU = [...hist.data_usage, ...Array(d.dates.length).fill(null)];
|
| 441 |
+
const fU = [...Array(hist.dates.length-1).fill(null), hist.data_usage.at(-1), ...d.data_usage.forecast];
|
| 442 |
+
const uU = [...Array(hist.dates.length-1).fill(null), hist.data_usage.at(-1), ...d.data_usage.upper];
|
| 443 |
+
const lU = [...Array(hist.dates.length-1).fill(null), hist.data_usage.at(-1), ...d.data_usage.lower];
|
| 444 |
+
|
| 445 |
+
charts['seasonalUsage'] = new Chart(document.getElementById('seasonalUsageChart'), {
|
| 446 |
+
type: 'line',
|
| 447 |
+
data: { labels: allDates, datasets: [
|
| 448 |
+
{ label: 'Historical', data: hU, borderColor: C.primary, backgroundColor: C.primary+'18', fill: true, tension: 0.4, borderWidth: 2.5, pointRadius: 4, pointHoverRadius: 6 },
|
| 449 |
+
{ label: 'Forecast', data: fU, borderColor: C.success, borderDash: [6,3], backgroundColor: C.success+'15', fill: true, tension: 0.4, borderWidth: 2.5, pointRadius: 4, pointHoverRadius: 6 },
|
| 450 |
+
{ label: 'Upper CI', data: uU, borderColor: 'transparent', backgroundColor: C.success+'0D', fill: '+1', pointRadius: 0 },
|
| 451 |
+
{ label: 'Lower CI', data: lU, borderColor: 'transparent', backgroundColor: 'transparent', fill: false, pointRadius: 0 },
|
| 452 |
+
]},
|
| 453 |
+
options: {
|
| 454 |
+
responsive: true, maintainAspectRatio: false,
|
| 455 |
+
plugins: { legend: { position: 'top', labels: { filter: legendFilter, padding: 16, usePointStyle: true, pointStyle: 'circle' } }, tooltip: tooltipCfg },
|
| 456 |
+
scales: { y: { title: { display: true, text: 'Avg Data Usage (GB)', font: { size: 12, weight: '600' } }, grid: { color: '#f1f5f9' } }, x: xAxisCfg(allDates) }
|
| 457 |
+
}
|
| 458 |
+
});
|
| 459 |
+
|
| 460 |
+
// --- Holiday Impact ---
|
| 461 |
+
destroyChart('holidayImpact');
|
| 462 |
+
charts['holidayImpact'] = new Chart(document.getElementById('holidayImpactChart'), {
|
| 463 |
+
type: 'bar',
|
| 464 |
+
data: { labels: d.dates, datasets: [{
|
| 465 |
+
label: 'Forecasted Usage (GB)', data: d.data_usage.forecast,
|
| 466 |
+
backgroundColor: d.holiday_months.map(h => h ? C.warning+'BB' : C.primary+'77'),
|
| 467 |
+
borderColor: d.holiday_months.map(h => h ? C.warning : C.primary),
|
| 468 |
+
borderWidth: 1.5, borderRadius: 8, barPercentage: 0.7
|
| 469 |
+
}]},
|
| 470 |
+
options: {
|
| 471 |
+
responsive: true, maintainAspectRatio: false,
|
| 472 |
+
plugins: {
|
| 473 |
+
legend: { display: false },
|
| 474 |
+
tooltip: { ...tooltipCfg, callbacks: { ...tooltipCfg.callbacks, afterLabel: (ctx) => d.holiday_months[ctx.dataIndex] ? '🎉 Holiday / Event Month' : '' } }
|
| 475 |
+
},
|
| 476 |
+
scales: { y: { title: { display: true, text: 'GB', font: { size: 12, weight: '600' } }, grid: { color: '#f1f5f9' } }, x: xAxisCfg(d.dates) }
|
| 477 |
+
}
|
| 478 |
+
});
|
| 479 |
+
|
| 480 |
+
// --- Network Load ---
|
| 481 |
+
destroyChart('networkLoad');
|
| 482 |
+
const hL = [...hist.network_load, ...Array(d.dates.length).fill(null)];
|
| 483 |
+
const fL = [...Array(hist.dates.length-1).fill(null), hist.network_load.at(-1), ...d.network_load.forecast];
|
| 484 |
+
charts['networkLoad'] = new Chart(document.getElementById('networkLoadChart'), {
|
| 485 |
+
type: 'line',
|
| 486 |
+
data: { labels: allDates, datasets: [
|
| 487 |
+
{ label: 'Historical Load', data: hL, borderColor: C.info, backgroundColor: C.info+'15', fill: true, tension: 0.4, borderWidth: 2.5, pointRadius: 3, pointHoverRadius: 5 },
|
| 488 |
+
{ label: 'Forecast Load', data: fL, borderColor: C.warning, borderDash: [6,3], backgroundColor: C.warning+'12', fill: true, tension: 0.4, borderWidth: 2.5, pointRadius: 3, pointHoverRadius: 5 },
|
| 489 |
+
]},
|
| 490 |
+
options: {
|
| 491 |
+
responsive: true, maintainAspectRatio: false,
|
| 492 |
+
plugins: { legend: { position: 'top', labels: { padding: 16, usePointStyle: true, pointStyle: 'circle' } }, tooltip: tooltipCfg },
|
| 493 |
+
scales: { y: { min: 0.3, max: 1, title: { display: true, text: 'Load Factor', font: { size: 12, weight: '600' } }, grid: { color: '#f1f5f9' } }, x: xAxisCfg(allDates) }
|
| 494 |
+
}
|
| 495 |
+
});
|
| 496 |
+
|
| 497 |
+
// --- Peak Users ---
|
| 498 |
+
destroyChart('peakUsers');
|
| 499 |
+
const hP = [...hist.peak_users, ...Array(d.dates.length).fill(null)];
|
| 500 |
+
const fP = [...Array(hist.dates.length-1).fill(null), hist.peak_users.at(-1), ...d.peak_users.forecast];
|
| 501 |
+
charts['peakUsers'] = new Chart(document.getElementById('peakUsersChart'), {
|
| 502 |
+
type: 'bar',
|
| 503 |
+
data: { labels: allDates, datasets: [
|
| 504 |
+
{ label: 'Historical', data: hP, backgroundColor: C.purple+'88', borderRadius: 5, barPercentage: 0.6 },
|
| 505 |
+
{ label: 'Forecast', data: fP, backgroundColor: C.teal+'88', borderRadius: 5, barPercentage: 0.6 },
|
| 506 |
+
]},
|
| 507 |
+
options: {
|
| 508 |
+
responsive: true, maintainAspectRatio: false,
|
| 509 |
+
plugins: {
|
| 510 |
+
legend: { position: 'top', labels: { padding: 16, usePointStyle: true, pointStyle: 'rect' } },
|
| 511 |
+
tooltip: { ...tooltipCfg, callbacks: { ...tooltipCfg.callbacks, label: (ctx) => ctx.parsed.y ? ctx.dataset.label + ': ' + ctx.parsed.y.toLocaleString() : '' } }
|
| 512 |
+
},
|
| 513 |
+
scales: { y: { title: { display: true, text: 'Peak Users', font: { size: 12, weight: '600' } }, grid: { color: '#f1f5f9' }, ticks: { callback: v => (v/1000).toFixed(0)+'k' } }, x: xAxisCfg(allDates) }
|
| 514 |
+
}
|
| 515 |
+
});
|
| 516 |
+
|
| 517 |
+
// Insights
|
| 518 |
+
const maxLoad = Math.max(...d.network_load.forecast);
|
| 519 |
+
const holidayU = d.data_usage.forecast.filter((_,i) => d.holiday_months[i]);
|
| 520 |
+
const nonHolU = d.data_usage.forecast.filter((_,i) => !d.holiday_months[i]);
|
| 521 |
+
const pctD = holidayU.length && nonHolU.length ? ((Math.max(...holidayU) / Math.min(...nonHolU) - 1)*100).toFixed(0) : '0';
|
| 522 |
+
document.getElementById('insight-peak-demand').textContent = `Holiday months show ${pctD}% higher demand. Plan capacity upgrades for Q4.`;
|
| 523 |
+
document.getElementById('insight-capacity').textContent = `Network load peaks at ${(maxLoad*100).toFixed(0)}%. ${maxLoad > 0.85 ? '⚠️ Capacity expansion recommended!' : 'Within acceptable thresholds.'}`;
|
| 524 |
+
document.getElementById('insight-voice').textContent = `Voice usage trending ${d.voice_minutes.forecast.at(-1) < d.voice_minutes.forecast[0] ? 'downward' : 'upward'}. Data continues to dominate as primary service driver.`;
|
| 525 |
+
} catch(e) { console.error('Seasonal error:', e); }
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
+
// ======================================================================
|
| 529 |
+
// 2. TECH ADOPTION
|
| 530 |
+
// ======================================================================
|
| 531 |
+
async function loadTechAdoption(months) {
|
| 532 |
+
try {
|
| 533 |
+
const r = await fetch(`/api/forecasting/tech-adoption?months=${months}`);
|
| 534 |
+
const d = await r.json(); if (d.error) return;
|
| 535 |
+
const hist = d.historical;
|
| 536 |
+
const allDates = [...hist.dates, ...d.dates];
|
| 537 |
+
|
| 538 |
+
// --- 5G S-Curve ---
|
| 539 |
+
destroyChart('techAdoption');
|
| 540 |
+
const h5 = [...hist.five_g_adoption, ...Array(d.dates.length).fill(null)];
|
| 541 |
+
const f5 = [...Array(hist.dates.length-1).fill(null), hist.five_g_adoption.at(-1), ...d.five_g_adoption.forecast];
|
| 542 |
+
const u5 = [...Array(hist.dates.length-1).fill(null), hist.five_g_adoption.at(-1), ...d.five_g_adoption.upper];
|
| 543 |
+
const l5 = [...Array(hist.dates.length-1).fill(null), hist.five_g_adoption.at(-1), ...d.five_g_adoption.lower];
|
| 544 |
+
|
| 545 |
+
charts['techAdoption'] = new Chart(document.getElementById('techAdoptionChart'), {
|
| 546 |
+
type: 'line',
|
| 547 |
+
data: { labels: allDates, datasets: [
|
| 548 |
+
{ label: 'Historical 5G %', data: h5, borderColor: C.primary, tension: 0.4, borderWidth: 3, pointRadius: 4, pointHoverRadius: 6, fill: false },
|
| 549 |
+
{ label: 'Forecast 5G %', data: f5, borderColor: C.success, borderDash: [6,3], tension: 0.4, borderWidth: 3, pointRadius: 4, pointHoverRadius: 6, fill: false },
|
| 550 |
+
{ label: 'Upper CI', data: u5, borderColor: 'transparent', backgroundColor: C.success+'12', fill: '+1', pointRadius: 0 },
|
| 551 |
+
{ label: 'Lower CI', data: l5, borderColor: 'transparent', backgroundColor: 'transparent', fill: false, pointRadius: 0 },
|
| 552 |
+
]},
|
| 553 |
+
options: {
|
| 554 |
+
responsive: true, maintainAspectRatio: false,
|
| 555 |
+
plugins: { legend: { position: 'top', labels: { filter: legendFilter, padding: 16, usePointStyle: true, pointStyle: 'circle' } }, tooltip: tooltipCfg },
|
| 556 |
+
scales: { y: { title: { display: true, text: '5G Adoption (%)', font: { size: 12, weight: '600' } }, grid: { color: '#f1f5f9' } }, x: xAxisCfg(allDates) }
|
| 557 |
+
}
|
| 558 |
+
});
|
| 559 |
+
|
| 560 |
+
// --- Tech Mix Stacked ---
|
| 561 |
+
destroyChart('techMix');
|
| 562 |
+
charts['techMix'] = new Chart(document.getElementById('techMixChart'), {
|
| 563 |
+
type: 'bar',
|
| 564 |
+
data: { labels: d.dates, datasets: [
|
| 565 |
+
{ label: '5G', data: d.five_g_adoption.forecast, backgroundColor: C.success+'CC', stack: 'tech', borderRadius: { topLeft: 4, topRight: 4 }, barPercentage: 0.65 },
|
| 566 |
+
{ label: '4G', data: d.four_g_pct, backgroundColor: C.info+'CC', stack: 'tech', barPercentage: 0.65 },
|
| 567 |
+
{ label: '3G', data: d.three_g_pct, backgroundColor: C.warning+'99', stack: 'tech', barPercentage: 0.65 },
|
| 568 |
+
]},
|
| 569 |
+
options: {
|
| 570 |
+
responsive: true, maintainAspectRatio: false,
|
| 571 |
+
plugins: { legend: { position: 'top', labels: { padding: 16, usePointStyle: true, pointStyle: 'rect' } }, tooltip: { ...tooltipCfg, mode: 'index' } },
|
| 572 |
+
scales: { y: { stacked: true, max: 100, title: { display: true, text: 'Share (%)', font: { size: 12, weight: '600' } }, grid: { color: '#f1f5f9' } }, x: { stacked: true, ...xAxisCfg(d.dates) } }
|
| 573 |
+
}
|
| 574 |
+
});
|
| 575 |
+
|
| 576 |
+
// --- Tower Deployment ---
|
| 577 |
+
destroyChart('towerDeploy');
|
| 578 |
+
const hT = [...hist.towers_deployed, ...Array(d.dates.length).fill(null)];
|
| 579 |
+
const fT = [...Array(hist.dates.length-1).fill(null), hist.towers_deployed.at(-1), ...d.towers_deployed.forecast];
|
| 580 |
+
charts['towerDeploy'] = new Chart(document.getElementById('towerDeployChart'), {
|
| 581 |
+
type: 'line',
|
| 582 |
+
data: { labels: allDates, datasets: [
|
| 583 |
+
{ label: 'Deployed (Historical)', data: hT, borderColor: C.purple, backgroundColor: C.purple+'20', fill: true, tension: 0.3, borderWidth: 2.5, pointRadius: 3 },
|
| 584 |
+
{ label: 'Projected', data: fT, borderColor: C.teal, backgroundColor: C.teal+'15', fill: true, tension: 0.3, borderDash: [6,3], borderWidth: 2.5, pointRadius: 3 },
|
| 585 |
+
]},
|
| 586 |
+
options: {
|
| 587 |
+
responsive: true, maintainAspectRatio: false,
|
| 588 |
+
plugins: { legend: { position: 'top', labels: { padding: 16, usePointStyle: true, pointStyle: 'circle' } }, tooltip: tooltipCfg },
|
| 589 |
+
scales: { y: { title: { display: true, text: 'Cumulative 5G Towers', font: { size: 12, weight: '600' } }, grid: { color: '#f1f5f9' } }, x: xAxisCfg(allDates) }
|
| 590 |
+
}
|
| 591 |
+
});
|
| 592 |
+
|
| 593 |
+
// --- Revenue Premium & Speed ---
|
| 594 |
+
destroyChart('revenuePremium');
|
| 595 |
+
charts['revenuePremium'] = new Chart(document.getElementById('revenuePremiumChart'), {
|
| 596 |
+
type: 'line',
|
| 597 |
+
data: { labels: d.dates, datasets: [
|
| 598 |
+
{ label: 'Revenue Premium (%)', data: d.revenue_premium.forecast, borderColor: C.success, tension: 0.4, borderWidth: 2.5, yAxisID: 'y', pointRadius: 4, pointHoverRadius: 6 },
|
| 599 |
+
{ label: 'Avg 5G Speed (Mbps)', data: d.avg_speed.forecast, borderColor: C.info, tension: 0.4, borderWidth: 2.5, yAxisID: 'y1', pointRadius: 4, pointHoverRadius: 6 },
|
| 600 |
+
]},
|
| 601 |
+
options: {
|
| 602 |
+
responsive: true, maintainAspectRatio: false,
|
| 603 |
+
plugins: { legend: { position: 'top', labels: { padding: 16, usePointStyle: true, pointStyle: 'circle' } }, tooltip: tooltipCfg },
|
| 604 |
+
scales: {
|
| 605 |
+
y: { position: 'left', title: { display: true, text: 'Premium (%)', font: { size: 12, weight: '600' } }, grid: { color: '#f1f5f9' } },
|
| 606 |
+
y1: { position: 'right', title: { display: true, text: 'Speed (Mbps)', font: { size: 12, weight: '600' } }, grid: { display: false } },
|
| 607 |
+
x: xAxisCfg(d.dates)
|
| 608 |
+
}
|
| 609 |
+
}
|
| 610 |
+
});
|
| 611 |
+
|
| 612 |
+
// Insights
|
| 613 |
+
const lastFc5g = d.five_g_adoption.forecast.at(-1);
|
| 614 |
+
document.getElementById('insight-migration').textContent = `5G adoption projected to reach ${lastFc5g.toFixed(1)}% by end of forecast period. Accelerating customer migration recommended.`;
|
| 615 |
+
document.getElementById('insight-roi').textContent = `Revenue premium of ~${d.revenue_premium.forecast.at(-1).toFixed(0)}% on 5G customers justifies continued infrastructure investment.`;
|
| 616 |
+
const last3g = d.three_g_pct.at(-1);
|
| 617 |
+
document.getElementById('insight-sunset').textContent = `3G declining to ${last3g.toFixed(1)}%. ${last3g < 5 ? 'Consider accelerating 3G sunset.' : 'Monitor migration pace before sunset.'}`;
|
| 618 |
+
} catch(e) { console.error('Tech adoption error:', e); }
|
| 619 |
+
}
|
| 620 |
+
|
| 621 |
+
// ======================================================================
|
| 622 |
+
// 3. COMPETITIVE DYNAMICS
|
| 623 |
+
// ======================================================================
|
| 624 |
+
async function loadCompetitive(months) {
|
| 625 |
+
try {
|
| 626 |
+
const r = await fetch(`/api/forecasting/competitive?months=${months}`);
|
| 627 |
+
const d = await r.json(); if (d.error) return;
|
| 628 |
+
const hist = d.historical;
|
| 629 |
+
const allDates = [...hist.dates, ...d.dates];
|
| 630 |
+
|
| 631 |
+
// --- Market Share ---
|
| 632 |
+
destroyChart('marketShare');
|
| 633 |
+
const hO = [...hist.our_share, ...Array(d.dates.length).fill(null)];
|
| 634 |
+
const hA = [...hist.competitor_a, ...Array(d.dates.length).fill(null)];
|
| 635 |
+
const hB = [...hist.competitor_b, ...Array(d.dates.length).fill(null)];
|
| 636 |
+
const fO = [...Array(hist.dates.length-1).fill(null), hist.our_share.at(-1), ...d.market_shares.ours];
|
| 637 |
+
const fA = [...Array(hist.dates.length-1).fill(null), hist.competitor_a.at(-1), ...d.market_shares.competitor_a];
|
| 638 |
+
const fB = [...Array(hist.dates.length-1).fill(null), hist.competitor_b.at(-1), ...d.market_shares.competitor_b];
|
| 639 |
+
|
| 640 |
+
charts['marketShare'] = new Chart(document.getElementById('marketShareChart'), {
|
| 641 |
+
type: 'line',
|
| 642 |
+
data: { labels: allDates, datasets: [
|
| 643 |
+
{ label: 'TelecomIQ (Hist)', data: hO, borderColor: C.primary, borderWidth: 3, tension: 0.4, pointRadius: 3 },
|
| 644 |
+
{ label: 'TelecomIQ (FC)', data: fO, borderColor: C.primary, borderDash: [6,3], borderWidth: 3, tension: 0.4, pointRadius: 3 },
|
| 645 |
+
{ label: 'Comp A (Hist)', data: hA, borderColor: C.danger, borderWidth: 2, tension: 0.4, pointRadius: 2 },
|
| 646 |
+
{ label: 'Comp A (FC)', data: fA, borderColor: C.danger, borderDash: [6,3], borderWidth: 2, tension: 0.4, pointRadius: 2 },
|
| 647 |
+
{ label: 'Comp B (Hist)', data: hB, borderColor: C.warning, borderWidth: 2, tension: 0.4, pointRadius: 2 },
|
| 648 |
+
{ label: 'Comp B (FC)', data: fB, borderColor: C.warning, borderDash: [6,3], borderWidth: 2, tension: 0.4, pointRadius: 2 },
|
| 649 |
+
]},
|
| 650 |
+
options: {
|
| 651 |
+
responsive: true, maintainAspectRatio: false,
|
| 652 |
+
plugins: { legend: { position: 'top', labels: { padding: 12, usePointStyle: true, pointStyle: 'circle', font: { size: 11 } } }, tooltip: tooltipCfg },
|
| 653 |
+
scales: { y: { title: { display: true, text: 'Market Share (%)', font: { size: 12, weight: '600' } }, grid: { color: '#f1f5f9' } }, x: xAxisCfg(allDates) }
|
| 654 |
+
}
|
| 655 |
+
});
|
| 656 |
+
|
| 657 |
+
// --- Pricing War Risk ---
|
| 658 |
+
destroyChart('pricingWar');
|
| 659 |
+
charts['pricingWar'] = new Chart(document.getElementById('pricingWarChart'), {
|
| 660 |
+
type: 'bar',
|
| 661 |
+
data: { labels: d.dates, datasets: [{
|
| 662 |
+
label: 'Pricing War Risk (%)', data: d.pricing_war_risk,
|
| 663 |
+
backgroundColor: d.pricing_war_risk.map(v => v > 60 ? C.danger+'CC' : v > 35 ? C.warning+'CC' : C.success+'CC'),
|
| 664 |
+
borderRadius: 8, barPercentage: 0.65
|
| 665 |
+
}]},
|
| 666 |
+
options: {
|
| 667 |
+
responsive: true, maintainAspectRatio: false,
|
| 668 |
+
plugins: { legend: { display: false }, tooltip: tooltipCfg },
|
| 669 |
+
scales: { y: { max: 100, title: { display: true, text: 'Risk (%)', font: { size: 12, weight: '600' } }, grid: { color: '#f1f5f9' } }, x: xAxisCfg(d.dates) }
|
| 670 |
+
}
|
| 671 |
+
});
|
| 672 |
+
|
| 673 |
+
// --- Pricing ---
|
| 674 |
+
destroyChart('pricing');
|
| 675 |
+
const hOP = [...hist.our_price, ...Array(d.dates.length).fill(null)];
|
| 676 |
+
const hMP = [...hist.market_price, ...Array(d.dates.length).fill(null)];
|
| 677 |
+
const fOP = [...Array(hist.dates.length-1).fill(null), hist.our_price.at(-1), ...d.pricing.our_price];
|
| 678 |
+
const fMP = [...Array(hist.dates.length-1).fill(null), hist.market_price.at(-1), ...d.pricing.market_price];
|
| 679 |
+
|
| 680 |
+
charts['pricing'] = new Chart(document.getElementById('pricingChart'), {
|
| 681 |
+
type: 'line',
|
| 682 |
+
data: { labels: allDates, datasets: [
|
| 683 |
+
{ label: 'Our Price (Hist)', data: hOP, borderColor: C.primary, borderWidth: 2.5, tension: 0.4, pointRadius: 3 },
|
| 684 |
+
{ label: 'Our Price (FC)', data: fOP, borderColor: C.primary, borderDash: [6,3], borderWidth: 2.5, tension: 0.4, pointRadius: 3 },
|
| 685 |
+
{ label: 'Market Avg (Hist)', data: hMP, borderColor: C.secondary, borderWidth: 2, tension: 0.4, pointRadius: 3 },
|
| 686 |
+
{ label: 'Market Avg (FC)', data: fMP, borderColor: C.secondary, borderDash: [6,3], borderWidth: 2, tension: 0.4, pointRadius: 3 },
|
| 687 |
+
]},
|
| 688 |
+
options: {
|
| 689 |
+
responsive: true, maintainAspectRatio: false,
|
| 690 |
+
plugins: { legend: { position: 'top', labels: { padding: 12, usePointStyle: true, pointStyle: 'circle', font: { size: 11 } } }, tooltip: tooltipCfg },
|
| 691 |
+
scales: { y: { title: { display: true, text: 'Price ($/line)', font: { size: 12, weight: '600' } }, grid: { color: '#f1f5f9' } }, x: xAxisCfg(allDates) }
|
| 692 |
+
}
|
| 693 |
+
});
|
| 694 |
+
|
| 695 |
+
// --- Net Adds ---
|
| 696 |
+
destroyChart('netAdds');
|
| 697 |
+
const hNA = [...hist.net_adds, ...Array(d.dates.length).fill(null)];
|
| 698 |
+
const fNA = [...Array(hist.dates.length-1).fill(null), hist.net_adds.at(-1), ...d.net_adds.forecast];
|
| 699 |
+
charts['netAdds'] = new Chart(document.getElementById('netAddsChart'), {
|
| 700 |
+
type: 'bar',
|
| 701 |
+
data: { labels: allDates, datasets: [
|
| 702 |
+
{ label: 'Historical', data: hNA, backgroundColor: C.info+'88', borderRadius: 5, barPercentage: 0.55 },
|
| 703 |
+
{ label: 'Forecast', data: fNA, backgroundColor: C.teal+'88', borderRadius: 5, barPercentage: 0.55 },
|
| 704 |
+
]},
|
| 705 |
+
options: {
|
| 706 |
+
responsive: true, maintainAspectRatio: false,
|
| 707 |
+
plugins: {
|
| 708 |
+
legend: { position: 'top', labels: { padding: 16, usePointStyle: true, pointStyle: 'rect' } },
|
| 709 |
+
tooltip: { ...tooltipCfg, callbacks: { ...tooltipCfg.callbacks, label: ctx => ctx.parsed.y ? ctx.dataset.label + ': ' + ctx.parsed.y.toLocaleString() : '' } }
|
| 710 |
+
},
|
| 711 |
+
scales: { y: { title: { display: true, text: 'Net Adds', font: { size: 12, weight: '600' } }, grid: { color: '#f1f5f9' } }, x: xAxisCfg(allDates) }
|
| 712 |
+
}
|
| 713 |
+
});
|
| 714 |
+
|
| 715 |
+
// Insights
|
| 716 |
+
const shareEnd = d.market_shares.ours.at(-1), shareStart = hist.our_share.at(-1);
|
| 717 |
+
document.getElementById('insight-position').textContent = `Market share projected at ${shareEnd.toFixed(1)}% (${shareEnd > shareStart ? '+' : ''}${(shareEnd-shareStart).toFixed(1)}pp). ${shareEnd > shareStart ? 'Positive trajectory.' : 'Defensive measures needed.'}`;
|
| 718 |
+
const avgRisk = d.pricing_war_risk.reduce((a,b)=>a+b,0)/d.pricing_war_risk.length;
|
| 719 |
+
document.getElementById('insight-pricing').textContent = `Avg pricing war risk: ${avgRisk.toFixed(0)}%. ${avgRisk > 50 ? 'High risk of competitive pressure on margins.' : 'Stable pricing environment forecasted.'}`;
|
| 720 |
+
const totalAdds = d.net_adds.forecast.reduce((a,b)=>a+b,0);
|
| 721 |
+
document.getElementById('insight-growth').textContent = `Projected net additions: ${totalAdds.toLocaleString()} over forecast period. ${totalAdds > 0 ? 'Positive growth outlook.' : '⚠️ Subscriber base may contract.'}`;
|
| 722 |
+
} catch(e) { console.error('Competitive error:', e); }
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
// ======================================================================
|
| 726 |
+
// 4. ECONOMIC IMPACT
|
| 727 |
+
// ======================================================================
|
| 728 |
+
async function loadEconomic(months) {
|
| 729 |
+
try {
|
| 730 |
+
const r = await fetch(`/api/forecasting/economic?months=${months}`);
|
| 731 |
+
const d = await r.json(); if (d.error) return;
|
| 732 |
+
const hist = d.historical;
|
| 733 |
+
const allDates = [...hist.dates, ...d.dates];
|
| 734 |
+
|
| 735 |
+
// --- GDP + CCI ---
|
| 736 |
+
destroyChart('economicOverview');
|
| 737 |
+
const hG = [...hist.gdp_growth, ...Array(d.dates.length).fill(null)];
|
| 738 |
+
const fG = [...Array(hist.dates.length-1).fill(null), hist.gdp_growth.at(-1), ...d.gdp_growth.forecast];
|
| 739 |
+
const uG = [...Array(hist.dates.length-1).fill(null), hist.gdp_growth.at(-1), ...d.gdp_growth.upper];
|
| 740 |
+
const lG = [...Array(hist.dates.length-1).fill(null), hist.gdp_growth.at(-1), ...d.gdp_growth.lower];
|
| 741 |
+
const hC = [...hist.consumer_confidence, ...Array(d.dates.length).fill(null)];
|
| 742 |
+
const fC = [...Array(hist.dates.length-1).fill(null), hist.consumer_confidence.at(-1), ...d.consumer_confidence.forecast];
|
| 743 |
+
|
| 744 |
+
charts['economicOverview'] = new Chart(document.getElementById('economicOverviewChart'), {
|
| 745 |
+
type: 'line',
|
| 746 |
+
data: { labels: allDates, datasets: [
|
| 747 |
+
{ label: 'GDP Growth % (Hist)', data: hG, borderColor: C.primary, borderWidth: 3, tension: 0.4, pointRadius: 3, yAxisID: 'y' },
|
| 748 |
+
{ label: 'GDP Growth % (FC)', data: fG, borderColor: C.primary, borderDash: [6,3], borderWidth: 3, tension: 0.4, pointRadius: 3, yAxisID: 'y' },
|
| 749 |
+
{ label: 'Upper CI', data: uG, borderColor: 'transparent', backgroundColor: C.primary+'10', fill: '+1', pointRadius: 0, yAxisID: 'y' },
|
| 750 |
+
{ label: 'Lower CI', data: lG, borderColor: 'transparent', fill: false, pointRadius: 0, yAxisID: 'y' },
|
| 751 |
+
{ label: 'Consumer Confidence (Hist)', data: hC, borderColor: C.teal, borderWidth: 2.5, tension: 0.4, pointRadius: 2, yAxisID: 'y1' },
|
| 752 |
+
{ label: 'Consumer Confidence (FC)', data: fC, borderColor: C.teal, borderDash: [6,3], borderWidth: 2.5, tension: 0.4, pointRadius: 2, yAxisID: 'y1' },
|
| 753 |
+
]},
|
| 754 |
+
options: {
|
| 755 |
+
responsive: true, maintainAspectRatio: false,
|
| 756 |
+
plugins: { legend: { position: 'top', labels: { filter: legendFilter, padding: 12, usePointStyle: true, pointStyle: 'circle', font: { size: 11 } } }, tooltip: tooltipCfg },
|
| 757 |
+
scales: {
|
| 758 |
+
y: { position: 'left', title: { display: true, text: 'GDP Growth (%)', font: { size: 12, weight: '600' } }, grid: { color: '#f1f5f9' } },
|
| 759 |
+
y1: { position: 'right', title: { display: true, text: 'CCI (0-100)', font: { size: 12, weight: '600' } }, grid: { display: false } },
|
| 760 |
+
x: xAxisCfg(allDates)
|
| 761 |
+
}
|
| 762 |
+
}
|
| 763 |
+
});
|
| 764 |
+
|
| 765 |
+
// --- Recession Probability ---
|
| 766 |
+
destroyChart('recessionProb');
|
| 767 |
+
charts['recessionProb'] = new Chart(document.getElementById('recessionProbChart'), {
|
| 768 |
+
type: 'bar',
|
| 769 |
+
data: { labels: d.dates, datasets: [{
|
| 770 |
+
label: 'Recession Probability (%)', data: d.recession_probability,
|
| 771 |
+
backgroundColor: d.recession_probability.map(v => v > 50 ? C.danger+'CC' : v > 25 ? C.warning+'CC' : C.success+'CC'),
|
| 772 |
+
borderRadius: 8, barPercentage: 0.65
|
| 773 |
+
}]},
|
| 774 |
+
options: {
|
| 775 |
+
responsive: true, maintainAspectRatio: false,
|
| 776 |
+
plugins: { legend: { display: false }, tooltip: tooltipCfg },
|
| 777 |
+
scales: { y: { max: 100, title: { display: true, text: 'Probability (%)', font: { size: 12, weight: '600' } }, grid: { color: '#f1f5f9' } }, x: xAxisCfg(d.dates) }
|
| 778 |
+
}
|
| 779 |
+
});
|
| 780 |
+
|
| 781 |
+
// --- Revenue at Risk ---
|
| 782 |
+
destroyChart('revenueRisk');
|
| 783 |
+
const hR = [...hist.revenue_at_risk, ...Array(d.dates.length).fill(null)];
|
| 784 |
+
const fR = [...Array(hist.dates.length-1).fill(null), hist.revenue_at_risk.at(-1), ...d.revenue_at_risk.forecast];
|
| 785 |
+
const uR = [...Array(hist.dates.length-1).fill(null), hist.revenue_at_risk.at(-1), ...d.revenue_at_risk.upper];
|
| 786 |
+
const lR = [...Array(hist.dates.length-1).fill(null), hist.revenue_at_risk.at(-1), ...d.revenue_at_risk.lower];
|
| 787 |
+
|
| 788 |
+
charts['revenueRisk'] = new Chart(document.getElementById('revenueRiskChart'), {
|
| 789 |
+
type: 'line',
|
| 790 |
+
data: { labels: allDates, datasets: [
|
| 791 |
+
{ label: 'Historical', data: hR, borderColor: C.danger, backgroundColor: C.danger+'18', fill: true, tension: 0.4, borderWidth: 2.5, pointRadius: 3 },
|
| 792 |
+
{ label: 'Forecast', data: fR, borderColor: C.orange, backgroundColor: C.orange+'15', fill: true, tension: 0.4, borderDash: [6,3], borderWidth: 2.5, pointRadius: 3 },
|
| 793 |
+
{ label: 'Upper CI', data: uR, borderColor: 'transparent', backgroundColor: C.orange+'0D', fill: '+1', pointRadius: 0 },
|
| 794 |
+
{ label: 'Lower CI', data: lR, borderColor: 'transparent', fill: false, pointRadius: 0 },
|
| 795 |
+
]},
|
| 796 |
+
options: {
|
| 797 |
+
responsive: true, maintainAspectRatio: false,
|
| 798 |
+
plugins: { legend: { position: 'top', labels: { filter: legendFilter, padding: 16, usePointStyle: true, pointStyle: 'circle' } }, tooltip: tooltipCfg },
|
| 799 |
+
scales: { y: { title: { display: true, text: 'Revenue at Risk ($M)', font: { size: 12, weight: '600' } }, grid: { color: '#f1f5f9' } }, x: xAxisCfg(allDates) }
|
| 800 |
+
}
|
| 801 |
+
});
|
| 802 |
+
|
| 803 |
+
// --- Behavior Stress ---
|
| 804 |
+
destroyChart('behaviorStress');
|
| 805 |
+
charts['behaviorStress'] = new Chart(document.getElementById('behaviorStressChart'), {
|
| 806 |
+
type: 'line',
|
| 807 |
+
data: { labels: d.dates, datasets: [
|
| 808 |
+
{ label: 'Downgrade Rate (%)', data: d.downgrade_rate.forecast, borderColor: C.danger, borderWidth: 2.5, tension: 0.4, pointRadius: 4, pointHoverRadius: 6, yAxisID: 'y' },
|
| 809 |
+
{ label: 'Delinquency Rate (%)', data: d.delinquency_rate.forecast, borderColor: C.warning, borderWidth: 2.5, tension: 0.4, pointRadius: 4, pointHoverRadius: 6, yAxisID: 'y' },
|
| 810 |
+
{ label: 'Sentiment Index', data: d.sentiment_index.forecast, borderColor: C.success, borderWidth: 2.5, tension: 0.4, pointRadius: 4, pointHoverRadius: 6, yAxisID: 'y1' },
|
| 811 |
+
]},
|
| 812 |
+
options: {
|
| 813 |
+
responsive: true, maintainAspectRatio: false,
|
| 814 |
+
plugins: { legend: { position: 'top', labels: { padding: 14, usePointStyle: true, pointStyle: 'circle', font: { size: 11 } } }, tooltip: tooltipCfg },
|
| 815 |
+
scales: {
|
| 816 |
+
y: { position: 'left', title: { display: true, text: 'Rate (%)', font: { size: 12, weight: '600' } }, grid: { color: '#f1f5f9' } },
|
| 817 |
+
y1: { position: 'right', title: { display: true, text: 'Sentiment (0-100)', font: { size: 12, weight: '600' } }, grid: { display: false } },
|
| 818 |
+
x: xAxisCfg(d.dates)
|
| 819 |
+
}
|
| 820 |
+
}
|
| 821 |
+
});
|
| 822 |
+
|
| 823 |
+
// Insights
|
| 824 |
+
const avgGdp = d.gdp_growth.forecast.reduce((a,b)=>a+b,0)/d.gdp_growth.forecast.length;
|
| 825 |
+
const maxRecess = Math.max(...d.recession_probability);
|
| 826 |
+
document.getElementById('insight-economy').textContent = `Avg GDP growth forecast: ${avgGdp.toFixed(1)}%. ${maxRecess > 40 ? '⚠️ Elevated recession risk detected in forecast window.' : 'Low recession risk period expected.'}`;
|
| 827 |
+
const totalRisk = d.revenue_at_risk.forecast.reduce((a,b)=>a+b,0);
|
| 828 |
+
document.getElementById('insight-revenue').textContent = `Total revenue at risk: $${totalRisk.toFixed(1)}M over forecast period. Proactive retention campaigns recommended.`;
|
| 829 |
+
const avgSent = d.sentiment_index.forecast.reduce((a,b)=>a+b,0)/d.sentiment_index.forecast.length;
|
| 830 |
+
document.getElementById('insight-sentiment').textContent = `Avg customer sentiment: ${avgSent.toFixed(0)}/100. ${avgSent < 55 ? 'Below healthy threshold - engagement programs needed.' : 'Sentiment within acceptable range.'}`;
|
| 831 |
+
} catch(e) { console.error('Economic error:', e); }
|
| 832 |
+
}
|
| 833 |
+
|
| 834 |
+
// ======================================================================
|
| 835 |
+
// INIT
|
| 836 |
+
// ======================================================================
|
| 837 |
+
loadSummary();
|
| 838 |
+
loadSeasonal(getHorizon());
|
| 839 |
+
</script>
|
| 840 |
+
{% endblock %}
|
templates/geographic.html
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}Geographic Network Performance - TelecomIQ{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block extra_css %}
|
| 6 |
+
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
| 7 |
+
<style>
|
| 8 |
+
#networkMap { height: 450px; border-radius: 12px; border: 1px solid var(--border-color); }
|
| 9 |
+
.city-table { width: 100%; border-collapse: separate; border-spacing: 0; }
|
| 10 |
+
.city-table th { background: var(--primary-color); color: white; padding: 12px 16px; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.5px; }
|
| 11 |
+
.city-table td { padding: 10px 16px; border-bottom: 1px solid var(--border-color); font-size: 0.9rem; }
|
| 12 |
+
.city-table tr:hover { background: #f1f5f9; }
|
| 13 |
+
.badge-good { background: #dcfce7; color: #166534; padding: 4px 10px; border-radius: 20px; font-size: 0.75rem; font-weight: 600; }
|
| 14 |
+
.badge-warn { background: #fef3c7; color: #92400e; padding: 4px 10px; border-radius: 20px; font-size: 0.75rem; font-weight: 600; }
|
| 15 |
+
.badge-bad { background: #fecaca; color: #991b1b; padding: 4px 10px; border-radius: 20px; font-size: 0.75rem; font-weight: 600; }
|
| 16 |
+
</style>
|
| 17 |
+
{% endblock %}
|
| 18 |
+
|
| 19 |
+
{% block content %}
|
| 20 |
+
<h2 class="text-primary-custom mb-4">Geographic Network Performance</h2>
|
| 21 |
+
|
| 22 |
+
<div class="row">
|
| 23 |
+
<div class="col-md-3"><div class="metric-card">
|
| 24 |
+
<h6>CITIES COVERED <button class="info-btn" data-info-icon="🏙️" data-info-title="Cities with Active Coverage" data-info-section="Geographic · Coverage KPI" data-info="The number of distinct cities or metropolitan areas with at least one active cell tower providing network service." data-info-tips="Major urban centres typically have 5–20+ towers per city.|Rural city count rising = coverage expansion success.|Cities with only 1 tower are single points of failure — flag for resilience.">ⓘ</button></h6>
|
| 25 |
+
<h3 class="text-primary-custom" id="kpi-cities">-</h3><small>Active coverage areas</small>
|
| 26 |
+
</div></div>
|
| 27 |
+
<div class="col-md-3"><div class="metric-card">
|
| 28 |
+
<h6>TOTAL TOWERS <button class="info-btn" data-info-icon="🗼" data-info-title="Total Cell Tower Count" data-info-section="Geographic · Infrastructure KPI" data-info="The combined count of all active cell towers across every city in the coverage area. This is the foundational infrastructure metric for the geographic view." data-info-tips="More towers = better coverage density and capacity.|National average: ~800 towers per million population for good coverage.|Track tower growth rate to validate infrastructure investment plans.">ⓘ</button></h6>
|
| 29 |
+
<h3 class="text-success-custom" id="kpi-towers">-</h3><small>Network infrastructure</small>
|
| 30 |
+
</div></div>
|
| 31 |
+
<div class="col-md-3"><div class="metric-card">
|
| 32 |
+
<h6>AVG COVERAGE RADIUS <button class="info-btn" data-info-icon="📏" data-info-title="Average Tower Coverage Radius" data-info-section="Geographic · Coverage KPI" data-info="The mean radius (in kilometres) that each cell tower covers. Larger radius = wider geographic coverage per tower, but typically lower signal quality at the edges." data-info-tips="Urban towers: 0.5–2 km radius (dense cells for capacity).|Rural towers: 5–20 km radius (wide coverage for geography).|Radius shrinks as you add more towers — expected in growing networks.">ⓘ</button></h6>
|
| 33 |
+
<h3 class="text-primary-custom" id="kpi-radius">-</h3><small>Per tower (km)</small>
|
| 34 |
+
</div></div>
|
| 35 |
+
<div class="col-md-3"><div class="metric-card">
|
| 36 |
+
<h6>CUSTOMERS/TOWER <button class="info-btn" data-info-icon="👥" data-info-title="Customers per Tower (Tower Load)" data-info-section="Geographic · Capacity KPI" data-info="The average number of customers served per cell tower. High density = tower overload risk. This is a leading indicator of where new tower investment is needed." data-info-tips="Ideal range: 50–100 customers/tower.|Above 150: congestion risk — schedule capacity upgrade or new tower.|Falling ratio = infrastructure growing faster than subscriber base.">ⓘ</button></h6>
|
| 37 |
+
<h3 class="text-warning-custom" id="kpi-density">-</h3><small>Avg load per tower</small>
|
| 38 |
+
</div></div>
|
| 39 |
+
</div>
|
| 40 |
+
|
| 41 |
+
<div class="row mt-4">
|
| 42 |
+
<div class="col-md-12"><div class="chart-card">
|
| 43 |
+
<h5 class="text-primary-custom mb-3">Tower Coverage Map <button class="info-btn" data-info-icon="🗺️" data-info-title="Interactive Tower Coverage Map" data-info-section="Geographic · Geospatial View" data-info="An interactive Leaflet map showing the geographic location of all cell towers across the network. Colour-coded by status: Green = Active, Amber = Maintenance, Red = Offline. Click any tower for details." data-info-tips="Green dots = healthy towers contributing to coverage.|Amber dots = temporary maintenance — check coverage gaps in that area.|Cluster of Red dots = potential regional outage — investigate immediately.">ⓘ</button></h5>
|
| 44 |
+
<div id="networkMap"></div>
|
| 45 |
+
</div></div>
|
| 46 |
+
</div>
|
| 47 |
+
|
| 48 |
+
<div class="row">
|
| 49 |
+
<div class="col-md-8"><div class="chart-card">
|
| 50 |
+
<h5 class="text-primary-custom mb-3">City-Level Performance <button class="info-btn" data-info-icon="🏙️" data-info-title="City-Level Network Performance Table" data-info-section="Geographic · City Table" data-info="A sortable data table listing each city with its tower count, subscriber count, customers-per-tower density, average latency, and health status badge." data-info-tips="Red 'Overloaded' cities (>150 cust/tower) are the highest priority for new tower investment.|Yellow 'Warning' cities should be monitored monthly.|Sort by Cust/Tower to quickly find the most under-served cities.">ⓘ</button></h5>
|
| 51 |
+
<div style="max-height: 400px; overflow-y: auto;">
|
| 52 |
+
<table class="city-table">
|
| 53 |
+
<thead><tr><th>City</th><th>Towers</th><th>Customers</th><th>Cust/Tower</th><th>Avg Latency</th><th>Status</th></tr></thead>
|
| 54 |
+
<tbody id="cityTableBody"></tbody>
|
| 55 |
+
</table>
|
| 56 |
+
</div>
|
| 57 |
+
</div></div>
|
| 58 |
+
<div class="col-md-4"><div class="chart-card">
|
| 59 |
+
<h5 class="text-primary-custom mb-3">Technology Distribution <button class="info-btn" data-info-icon="📶" data-info-title="Network Technology Distribution" data-info-section="Geographic · Technology Chart" data-info="A doughnut chart showing the split of towers by radio technology: 2G, 3G, 4G LTE, and 5G NR. Older technologies (2G/3G) should be phased out; 5G shows modernisation progress." data-info-tips="5G share rising = positive modernisation signal.|2G/3G towers still active = legacy decommission backlog.|4G LTE is the current workhorse — should be >60% of your fleet.">ⓘ</button></h5>
|
| 60 |
+
<canvas id="techDistChart"></canvas>
|
| 61 |
+
</div></div>
|
| 62 |
+
</div>
|
| 63 |
+
|
| 64 |
+
<div class="row">
|
| 65 |
+
<div class="col-md-6"><div class="chart-card">
|
| 66 |
+
<h5 class="text-primary-custom mb-3">Coverage Gaps (High Load Cities) <button class="info-btn" data-info-icon="🚨" data-info-title="Coverage Gaps — High Load Cities" data-info-section="Geographic · Capacity Chart" data-info="A horizontal bar chart ranking cities by customers-per-tower ratio, colour-coded by severity. Red = Overloaded (>150), Amber = Warning (>100), Blue = Acceptable. Use to prioritise new tower deployment." data-info-tips="This chart is your tower deployment priority list.|Combine with revenue/city data to maximise ROI of new tower investment.|Cities at >150 cust/tower with growing subscribers should get towers within 6 months.">ⓘ</button></h5>
|
| 67 |
+
<canvas id="coverageGapChart"></canvas>
|
| 68 |
+
</div></div>
|
| 69 |
+
<div class="col-md-6"><div class="chart-card">
|
| 70 |
+
<h5 class="text-primary-custom mb-3">Customer Density vs Tower Count <button class="info-btn" data-info-icon="🔢" data-info-title="Customer Density vs Tower Count Scatter" data-info-section="Geographic · Density Chart" data-info="A scatter plot showing each city as a point, with number of towers on the X-axis and number of customers on the Y-axis. Cities above the trend line are under-towered relative to their subscriber base." data-info-tips="Cities in the top-left quadrant need more towers urgently.|Cities in the bottom-right have excess capacity — consider tower redeployment.|A linear cluster along the diagonal = well-balanced network deployment.">ⓘ</button></h5>
|
| 71 |
+
<canvas id="densityChart"></canvas>
|
| 72 |
+
</div></div>
|
| 73 |
+
</div>
|
| 74 |
+
|
| 75 |
+
{% endblock %}
|
| 76 |
+
|
| 77 |
+
{% block extra_js %}
|
| 78 |
+
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
| 79 |
+
<script>
|
| 80 |
+
Chart.defaults.color = '#64748b';
|
| 81 |
+
Chart.defaults.borderColor = '#e2e8f0';
|
| 82 |
+
Chart.defaults.font.family = "'Inter', sans-serif";
|
| 83 |
+
|
| 84 |
+
const COLORS = {
|
| 85 |
+
primary: '#4f46e5',
|
| 86 |
+
success: '#10b981',
|
| 87 |
+
warning: '#f59e0b',
|
| 88 |
+
danger: '#ef4444',
|
| 89 |
+
info: '#3b82f6'
|
| 90 |
+
};
|
| 91 |
+
|
| 92 |
+
let map;
|
| 93 |
+
|
| 94 |
+
async function loadGeoData() {
|
| 95 |
+
try {
|
| 96 |
+
const response = await fetch('/api/geographic/overview');
|
| 97 |
+
const data = await response.json();
|
| 98 |
+
|
| 99 |
+
document.getElementById('kpi-cities').textContent = data.total_cities;
|
| 100 |
+
document.getElementById('kpi-towers').textContent = data.total_towers.toLocaleString();
|
| 101 |
+
document.getElementById('kpi-radius').textContent = `${data.avg_radius} km`;
|
| 102 |
+
document.getElementById('kpi-density').textContent = data.avg_customers_per_tower;
|
| 103 |
+
|
| 104 |
+
// Build city table
|
| 105 |
+
const tbody = document.getElementById('cityTableBody');
|
| 106 |
+
tbody.innerHTML = '';
|
| 107 |
+
data.cities.forEach(city => {
|
| 108 |
+
const ratio = city.customers_per_tower;
|
| 109 |
+
let badge = 'badge-good';
|
| 110 |
+
let statusText = 'Healthy';
|
| 111 |
+
if (ratio > 150) { badge = 'badge-bad'; statusText = 'Overloaded'; }
|
| 112 |
+
else if (ratio > 100) { badge = 'badge-warn'; statusText = 'Warning'; }
|
| 113 |
+
|
| 114 |
+
tbody.innerHTML += `<tr>
|
| 115 |
+
<td><strong>${city.city}</strong></td>
|
| 116 |
+
<td>${city.towers}</td>
|
| 117 |
+
<td>${city.customers.toLocaleString()}</td>
|
| 118 |
+
<td>${ratio}</td>
|
| 119 |
+
<td>${city.avg_latency} ms</td>
|
| 120 |
+
<td><span class="${badge}">${statusText}</span></td>
|
| 121 |
+
</tr>`;
|
| 122 |
+
});
|
| 123 |
+
|
| 124 |
+
// Coverage gap chart
|
| 125 |
+
const overloaded = data.cities.filter(c => c.customers_per_tower > 80).sort((a, b) => b.customers_per_tower - a.customers_per_tower).slice(0, 10);
|
| 126 |
+
const gapCtx = document.getElementById('coverageGapChart').getContext('2d');
|
| 127 |
+
new Chart(gapCtx, {
|
| 128 |
+
type: 'bar',
|
| 129 |
+
data: {
|
| 130 |
+
labels: overloaded.map(c => c.city),
|
| 131 |
+
datasets: [{
|
| 132 |
+
label: 'Customers/Tower',
|
| 133 |
+
data: overloaded.map(c => c.customers_per_tower),
|
| 134 |
+
backgroundColor: overloaded.map(c => c.customers_per_tower > 150 ? COLORS.danger : c.customers_per_tower > 100 ? COLORS.warning : COLORS.info),
|
| 135 |
+
borderRadius: 6
|
| 136 |
+
}]
|
| 137 |
+
},
|
| 138 |
+
options: {
|
| 139 |
+
responsive: true,
|
| 140 |
+
indexAxis: 'y',
|
| 141 |
+
plugins: { legend: { display: false } },
|
| 142 |
+
scales: { x: { beginAtZero: true, grid: { color: '#f1f5f9' } } }
|
| 143 |
+
}
|
| 144 |
+
});
|
| 145 |
+
|
| 146 |
+
// Customer density scatter
|
| 147 |
+
const densityCtx = document.getElementById('densityChart').getContext('2d');
|
| 148 |
+
new Chart(densityCtx, {
|
| 149 |
+
type: 'scatter',
|
| 150 |
+
data: {
|
| 151 |
+
datasets: [{
|
| 152 |
+
label: 'Cities',
|
| 153 |
+
data: data.cities.map(c => ({ x: c.towers, y: c.customers })),
|
| 154 |
+
backgroundColor: COLORS.primary,
|
| 155 |
+
pointRadius: 6
|
| 156 |
+
}]
|
| 157 |
+
},
|
| 158 |
+
options: {
|
| 159 |
+
responsive: true,
|
| 160 |
+
plugins: { legend: { display: false } },
|
| 161 |
+
scales: {
|
| 162 |
+
x: { title: { display: true, text: 'Number of Towers' }, grid: { color: '#f1f5f9' } },
|
| 163 |
+
y: { title: { display: true, text: 'Number of Customers' }, grid: { color: '#f1f5f9' } }
|
| 164 |
+
}
|
| 165 |
+
}
|
| 166 |
+
});
|
| 167 |
+
|
| 168 |
+
} catch (error) {
|
| 169 |
+
console.error('Error loading geographic data:', error);
|
| 170 |
+
}
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
async function loadTechDistribution() {
|
| 174 |
+
try {
|
| 175 |
+
const response = await fetch('/api/geographic/tech-distribution');
|
| 176 |
+
const data = await response.json();
|
| 177 |
+
|
| 178 |
+
const ctx = document.getElementById('techDistChart').getContext('2d');
|
| 179 |
+
new Chart(ctx, {
|
| 180 |
+
type: 'doughnut',
|
| 181 |
+
data: {
|
| 182 |
+
labels: data.labels,
|
| 183 |
+
datasets: [{
|
| 184 |
+
data: data.values,
|
| 185 |
+
backgroundColor: [COLORS.primary, COLORS.success, COLORS.warning, COLORS.info, COLORS.danger],
|
| 186 |
+
borderWidth: 4,
|
| 187 |
+
borderColor: '#ffffff'
|
| 188 |
+
}]
|
| 189 |
+
},
|
| 190 |
+
options: {
|
| 191 |
+
responsive: true,
|
| 192 |
+
plugins: { legend: { position: 'bottom' } }
|
| 193 |
+
}
|
| 194 |
+
});
|
| 195 |
+
} catch (error) {
|
| 196 |
+
console.error('Error loading tech distribution:', error);
|
| 197 |
+
}
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
async function loadMap() {
|
| 201 |
+
try {
|
| 202 |
+
const response = await fetch('/api/geographic/tower-locations');
|
| 203 |
+
const data = await response.json();
|
| 204 |
+
|
| 205 |
+
map = L.map('networkMap').setView([39.8, -98.5], 4);
|
| 206 |
+
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
| 207 |
+
attribution: '© OpenStreetMap contributors'
|
| 208 |
+
}).addTo(map);
|
| 209 |
+
|
| 210 |
+
data.towers.forEach(tower => {
|
| 211 |
+
const color = tower.status === 'Active' ? '#10b981' : tower.status === 'Maintenance' ? '#f59e0b' : '#ef4444';
|
| 212 |
+
L.circleMarker([tower.lat, tower.lng], {
|
| 213 |
+
radius: 5,
|
| 214 |
+
fillColor: color,
|
| 215 |
+
color: color,
|
| 216 |
+
weight: 1,
|
| 217 |
+
opacity: 0.8,
|
| 218 |
+
fillOpacity: 0.6
|
| 219 |
+
}).addTo(map).bindPopup(
|
| 220 |
+
`<b>${tower.id}</b><br>City: ${tower.city}<br>Type: ${tower.type}<br>Tech: ${tower.tech}<br>Status: ${tower.status}`
|
| 221 |
+
);
|
| 222 |
+
});
|
| 223 |
+
} catch (error) {
|
| 224 |
+
console.error('Error loading map:', error);
|
| 225 |
+
}
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 229 |
+
loadGeoData();
|
| 230 |
+
loadTechDistribution();
|
| 231 |
+
loadMap();
|
| 232 |
+
});
|
| 233 |
+
</script>
|
| 234 |
+
{% endblock %}
|
templates/journey.html
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}Customer Journey Analytics - TelecomIQ{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block extra_css %}
|
| 6 |
+
<style>
|
| 7 |
+
.journey-stage { padding: 20px; border-radius: 12px; text-align: center; position: relative; }
|
| 8 |
+
.journey-stage h4 { font-size: 1rem; font-weight: 700; margin-bottom: 8px; }
|
| 9 |
+
.journey-stage .count { font-size: 2rem; font-weight: 800; }
|
| 10 |
+
.stage-acquire { background: linear-gradient(135deg, #dbeafe, #bfdbfe); color: #1e40af; }
|
| 11 |
+
.stage-onboard { background: linear-gradient(135deg, #d1fae5, #a7f3d0); color: #065f46; }
|
| 12 |
+
.stage-grow { background: linear-gradient(135deg, #fef3c7, #fde68a); color: #92400e; }
|
| 13 |
+
.stage-retain { background: linear-gradient(135deg, #fce7f3, #fbcfe8); color: #9d174d; }
|
| 14 |
+
.stage-advocate { background: linear-gradient(135deg, #e0e7ff, #c7d2fe); color: #3730a3; }
|
| 15 |
+
.touchpoint-table { width: 100%; }
|
| 16 |
+
.touchpoint-table th { background: var(--primary-color); color: white; padding: 10px 14px; font-size: 0.8rem; text-transform: uppercase; }
|
| 17 |
+
.touchpoint-table td { padding: 8px 14px; border-bottom: 1px solid var(--border-color); font-size: 0.875rem; }
|
| 18 |
+
</style>
|
| 19 |
+
{% endblock %}
|
| 20 |
+
|
| 21 |
+
{% block content %}
|
| 22 |
+
<h2 class="text-primary-custom mb-4">Customer Journey Analytics</h2>
|
| 23 |
+
|
| 24 |
+
<!-- Journey Funnel KPIs -->
|
| 25 |
+
<div class="row">
|
| 26 |
+
<div class="col">
|
| 27 |
+
<div class="journey-stage stage-acquire">
|
| 28 |
+
<h4>ACQUIRE</h4>
|
| 29 |
+
<div class="count" id="stage-acquire">-</div>
|
| 30 |
+
<small>New signups (30d)</small>
|
| 31 |
+
</div>
|
| 32 |
+
</div>
|
| 33 |
+
<div class="col">
|
| 34 |
+
<div class="journey-stage stage-onboard">
|
| 35 |
+
<h4>ONBOARD</h4>
|
| 36 |
+
<div class="count" id="stage-onboard">-</div>
|
| 37 |
+
<small>First 90 days</small>
|
| 38 |
+
</div>
|
| 39 |
+
</div>
|
| 40 |
+
<div class="col">
|
| 41 |
+
<div class="journey-stage stage-grow">
|
| 42 |
+
<h4>GROW</h4>
|
| 43 |
+
<div class="count" id="stage-grow">-</div>
|
| 44 |
+
<small>Active & engaged</small>
|
| 45 |
+
</div>
|
| 46 |
+
</div>
|
| 47 |
+
<div class="col">
|
| 48 |
+
<div class="journey-stage stage-retain">
|
| 49 |
+
<h4>RETAIN</h4>
|
| 50 |
+
<div class="count" id="stage-retain">-</div>
|
| 51 |
+
<small>At risk of churn</small>
|
| 52 |
+
</div>
|
| 53 |
+
</div>
|
| 54 |
+
<div class="col">
|
| 55 |
+
<div class="journey-stage stage-advocate">
|
| 56 |
+
<h4>ADVOCATE</h4>
|
| 57 |
+
<div class="count" id="stage-advocate">-</div>
|
| 58 |
+
<small>Referral makers</small>
|
| 59 |
+
</div>
|
| 60 |
+
</div>
|
| 61 |
+
</div>
|
| 62 |
+
|
| 63 |
+
<!-- Charts Row 1 -->
|
| 64 |
+
<div class="row mt-4">
|
| 65 |
+
<div class="col-md-6">
|
| 66 |
+
<div class="chart-card">
|
| 67 |
+
<h5 class="text-primary-custom mb-3">Engagement Score Distribution</h5>
|
| 68 |
+
<canvas id="engagementChart"></canvas>
|
| 69 |
+
</div>
|
| 70 |
+
</div>
|
| 71 |
+
<div class="col-md-6">
|
| 72 |
+
<div class="chart-card">
|
| 73 |
+
<h5 class="text-primary-custom mb-3">Touchpoint Performance</h5>
|
| 74 |
+
<canvas id="touchpointChart"></canvas>
|
| 75 |
+
</div>
|
| 76 |
+
</div>
|
| 77 |
+
</div>
|
| 78 |
+
|
| 79 |
+
<!-- Charts Row 2 -->
|
| 80 |
+
<div class="row">
|
| 81 |
+
<div class="col-md-6">
|
| 82 |
+
<div class="chart-card">
|
| 83 |
+
<h5 class="text-primary-custom mb-3">Monthly Customer Acquisition vs Churn</h5>
|
| 84 |
+
<canvas id="acqChurnChart"></canvas>
|
| 85 |
+
</div>
|
| 86 |
+
</div>
|
| 87 |
+
<div class="col-md-6">
|
| 88 |
+
<div class="chart-card">
|
| 89 |
+
<h5 class="text-primary-custom mb-3">Service Channel Usage</h5>
|
| 90 |
+
<canvas id="channelUsageChart"></canvas>
|
| 91 |
+
</div>
|
| 92 |
+
</div>
|
| 93 |
+
</div>
|
| 94 |
+
|
| 95 |
+
<!-- Touchpoint Table -->
|
| 96 |
+
<div class="row">
|
| 97 |
+
<div class="col-md-12">
|
| 98 |
+
<div class="chart-card">
|
| 99 |
+
<h5 class="text-primary-custom mb-3">Customer Touchpoint Detail</h5>
|
| 100 |
+
<div style="max-height: 300px; overflow-y: auto;">
|
| 101 |
+
<table class="touchpoint-table">
|
| 102 |
+
<thead>
|
| 103 |
+
<tr>
|
| 104 |
+
<th>Channel</th>
|
| 105 |
+
<th>Interactions</th>
|
| 106 |
+
<th>Avg CSAT</th>
|
| 107 |
+
<th>Resolution Rate</th>
|
| 108 |
+
<th>Avg Resolution Time</th>
|
| 109 |
+
</tr>
|
| 110 |
+
</thead>
|
| 111 |
+
<tbody id="touchpointTableBody">
|
| 112 |
+
</tbody>
|
| 113 |
+
</table>
|
| 114 |
+
</div>
|
| 115 |
+
</div>
|
| 116 |
+
</div>
|
| 117 |
+
</div>
|
| 118 |
+
|
| 119 |
+
{% endblock %}
|
| 120 |
+
|
| 121 |
+
{% block extra_js %}
|
| 122 |
+
<script>
|
| 123 |
+
Chart.defaults.color = '#64748b';
|
| 124 |
+
Chart.defaults.borderColor = '#e2e8f0';
|
| 125 |
+
Chart.defaults.font.family = "'Inter', sans-serif";
|
| 126 |
+
|
| 127 |
+
const COLORS = {
|
| 128 |
+
primary: '#4f46e5',
|
| 129 |
+
success: '#10b981',
|
| 130 |
+
warning: '#f59e0b',
|
| 131 |
+
danger: '#ef4444',
|
| 132 |
+
info: '#3b82f6'
|
| 133 |
+
};
|
| 134 |
+
|
| 135 |
+
async function loadJourneyData() {
|
| 136 |
+
try {
|
| 137 |
+
const response = await fetch('/api/journey/overview');
|
| 138 |
+
const data = await response.json();
|
| 139 |
+
|
| 140 |
+
document.getElementById('stage-acquire').textContent = data.acquire.toLocaleString();
|
| 141 |
+
document.getElementById('stage-onboard').textContent = data.onboard.toLocaleString();
|
| 142 |
+
document.getElementById('stage-grow').textContent = data.grow.toLocaleString();
|
| 143 |
+
document.getElementById('stage-retain').textContent = data.retain.toLocaleString();
|
| 144 |
+
document.getElementById('stage-advocate').textContent = data.advocate.toLocaleString();
|
| 145 |
+
|
| 146 |
+
// Touchpoint table
|
| 147 |
+
const tbody = document.getElementById('touchpointTableBody');
|
| 148 |
+
tbody.innerHTML = '';
|
| 149 |
+
data.touchpoints.forEach(tp => {
|
| 150 |
+
tbody.innerHTML += `<tr>
|
| 151 |
+
<td><strong>${tp.channel}</strong></td>
|
| 152 |
+
<td>${tp.interactions.toLocaleString()}</td>
|
| 153 |
+
<td>${tp.avg_csat}/10</td>
|
| 154 |
+
<td>${tp.resolution_rate}%</td>
|
| 155 |
+
<td>${tp.avg_resolution} min</td>
|
| 156 |
+
</tr>`;
|
| 157 |
+
});
|
| 158 |
+
|
| 159 |
+
} catch (error) {
|
| 160 |
+
console.error('Error loading journey data:', error);
|
| 161 |
+
}
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
async function loadEngagementChart() {
|
| 165 |
+
try {
|
| 166 |
+
const response = await fetch('/api/journey/engagement');
|
| 167 |
+
const data = await response.json();
|
| 168 |
+
|
| 169 |
+
const ctx = document.getElementById('engagementChart').getContext('2d');
|
| 170 |
+
new Chart(ctx, {
|
| 171 |
+
type: 'bar',
|
| 172 |
+
data: {
|
| 173 |
+
labels: data.labels,
|
| 174 |
+
datasets: [{
|
| 175 |
+
label: 'Customers',
|
| 176 |
+
data: data.values,
|
| 177 |
+
backgroundColor: [COLORS.danger, COLORS.warning, COLORS.info, COLORS.success, COLORS.primary],
|
| 178 |
+
borderRadius: 8
|
| 179 |
+
}]
|
| 180 |
+
},
|
| 181 |
+
options: {
|
| 182 |
+
responsive: true,
|
| 183 |
+
plugins: { legend: { display: false } },
|
| 184 |
+
scales: { y: { beginAtZero: true, grid: { color: '#f1f5f9' } } }
|
| 185 |
+
}
|
| 186 |
+
});
|
| 187 |
+
} catch (error) {
|
| 188 |
+
console.error('Error loading engagement chart:', error);
|
| 189 |
+
}
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
async function loadTouchpointChart() {
|
| 193 |
+
try {
|
| 194 |
+
const response = await fetch('/api/journey/touchpoint-satisfaction');
|
| 195 |
+
const data = await response.json();
|
| 196 |
+
|
| 197 |
+
const ctx = document.getElementById('touchpointChart').getContext('2d');
|
| 198 |
+
new Chart(ctx, {
|
| 199 |
+
type: 'radar',
|
| 200 |
+
data: {
|
| 201 |
+
labels: data.channels,
|
| 202 |
+
datasets: [{
|
| 203 |
+
label: 'CSAT Score',
|
| 204 |
+
data: data.scores,
|
| 205 |
+
borderColor: COLORS.primary,
|
| 206 |
+
backgroundColor: 'rgba(79, 70, 229, 0.15)',
|
| 207 |
+
borderWidth: 2,
|
| 208 |
+
pointBackgroundColor: COLORS.primary
|
| 209 |
+
}]
|
| 210 |
+
},
|
| 211 |
+
options: {
|
| 212 |
+
responsive: true,
|
| 213 |
+
scales: {
|
| 214 |
+
r: { min: 0, max: 10, ticks: { stepSize: 2 }, grid: { color: '#e2e8f0' } }
|
| 215 |
+
}
|
| 216 |
+
}
|
| 217 |
+
});
|
| 218 |
+
} catch (error) {
|
| 219 |
+
console.error('Error loading touchpoint chart:', error);
|
| 220 |
+
}
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
async function loadAcqChurnChart() {
|
| 224 |
+
try {
|
| 225 |
+
const response = await fetch('/api/journey/acquisition-churn');
|
| 226 |
+
const data = await response.json();
|
| 227 |
+
|
| 228 |
+
const ctx = document.getElementById('acqChurnChart').getContext('2d');
|
| 229 |
+
new Chart(ctx, {
|
| 230 |
+
type: 'bar',
|
| 231 |
+
data: {
|
| 232 |
+
labels: data.months,
|
| 233 |
+
datasets: [
|
| 234 |
+
{ label: 'New Customers', data: data.acquisitions, backgroundColor: COLORS.success, borderRadius: 6 },
|
| 235 |
+
{ label: 'Churned', data: data.churns, backgroundColor: COLORS.danger, borderRadius: 6 }
|
| 236 |
+
]
|
| 237 |
+
},
|
| 238 |
+
options: {
|
| 239 |
+
responsive: true,
|
| 240 |
+
scales: { y: { beginAtZero: true, grid: { color: '#f1f5f9' } } }
|
| 241 |
+
}
|
| 242 |
+
});
|
| 243 |
+
} catch (error) {
|
| 244 |
+
console.error('Error loading acquisition/churn chart:', error);
|
| 245 |
+
}
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
async function loadChannelUsageChart() {
|
| 249 |
+
try {
|
| 250 |
+
const response = await fetch('/api/journey/channel-usage');
|
| 251 |
+
const data = await response.json();
|
| 252 |
+
|
| 253 |
+
const ctx = document.getElementById('channelUsageChart').getContext('2d');
|
| 254 |
+
new Chart(ctx, {
|
| 255 |
+
type: 'doughnut',
|
| 256 |
+
data: {
|
| 257 |
+
labels: data.channels,
|
| 258 |
+
datasets: [{
|
| 259 |
+
data: data.values,
|
| 260 |
+
backgroundColor: [COLORS.primary, COLORS.success, COLORS.warning, COLORS.info, COLORS.danger],
|
| 261 |
+
borderWidth: 4,
|
| 262 |
+
borderColor: '#ffffff'
|
| 263 |
+
}]
|
| 264 |
+
},
|
| 265 |
+
options: {
|
| 266 |
+
responsive: true,
|
| 267 |
+
plugins: { legend: { position: 'right' } }
|
| 268 |
+
}
|
| 269 |
+
});
|
| 270 |
+
} catch (error) {
|
| 271 |
+
console.error('Error loading channel usage chart:', error);
|
| 272 |
+
}
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 276 |
+
loadJourneyData();
|
| 277 |
+
loadEngagementChart();
|
| 278 |
+
loadTouchpointChart();
|
| 279 |
+
loadAcqChurnChart();
|
| 280 |
+
loadChannelUsageChart();
|
| 281 |
+
});
|
| 282 |
+
</script>
|
| 283 |
+
{% endblock %}
|
templates/landing.html
ADDED
|
@@ -0,0 +1,966 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>TelecomIQ Analytics Platform — Telecommunications Customer Intelligence</title>
|
| 7 |
+
<meta name="description" content="End-to-end Telecommunications Customer Intelligence Platform with AI-powered churn prediction, network analytics, revenue forecasting, and real-time ML dashboards." />
|
| 8 |
+
|
| 9 |
+
<!-- Bootstrap CSS -->
|
| 10 |
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" />
|
| 11 |
+
<!-- Google Fonts -->
|
| 12 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet" />
|
| 13 |
+
<!-- Bootstrap Icons -->
|
| 14 |
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" />
|
| 15 |
+
|
| 16 |
+
<style>
|
| 17 |
+
:root {
|
| 18 |
+
--primary: #4f46e5;
|
| 19 |
+
--primary-light: #6366f1;
|
| 20 |
+
--primary-dark: #4338ca;
|
| 21 |
+
--accent: #ec4899;
|
| 22 |
+
--success: #10b981;
|
| 23 |
+
--warning: #f59e0b;
|
| 24 |
+
--danger: #ef4444;
|
| 25 |
+
--info: #3b82f6;
|
| 26 |
+
--bg: #0b0f1a;
|
| 27 |
+
--bg-card: #111827;
|
| 28 |
+
--bg-surface: #1a2235;
|
| 29 |
+
--border: rgba(255,255,255,0.07);
|
| 30 |
+
--text: #f1f5f9;
|
| 31 |
+
--text-muted: #94a3b8;
|
| 32 |
+
--glow: rgba(99,102,241,0.35);
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
| 36 |
+
|
| 37 |
+
html { scroll-behavior: smooth; }
|
| 38 |
+
|
| 39 |
+
body {
|
| 40 |
+
font-family: 'Inter', sans-serif;
|
| 41 |
+
background: var(--bg);
|
| 42 |
+
color: var(--text);
|
| 43 |
+
overflow-x: hidden;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
/* ── CANVAS BACKGROUND ─────────────────────────────── */
|
| 47 |
+
#bg-canvas {
|
| 48 |
+
position: fixed;
|
| 49 |
+
inset: 0;
|
| 50 |
+
z-index: 0;
|
| 51 |
+
pointer-events: none;
|
| 52 |
+
opacity: 0.45;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
/* ── NAVBAR ─────────────────────────────────────────── */
|
| 56 |
+
.nav-top {
|
| 57 |
+
position: fixed;
|
| 58 |
+
top: 0; left: 0; right: 0;
|
| 59 |
+
z-index: 100;
|
| 60 |
+
backdrop-filter: blur(20px);
|
| 61 |
+
-webkit-backdrop-filter: blur(20px);
|
| 62 |
+
background: rgba(11,15,26,0.82);
|
| 63 |
+
border-bottom: 1px solid var(--border);
|
| 64 |
+
padding: 0.9rem 0;
|
| 65 |
+
transition: padding 0.3s;
|
| 66 |
+
}
|
| 67 |
+
.nav-inner {
|
| 68 |
+
max-width: 1200px;
|
| 69 |
+
margin: 0 auto;
|
| 70 |
+
padding: 0 1.5rem;
|
| 71 |
+
display: flex;
|
| 72 |
+
align-items: center;
|
| 73 |
+
gap: 1.5rem;
|
| 74 |
+
}
|
| 75 |
+
.logo {
|
| 76 |
+
font-size: 1.35rem;
|
| 77 |
+
font-weight: 800;
|
| 78 |
+
background: linear-gradient(135deg, #a5b4fc, var(--accent));
|
| 79 |
+
-webkit-background-clip: text;
|
| 80 |
+
-webkit-text-fill-color: transparent;
|
| 81 |
+
text-decoration: none;
|
| 82 |
+
letter-spacing: -0.5px;
|
| 83 |
+
}
|
| 84 |
+
.logo span { font-weight: 300; opacity: .7; }
|
| 85 |
+
.nav-links { display: flex; gap: 0.25rem; margin-left: auto; flex-wrap: wrap; }
|
| 86 |
+
.nav-links a {
|
| 87 |
+
color: var(--text-muted);
|
| 88 |
+
font-size: 0.875rem;
|
| 89 |
+
font-weight: 500;
|
| 90 |
+
text-decoration: none;
|
| 91 |
+
padding: 0.45rem 0.85rem;
|
| 92 |
+
border-radius: 8px;
|
| 93 |
+
transition: all 0.25s;
|
| 94 |
+
}
|
| 95 |
+
.nav-links a:hover { color: #fff; background: rgba(255,255,255,0.08); }
|
| 96 |
+
.nav-cta {
|
| 97 |
+
background: linear-gradient(135deg, var(--primary), var(--primary-light));
|
| 98 |
+
color: #fff !important;
|
| 99 |
+
padding: 0.5rem 1.1rem !important;
|
| 100 |
+
border-radius: 10px !important;
|
| 101 |
+
font-weight: 600 !important;
|
| 102 |
+
box-shadow: 0 0 20px var(--glow);
|
| 103 |
+
}
|
| 104 |
+
.nav-cta:hover { transform: translateY(-1px); box-shadow: 0 0 30px var(--glow); }
|
| 105 |
+
|
| 106 |
+
/* ── HERO ────────────────────────────────────────────── */
|
| 107 |
+
.hero {
|
| 108 |
+
position: relative;
|
| 109 |
+
z-index: 1;
|
| 110 |
+
min-height: 100vh;
|
| 111 |
+
display: flex;
|
| 112 |
+
flex-direction: column;
|
| 113 |
+
align-items: center;
|
| 114 |
+
justify-content: center;
|
| 115 |
+
text-align: center;
|
| 116 |
+
padding: 8rem 1.5rem 5rem;
|
| 117 |
+
}
|
| 118 |
+
.hero-badge {
|
| 119 |
+
display: inline-flex;
|
| 120 |
+
align-items: center;
|
| 121 |
+
gap: 0.5rem;
|
| 122 |
+
background: rgba(99,102,241,0.15);
|
| 123 |
+
border: 1px solid rgba(99,102,241,0.35);
|
| 124 |
+
border-radius: 999px;
|
| 125 |
+
padding: 0.4rem 1.1rem;
|
| 126 |
+
font-size: 0.8rem;
|
| 127 |
+
font-weight: 600;
|
| 128 |
+
color: #a5b4fc;
|
| 129 |
+
letter-spacing: 0.4px;
|
| 130 |
+
margin-bottom: 2rem;
|
| 131 |
+
animation: fadeUp 0.7s ease both;
|
| 132 |
+
}
|
| 133 |
+
.hero-badge .dot {
|
| 134 |
+
width: 7px; height: 7px;
|
| 135 |
+
background: var(--success);
|
| 136 |
+
border-radius: 50%;
|
| 137 |
+
animation: pulse 1.8s infinite;
|
| 138 |
+
}
|
| 139 |
+
@keyframes pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:.6;transform:scale(1.3)} }
|
| 140 |
+
|
| 141 |
+
.hero h1 {
|
| 142 |
+
font-size: clamp(2.4rem, 6vw, 4.5rem);
|
| 143 |
+
font-weight: 900;
|
| 144 |
+
line-height: 1.08;
|
| 145 |
+
letter-spacing: -1.5px;
|
| 146 |
+
margin-bottom: 1.5rem;
|
| 147 |
+
animation: fadeUp 0.8s 0.1s ease both;
|
| 148 |
+
}
|
| 149 |
+
.hero h1 .grad {
|
| 150 |
+
background: linear-gradient(135deg, #a5b4fc 0%, var(--accent) 55%, #fbbf24 100%);
|
| 151 |
+
-webkit-background-clip: text;
|
| 152 |
+
-webkit-text-fill-color: transparent;
|
| 153 |
+
}
|
| 154 |
+
.hero p {
|
| 155 |
+
max-width: 640px;
|
| 156 |
+
font-size: 1.15rem;
|
| 157 |
+
color: var(--text-muted);
|
| 158 |
+
line-height: 1.75;
|
| 159 |
+
margin: 0 auto 2.5rem;
|
| 160 |
+
animation: fadeUp 0.8s 0.2s ease both;
|
| 161 |
+
}
|
| 162 |
+
.hero-btns {
|
| 163 |
+
display: flex;
|
| 164 |
+
gap: 1rem;
|
| 165 |
+
justify-content: center;
|
| 166 |
+
flex-wrap: wrap;
|
| 167 |
+
animation: fadeUp 0.8s 0.3s ease both;
|
| 168 |
+
}
|
| 169 |
+
.btn-primary-glow {
|
| 170 |
+
display: inline-flex;
|
| 171 |
+
align-items: center;
|
| 172 |
+
gap: 0.5rem;
|
| 173 |
+
background: linear-gradient(135deg, var(--primary), var(--primary-light));
|
| 174 |
+
color: #fff;
|
| 175 |
+
padding: 0.85rem 2rem;
|
| 176 |
+
border-radius: 12px;
|
| 177 |
+
font-weight: 700;
|
| 178 |
+
font-size: 1rem;
|
| 179 |
+
text-decoration: none;
|
| 180 |
+
box-shadow: 0 0 40px var(--glow), 0 8px 24px rgba(0,0,0,0.3);
|
| 181 |
+
transition: all 0.3s;
|
| 182 |
+
border: none;
|
| 183 |
+
}
|
| 184 |
+
.btn-primary-glow:hover { transform: translateY(-3px); box-shadow: 0 0 60px var(--glow), 0 12px 32px rgba(0,0,0,0.4); color: #fff; }
|
| 185 |
+
.btn-ghost {
|
| 186 |
+
display: inline-flex;
|
| 187 |
+
align-items: center;
|
| 188 |
+
gap: 0.5rem;
|
| 189 |
+
background: transparent;
|
| 190 |
+
border: 1px solid var(--border);
|
| 191 |
+
color: var(--text);
|
| 192 |
+
padding: 0.85rem 2rem;
|
| 193 |
+
border-radius: 12px;
|
| 194 |
+
font-weight: 600;
|
| 195 |
+
font-size: 1rem;
|
| 196 |
+
text-decoration: none;
|
| 197 |
+
transition: all 0.3s;
|
| 198 |
+
backdrop-filter: blur(10px);
|
| 199 |
+
}
|
| 200 |
+
.btn-ghost:hover { border-color: rgba(255,255,255,0.25); background: rgba(255,255,255,0.07); color: #fff; transform: translateY(-2px); }
|
| 201 |
+
|
| 202 |
+
/* stats strip */
|
| 203 |
+
.hero-stats {
|
| 204 |
+
display: flex;
|
| 205 |
+
gap: 2.5rem;
|
| 206 |
+
justify-content: center;
|
| 207 |
+
flex-wrap: wrap;
|
| 208 |
+
margin-top: 3.5rem;
|
| 209 |
+
animation: fadeUp 0.8s 0.45s ease both;
|
| 210 |
+
}
|
| 211 |
+
.stat-item { text-align: center; }
|
| 212 |
+
.stat-item .num {
|
| 213 |
+
font-size: 1.8rem;
|
| 214 |
+
font-weight: 800;
|
| 215 |
+
background: linear-gradient(135deg, #a5b4fc, var(--accent));
|
| 216 |
+
-webkit-background-clip: text;
|
| 217 |
+
-webkit-text-fill-color: transparent;
|
| 218 |
+
}
|
| 219 |
+
.stat-item .lbl { font-size: 0.78rem; color: var(--text-muted); font-weight: 500; letter-spacing: 0.3px; margin-top: 2px; }
|
| 220 |
+
.stat-divider { width: 1px; background: var(--border); align-self: stretch; }
|
| 221 |
+
|
| 222 |
+
/* scroll hint */
|
| 223 |
+
.scroll-hint {
|
| 224 |
+
position: absolute;
|
| 225 |
+
bottom: 2rem;
|
| 226 |
+
left: 50%;
|
| 227 |
+
transform: translateX(-50%);
|
| 228 |
+
display: flex;
|
| 229 |
+
flex-direction: column;
|
| 230 |
+
align-items: center;
|
| 231 |
+
gap: 0.5rem;
|
| 232 |
+
color: var(--text-muted);
|
| 233 |
+
font-size: 0.75rem;
|
| 234 |
+
animation: fadeUp 1s 0.8s ease both;
|
| 235 |
+
}
|
| 236 |
+
.scroll-hint .mouse {
|
| 237 |
+
width: 22px; height: 34px;
|
| 238 |
+
border: 2px solid var(--border);
|
| 239 |
+
border-radius: 11px;
|
| 240 |
+
display: flex;
|
| 241 |
+
justify-content: center;
|
| 242 |
+
padding-top: 5px;
|
| 243 |
+
}
|
| 244 |
+
.scroll-hint .mouse::after {
|
| 245 |
+
content: '';
|
| 246 |
+
width: 3px; height: 7px;
|
| 247 |
+
background: var(--text-muted);
|
| 248 |
+
border-radius: 99px;
|
| 249 |
+
animation: scrollAnim 1.6s infinite;
|
| 250 |
+
}
|
| 251 |
+
@keyframes scrollAnim { 0%{opacity:1;transform:translateY(0)} 100%{opacity:0;transform:translateY(10px)} }
|
| 252 |
+
|
| 253 |
+
@keyframes fadeUp { from{opacity:0;transform:translateY(24px)} to{opacity:1;transform:translateY(0)} }
|
| 254 |
+
|
| 255 |
+
/* ── SECTION BASE ───────────────────────────────────── */
|
| 256 |
+
section { position: relative; z-index: 1; }
|
| 257 |
+
.section-inner { max-width: 1200px; margin: 0 auto; padding: 5rem 1.5rem; }
|
| 258 |
+
.section-label {
|
| 259 |
+
display: inline-block;
|
| 260 |
+
font-size: 0.72rem;
|
| 261 |
+
font-weight: 700;
|
| 262 |
+
letter-spacing: 1.5px;
|
| 263 |
+
text-transform: uppercase;
|
| 264 |
+
color: #a5b4fc;
|
| 265 |
+
margin-bottom: 0.75rem;
|
| 266 |
+
}
|
| 267 |
+
.section-title {
|
| 268 |
+
font-size: clamp(1.8rem, 4vw, 2.8rem);
|
| 269 |
+
font-weight: 800;
|
| 270 |
+
letter-spacing: -0.75px;
|
| 271 |
+
line-height: 1.15;
|
| 272 |
+
margin-bottom: 1rem;
|
| 273 |
+
}
|
| 274 |
+
.section-sub {
|
| 275 |
+
font-size: 1.05rem;
|
| 276 |
+
color: var(--text-muted);
|
| 277 |
+
max-width: 560px;
|
| 278 |
+
line-height: 1.7;
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
/* ── FEATURES ────────────────────────────────────────── */
|
| 282 |
+
.features-bg { background: var(--bg-card); border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); }
|
| 283 |
+
.features-grid {
|
| 284 |
+
display: grid;
|
| 285 |
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
| 286 |
+
gap: 1.5rem;
|
| 287 |
+
margin-top: 3rem;
|
| 288 |
+
}
|
| 289 |
+
.feat-card {
|
| 290 |
+
background: var(--bg-surface);
|
| 291 |
+
border: 1px solid var(--border);
|
| 292 |
+
border-radius: 20px;
|
| 293 |
+
padding: 2rem;
|
| 294 |
+
transition: all 0.35s cubic-bezier(.25,.8,.25,1);
|
| 295 |
+
position: relative;
|
| 296 |
+
overflow: hidden;
|
| 297 |
+
cursor: default;
|
| 298 |
+
}
|
| 299 |
+
.feat-card::before {
|
| 300 |
+
content: '';
|
| 301 |
+
position: absolute;
|
| 302 |
+
inset: 0;
|
| 303 |
+
background: radial-gradient(circle at top left, var(--card-glow, rgba(99,102,241,0.08)), transparent 65%);
|
| 304 |
+
opacity: 0;
|
| 305 |
+
transition: opacity 0.35s;
|
| 306 |
+
}
|
| 307 |
+
.feat-card:hover { transform: translateY(-6px); border-color: rgba(165,180,252,0.25); box-shadow: 0 20px 60px rgba(0,0,0,0.4); }
|
| 308 |
+
.feat-card:hover::before { opacity: 1; }
|
| 309 |
+
.feat-icon {
|
| 310 |
+
width: 52px; height: 52px;
|
| 311 |
+
border-radius: 14px;
|
| 312 |
+
display: flex; align-items: center; justify-content: center;
|
| 313 |
+
font-size: 1.5rem;
|
| 314 |
+
margin-bottom: 1.25rem;
|
| 315 |
+
}
|
| 316 |
+
.feat-card h3 { font-size: 1.05rem; font-weight: 700; margin-bottom: 0.6rem; }
|
| 317 |
+
.feat-card p { font-size: 0.9rem; color: var(--text-muted); line-height: 1.7; }
|
| 318 |
+
.feat-link {
|
| 319 |
+
display: inline-flex;
|
| 320 |
+
align-items: center;
|
| 321 |
+
gap: 0.35rem;
|
| 322 |
+
margin-top: 1.25rem;
|
| 323 |
+
font-size: 0.875rem;
|
| 324 |
+
font-weight: 600;
|
| 325 |
+
text-decoration: none;
|
| 326 |
+
transition: gap 0.2s;
|
| 327 |
+
}
|
| 328 |
+
.feat-link:hover { gap: 0.6rem; }
|
| 329 |
+
|
| 330 |
+
/* ── DASHBOARD MODULES ───────────────────────────────── */
|
| 331 |
+
.modules-grid {
|
| 332 |
+
display: grid;
|
| 333 |
+
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
| 334 |
+
gap: 1rem;
|
| 335 |
+
margin-top: 3rem;
|
| 336 |
+
}
|
| 337 |
+
.module-card {
|
| 338 |
+
background: var(--bg-card);
|
| 339 |
+
border: 1px solid var(--border);
|
| 340 |
+
border-radius: 16px;
|
| 341 |
+
padding: 1.5rem 1.5rem 1.25rem;
|
| 342 |
+
text-decoration: none;
|
| 343 |
+
color: var(--text);
|
| 344 |
+
display: flex;
|
| 345 |
+
flex-direction: column;
|
| 346 |
+
gap: 0.75rem;
|
| 347 |
+
transition: all 0.3s cubic-bezier(.25,.8,.25,1);
|
| 348 |
+
position: relative;
|
| 349 |
+
overflow: hidden;
|
| 350 |
+
}
|
| 351 |
+
.module-card::after {
|
| 352 |
+
content: '';
|
| 353 |
+
position: absolute;
|
| 354 |
+
bottom: 0; left: 0; right: 0;
|
| 355 |
+
height: 3px;
|
| 356 |
+
background: var(--chip-color, var(--primary));
|
| 357 |
+
transform: scaleX(0);
|
| 358 |
+
transform-origin: left;
|
| 359 |
+
transition: transform 0.35s ease;
|
| 360 |
+
}
|
| 361 |
+
.module-card:hover { transform: translateY(-4px); border-color: rgba(255,255,255,0.14); box-shadow: 0 16px 48px rgba(0,0,0,0.35); color: var(--text); }
|
| 362 |
+
.module-card:hover::after { transform: scaleX(1); }
|
| 363 |
+
.mod-icon { font-size: 1.6rem; }
|
| 364 |
+
.mod-title { font-size: 1rem; font-weight: 700; }
|
| 365 |
+
.mod-desc { font-size: 0.82rem; color: var(--text-muted); line-height: 1.6; flex: 1; }
|
| 366 |
+
.mod-tag {
|
| 367 |
+
align-self: flex-start;
|
| 368 |
+
font-size: 0.7rem;
|
| 369 |
+
font-weight: 600;
|
| 370 |
+
letter-spacing: 0.6px;
|
| 371 |
+
text-transform: uppercase;
|
| 372 |
+
padding: 0.28rem 0.7rem;
|
| 373 |
+
border-radius: 999px;
|
| 374 |
+
background: rgba(255,255,255,0.06);
|
| 375 |
+
color: var(--text-muted);
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
/* ── HOW IT WORKS ────────────────────────────────────── */
|
| 379 |
+
.pipeline {
|
| 380 |
+
display: flex;
|
| 381 |
+
flex-direction: column;
|
| 382 |
+
gap: 0;
|
| 383 |
+
margin-top: 3rem;
|
| 384 |
+
position: relative;
|
| 385 |
+
}
|
| 386 |
+
.pipeline::before {
|
| 387 |
+
content: '';
|
| 388 |
+
position: absolute;
|
| 389 |
+
left: 28px;
|
| 390 |
+
top: 48px;
|
| 391 |
+
bottom: 48px;
|
| 392 |
+
width: 2px;
|
| 393 |
+
background: linear-gradient(to bottom, var(--primary), var(--accent));
|
| 394 |
+
border-radius: 2px;
|
| 395 |
+
}
|
| 396 |
+
.pipe-step {
|
| 397 |
+
display: flex;
|
| 398 |
+
gap: 1.75rem;
|
| 399 |
+
align-items: flex-start;
|
| 400 |
+
padding: 1.5rem 0;
|
| 401 |
+
animation: fadeUp 0.5s ease both;
|
| 402 |
+
}
|
| 403 |
+
.pipe-num {
|
| 404 |
+
flex-shrink: 0;
|
| 405 |
+
width: 58px; height: 58px;
|
| 406 |
+
border-radius: 50%;
|
| 407 |
+
background: linear-gradient(135deg, var(--primary-dark), var(--primary-light));
|
| 408 |
+
display: flex;
|
| 409 |
+
align-items: center;
|
| 410 |
+
justify-content: center;
|
| 411 |
+
font-size: 1.15rem;
|
| 412 |
+
font-weight: 800;
|
| 413 |
+
box-shadow: 0 0 0 4px var(--bg), 0 0 20px var(--glow);
|
| 414 |
+
position: relative;
|
| 415 |
+
z-index: 1;
|
| 416 |
+
}
|
| 417 |
+
.pipe-body h4 { font-size: 1.05rem; font-weight: 700; margin-bottom: 0.4rem; }
|
| 418 |
+
.pipe-body p { font-size: 0.9rem; color: var(--text-muted); line-height: 1.7; }
|
| 419 |
+
|
| 420 |
+
/* ── TECH STACK ──────────────────────────────────────── */
|
| 421 |
+
.tech-grid {
|
| 422 |
+
display: flex;
|
| 423 |
+
flex-wrap: wrap;
|
| 424 |
+
gap: 0.75rem;
|
| 425 |
+
margin-top: 2.5rem;
|
| 426 |
+
}
|
| 427 |
+
.tech-chip {
|
| 428 |
+
background: var(--bg-card);
|
| 429 |
+
border: 1px solid var(--border);
|
| 430 |
+
border-radius: 10px;
|
| 431 |
+
padding: 0.6rem 1.1rem;
|
| 432 |
+
display: flex;
|
| 433 |
+
align-items: center;
|
| 434 |
+
gap: 0.55rem;
|
| 435 |
+
font-size: 0.875rem;
|
| 436 |
+
font-weight: 500;
|
| 437 |
+
color: var(--text-muted);
|
| 438 |
+
transition: all 0.25s;
|
| 439 |
+
}
|
| 440 |
+
.tech-chip:hover { border-color: rgba(165,180,252,0.3); color: var(--text); transform: translateY(-2px); }
|
| 441 |
+
.tech-chip .dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
| 442 |
+
|
| 443 |
+
/* ── CTA BANNER ──────────────────────────────────────── */
|
| 444 |
+
.cta-banner {
|
| 445 |
+
background: linear-gradient(135deg, #1e1b4b 0%, #312e81 50%, #1e1b4b 100%);
|
| 446 |
+
border-top: 1px solid rgba(165,180,252,0.15);
|
| 447 |
+
border-bottom: 1px solid rgba(165,180,252,0.15);
|
| 448 |
+
position: relative;
|
| 449 |
+
overflow: hidden;
|
| 450 |
+
}
|
| 451 |
+
.cta-banner::before {
|
| 452 |
+
content: '';
|
| 453 |
+
position: absolute;
|
| 454 |
+
inset: -50%;
|
| 455 |
+
background: radial-gradient(ellipse at center, rgba(99,102,241,0.2) 0%, transparent 65%);
|
| 456 |
+
pointer-events: none;
|
| 457 |
+
}
|
| 458 |
+
.cta-inner {
|
| 459 |
+
max-width: 720px;
|
| 460 |
+
margin: 0 auto;
|
| 461 |
+
padding: 5rem 1.5rem;
|
| 462 |
+
text-align: center;
|
| 463 |
+
position: relative;
|
| 464 |
+
z-index: 1;
|
| 465 |
+
}
|
| 466 |
+
.cta-inner h2 { font-size: clamp(1.8rem, 4vw, 2.6rem); font-weight: 800; margin-bottom: 1rem; letter-spacing: -0.5px; }
|
| 467 |
+
.cta-inner p { color: rgba(255,255,255,0.65); font-size: 1.05rem; margin-bottom: 2.5rem; line-height: 1.7; }
|
| 468 |
+
|
| 469 |
+
/* ── FOOTER ──────────────────────────────────────────── */
|
| 470 |
+
footer {
|
| 471 |
+
background: var(--bg-card);
|
| 472 |
+
border-top: 1px solid var(--border);
|
| 473 |
+
padding: 2.5rem 1.5rem;
|
| 474 |
+
text-align: center;
|
| 475 |
+
color: var(--text-muted);
|
| 476 |
+
font-size: 0.85rem;
|
| 477 |
+
position: relative;
|
| 478 |
+
z-index: 1;
|
| 479 |
+
}
|
| 480 |
+
footer .footer-inner { max-width: 1200px; margin: 0 auto; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 1rem; }
|
| 481 |
+
footer a { color: var(--text-muted); text-decoration: none; transition: color 0.2s; }
|
| 482 |
+
footer a:hover { color: #a5b4fc; }
|
| 483 |
+
.footer-links { display: flex; gap: 1.5rem; }
|
| 484 |
+
|
| 485 |
+
/* ── RESPONSIVE ──────────────────────────────────────── */
|
| 486 |
+
@media (max-width: 640px) {
|
| 487 |
+
.pipeline::before { left: 22px; }
|
| 488 |
+
.pipe-num { width: 46px; height: 46px; font-size: 1rem; }
|
| 489 |
+
.stat-divider { display: none; }
|
| 490 |
+
.nav-links { display: none; }
|
| 491 |
+
}
|
| 492 |
+
</style>
|
| 493 |
+
</head>
|
| 494 |
+
<body>
|
| 495 |
+
|
| 496 |
+
<!-- Animated Background Canvas -->
|
| 497 |
+
<canvas id="bg-canvas"></canvas>
|
| 498 |
+
|
| 499 |
+
<!-- ── NAVBAR ──────────────────────────────────────────────── -->
|
| 500 |
+
<nav class="nav-top">
|
| 501 |
+
<div class="nav-inner">
|
| 502 |
+
<a href="/" class="logo">TelecomIQ <span>Analytics</span></a>
|
| 503 |
+
<div class="nav-links">
|
| 504 |
+
<a href="/executive">Executive</a>
|
| 505 |
+
<a href="/network">Network</a>
|
| 506 |
+
<a href="/customer">Customer</a>
|
| 507 |
+
<a href="/churn">Churn</a>
|
| 508 |
+
<a href="/forecasting">Forecast</a>
|
| 509 |
+
<a href="/predictions">ML</a>
|
| 510 |
+
<a href="/executive" class="nav-cta">Open Dashboard →</a>
|
| 511 |
+
</div>
|
| 512 |
+
</div>
|
| 513 |
+
</nav>
|
| 514 |
+
|
| 515 |
+
<!-- ── HERO ────────────────────────────────────────────────── -->
|
| 516 |
+
<section class="hero">
|
| 517 |
+
<div class="hero-badge">
|
| 518 |
+
<span class="dot"></span>
|
| 519 |
+
AI-Powered Intelligence Platform · Live
|
| 520 |
+
</div>
|
| 521 |
+
|
| 522 |
+
<h1>
|
| 523 |
+
Telecommunications<br/>
|
| 524 |
+
<span class="grad">Customer Intelligence</span><br/>
|
| 525 |
+
at Scale
|
| 526 |
+
</h1>
|
| 527 |
+
|
| 528 |
+
<p>
|
| 529 |
+
A complete end-to-end analytics pipeline combining synthetic data generation,
|
| 530 |
+
ML-powered churn prediction, network monitoring, revenue forecasting,
|
| 531 |
+
and real-time business insights — all in one unified platform.
|
| 532 |
+
</p>
|
| 533 |
+
|
| 534 |
+
<div class="hero-btns">
|
| 535 |
+
<a href="/executive" class="btn-primary-glow" id="hero-open-dashboard">
|
| 536 |
+
<i class="bi bi-speedometer2"></i> Open Dashboard
|
| 537 |
+
</a>
|
| 538 |
+
<a href="/predictions" class="btn-ghost" id="hero-explore-ml">
|
| 539 |
+
<i class="bi bi-cpu"></i> Explore ML Models
|
| 540 |
+
</a>
|
| 541 |
+
</div>
|
| 542 |
+
|
| 543 |
+
<div class="hero-stats">
|
| 544 |
+
<div class="stat-item">
|
| 545 |
+
<div class="num" id="stat-customers">12K+</div>
|
| 546 |
+
<div class="lbl">Customers Modelled</div>
|
| 547 |
+
</div>
|
| 548 |
+
<div class="stat-divider"></div>
|
| 549 |
+
<div class="stat-item">
|
| 550 |
+
<div class="num">11</div>
|
| 551 |
+
<div class="lbl">Analytics Modules</div>
|
| 552 |
+
</div>
|
| 553 |
+
<div class="stat-divider"></div>
|
| 554 |
+
<div class="stat-item">
|
| 555 |
+
<div class="num">94.2%</div>
|
| 556 |
+
<div class="lbl">Model Accuracy</div>
|
| 557 |
+
</div>
|
| 558 |
+
<div class="stat-divider"></div>
|
| 559 |
+
<div class="stat-item">
|
| 560 |
+
<div class="num">Real-Time</div>
|
| 561 |
+
<div class="lbl">Score Inference</div>
|
| 562 |
+
</div>
|
| 563 |
+
</div>
|
| 564 |
+
|
| 565 |
+
<div class="scroll-hint">
|
| 566 |
+
<div class="mouse"></div>
|
| 567 |
+
Scroll to explore
|
| 568 |
+
</div>
|
| 569 |
+
</section>
|
| 570 |
+
|
| 571 |
+
<!-- ── FEATURES ─────────────────────────────────────────────── -->
|
| 572 |
+
<section class="features-bg">
|
| 573 |
+
<div class="section-inner">
|
| 574 |
+
<div class="text-center">
|
| 575 |
+
<span class="section-label">Core Capabilities</span>
|
| 576 |
+
<h2 class="section-title">Everything you need to understand<br/>your customers</h2>
|
| 577 |
+
<p class="section-sub mx-auto">From raw data to boardroom-ready insights, the platform covers every analytical dimension of your telecom business.</p>
|
| 578 |
+
</div>
|
| 579 |
+
|
| 580 |
+
<div class="features-grid">
|
| 581 |
+
<div class="feat-card" style="--card-glow: rgba(99,102,241,0.12);">
|
| 582 |
+
<div class="feat-icon" style="background: rgba(99,102,241,0.15); color: #a5b4fc;">
|
| 583 |
+
<i class="bi bi-brain"></i>
|
| 584 |
+
</div>
|
| 585 |
+
<h3>AI Churn Prediction</h3>
|
| 586 |
+
<p>Gradient Boosting & Random Forest models identify at-risk customers before they leave, enabling proactive retention campaigns with precision.</p>
|
| 587 |
+
<a href="/churn" class="feat-link" style="color: #a5b4fc;" id="feat-churn-link">
|
| 588 |
+
View Churn Module <i class="bi bi-arrow-right"></i>
|
| 589 |
+
</a>
|
| 590 |
+
</div>
|
| 591 |
+
<div class="feat-card" style="--card-glow: rgba(236,72,153,0.1);">
|
| 592 |
+
<div class="feat-icon" style="background: rgba(236,72,153,0.15); color: #f9a8d4;">
|
| 593 |
+
<i class="bi bi-graph-up-arrow"></i>
|
| 594 |
+
</div>
|
| 595 |
+
<h3>Revenue Forecasting</h3>
|
| 596 |
+
<p>Time-series models project monthly revenue, ARPU trends, and segment-level growth with confidence intervals for strategic planning.</p>
|
| 597 |
+
<a href="/forecasting" class="feat-link" style="color: #f9a8d4;" id="feat-forecast-link">
|
| 598 |
+
View Forecasting <i class="bi bi-arrow-right"></i>
|
| 599 |
+
</a>
|
| 600 |
+
</div>
|
| 601 |
+
<div class="feat-card" style="--card-glow: rgba(16,185,129,0.1);">
|
| 602 |
+
<div class="feat-icon" style="background: rgba(16,185,129,0.15); color: #6ee7b7;">
|
| 603 |
+
<i class="bi bi-wifi"></i>
|
| 604 |
+
</div>
|
| 605 |
+
<h3>Network Operations</h3>
|
| 606 |
+
<p>Monitor tower health, signal quality, outages, and capacity utilisation across all geographic regions in a single operations view.</p>
|
| 607 |
+
<a href="/network" class="feat-link" style="color: #6ee7b7;" id="feat-network-link">
|
| 608 |
+
View Network Ops <i class="bi bi-arrow-right"></i>
|
| 609 |
+
</a>
|
| 610 |
+
</div>
|
| 611 |
+
<div class="feat-card" style="--card-glow: rgba(245,158,11,0.1);">
|
| 612 |
+
<div class="feat-icon" style="background: rgba(245,158,11,0.15); color: #fde68a;">
|
| 613 |
+
<i class="bi bi-people-fill"></i>
|
| 614 |
+
</div>
|
| 615 |
+
<h3>Customer Segmentation</h3>
|
| 616 |
+
<p>K-Means clustering groups customers by lifetime value, usage behaviour, and loyalty risk — enabling hyper-targeted engagement.</p>
|
| 617 |
+
<a href="/segmentation" class="feat-link" style="color: #fde68a;" id="feat-segment-link">
|
| 618 |
+
View Segmentation <i class="bi bi-arrow-right"></i>
|
| 619 |
+
</a>
|
| 620 |
+
</div>
|
| 621 |
+
<div class="feat-card" style="--card-glow: rgba(59,130,246,0.1);">
|
| 622 |
+
<div class="feat-icon" style="background: rgba(59,130,246,0.15); color: #93c5fd;">
|
| 623 |
+
<i class="bi bi-cpu-fill"></i>
|
| 624 |
+
</div>
|
| 625 |
+
<h3>ML Model Hub</h3>
|
| 626 |
+
<p>Score individual customers in real-time using trained models, view feature importance, SHAP values, and ROC/AUC diagnostics.</p>
|
| 627 |
+
<a href="/predictions" class="feat-link" style="color: #93c5fd;" id="feat-ml-link">
|
| 628 |
+
View ML Hub <i class="bi bi-arrow-right"></i>
|
| 629 |
+
</a>
|
| 630 |
+
</div>
|
| 631 |
+
<div class="feat-card" style="--card-glow: rgba(239,68,68,0.1);">
|
| 632 |
+
<div class="feat-icon" style="background: rgba(239,68,68,0.15); color: #fca5a5;">
|
| 633 |
+
<i class="bi bi-bar-chart-line-fill"></i>
|
| 634 |
+
</div>
|
| 635 |
+
<h3>Executive Dashboard</h3>
|
| 636 |
+
<p>A C-suite ready overview of KPIs — subscriber growth, ARPU, NPS, churn rate, and network reliability — updated daily.</p>
|
| 637 |
+
<a href="/executive" class="feat-link" style="color: #fca5a5;" id="feat-exec-link">
|
| 638 |
+
View Executive View <i class="bi bi-arrow-right"></i>
|
| 639 |
+
</a>
|
| 640 |
+
</div>
|
| 641 |
+
</div>
|
| 642 |
+
</div>
|
| 643 |
+
</section>
|
| 644 |
+
|
| 645 |
+
<!-- ── DASHBOARD MODULES ──────────────────────────────────── -->
|
| 646 |
+
<section>
|
| 647 |
+
<div class="section-inner">
|
| 648 |
+
<span class="section-label">Platform Modules</span>
|
| 649 |
+
<h2 class="section-title">All analytical dashboards,<br/>at your fingertips</h2>
|
| 650 |
+
<p class="section-sub">Navigate directly to any module. Each one provides deep-dive analytics powered by a shared ML pipeline.</p>
|
| 651 |
+
|
| 652 |
+
<div class="modules-grid">
|
| 653 |
+
|
| 654 |
+
<a href="/executive" class="module-card" id="mod-executive" style="--chip-color: #6366f1;">
|
| 655 |
+
<div class="mod-icon">📊</div>
|
| 656 |
+
<div class="mod-title">Executive Dashboard</div>
|
| 657 |
+
<div class="mod-desc">Top-level KPIs, subscriber trends, revenue and ARPU at a glance.</div>
|
| 658 |
+
<div class="mod-tag">Strategy</div>
|
| 659 |
+
</a>
|
| 660 |
+
|
| 661 |
+
<a href="/network" class="module-card" id="mod-network" style="--chip-color: #10b981;">
|
| 662 |
+
<div class="mod-icon">📡</div>
|
| 663 |
+
<div class="mod-title">Network Operations</div>
|
| 664 |
+
<div class="mod-desc">Tower health, signal quality, outage tracking, and coverage maps.</div>
|
| 665 |
+
<div class="mod-tag">Operations</div>
|
| 666 |
+
</a>
|
| 667 |
+
|
| 668 |
+
<a href="/geographic" class="module-card" id="mod-geographic" style="--chip-color: #3b82f6;">
|
| 669 |
+
<div class="mod-icon">🗺️</div>
|
| 670 |
+
<div class="mod-title">Geographic Analysis</div>
|
| 671 |
+
<div class="mod-desc">Regional revenue, churn hotspots, and subscriber density maps.</div>
|
| 672 |
+
<div class="mod-tag">Geospatial</div>
|
| 673 |
+
</a>
|
| 674 |
+
|
| 675 |
+
<a href="/customer" class="module-card" id="mod-customer" style="--chip-color: #f59e0b;">
|
| 676 |
+
<div class="mod-icon">👥</div>
|
| 677 |
+
<div class="mod-title">Customer Analytics</div>
|
| 678 |
+
<div class="mod-desc">Usage trends, plan distribution, device profiles and demographics.</div>
|
| 679 |
+
<div class="mod-tag">CRM</div>
|
| 680 |
+
</a>
|
| 681 |
+
|
| 682 |
+
<a href="/journey" class="module-card" id="mod-journey" style="--chip-color: #ec4899;">
|
| 683 |
+
<div class="mod-icon">🛤️</div>
|
| 684 |
+
<div class="mod-title">Customer Journey</div>
|
| 685 |
+
<div class="mod-desc">Lifecycle stages, touchpoints, engagement scores and churn moments.</div>
|
| 686 |
+
<div class="mod-tag">Experience</div>
|
| 687 |
+
</a>
|
| 688 |
+
|
| 689 |
+
<a href="/segmentation" class="module-card" id="mod-segmentation" style="--chip-color: #8b5cf6;">
|
| 690 |
+
<div class="mod-icon">🔬</div>
|
| 691 |
+
<div class="mod-title">Segmentation</div>
|
| 692 |
+
<div class="mod-desc">K-Means clusters by ARPU, tenure, data usage, and loyalty score.</div>
|
| 693 |
+
<div class="mod-tag">AI / ML</div>
|
| 694 |
+
</a>
|
| 695 |
+
|
| 696 |
+
<a href="/churn" class="module-card" id="mod-churn" style="--chip-color: #ef4444;">
|
| 697 |
+
<div class="mod-icon">⚠️</div>
|
| 698 |
+
<div class="mod-title">Churn Risk</div>
|
| 699 |
+
<div class="mod-desc">At-risk customer lists, risk scores, and retention recommendations.</div>
|
| 700 |
+
<div class="mod-tag">Retention</div>
|
| 701 |
+
</a>
|
| 702 |
+
|
| 703 |
+
<a href="/quality" class="module-card" id="mod-quality" style="--chip-color: #06b6d4;">
|
| 704 |
+
<div class="mod-icon">✅</div>
|
| 705 |
+
<div class="mod-title">Quality of Service</div>
|
| 706 |
+
<div class="mod-desc">Call drop rates, data speeds, complaint resolutions, and SLA compliance.</div>
|
| 707 |
+
<div class="mod-tag">QoS</div>
|
| 708 |
+
</a>
|
| 709 |
+
|
| 710 |
+
<a href="/financial" class="module-card" id="mod-financial" style="--chip-color: #22c55e;">
|
| 711 |
+
<div class="mod-icon">💰</div>
|
| 712 |
+
<div class="mod-title">Financial Analytics</div>
|
| 713 |
+
<div class="mod-desc">Margin analysis, revenue per segment, cost drivers, and EBITDA trends.</div>
|
| 714 |
+
<div class="mod-tag">Finance</div>
|
| 715 |
+
</a>
|
| 716 |
+
|
| 717 |
+
<a href="/predictions" class="module-card" id="mod-predictions" style="--chip-color: #a855f7;">
|
| 718 |
+
<div class="mod-icon">🤖</div>
|
| 719 |
+
<div class="mod-title">ML Predictions</div>
|
| 720 |
+
<div class="mod-desc">Live model scoring, feature importance, SHAP values, and ROC curves.</div>
|
| 721 |
+
<div class="mod-tag">AI / ML</div>
|
| 722 |
+
</a>
|
| 723 |
+
|
| 724 |
+
<a href="/forecasting" class="module-card" id="mod-forecasting" style="--chip-color: #f97316;">
|
| 725 |
+
<div class="mod-icon">📈</div>
|
| 726 |
+
<div class="mod-title">Forecasting</div>
|
| 727 |
+
<div class="mod-desc">12-month revenue, subscriber, and ARPU projections with CI bands.</div>
|
| 728 |
+
<div class="mod-tag">Planning</div>
|
| 729 |
+
</a>
|
| 730 |
+
|
| 731 |
+
</div>
|
| 732 |
+
</div>
|
| 733 |
+
</section>
|
| 734 |
+
|
| 735 |
+
<!-- ── HOW IT WORKS ─────────────────────────────────────────── -->
|
| 736 |
+
<section class="features-bg">
|
| 737 |
+
<div class="section-inner">
|
| 738 |
+
<div style="display:grid; grid-template-columns: 1fr 1fr; gap: 4rem; align-items: center;">
|
| 739 |
+
<div>
|
| 740 |
+
<span class="section-label">Architecture</span>
|
| 741 |
+
<h2 class="section-title" style="margin-bottom:0.6rem;">How the pipeline works</h2>
|
| 742 |
+
<p class="section-sub" style="margin-bottom: 0;">From raw synthetic data to AI-powered dashboards — a fully automated, reproducible MLOps workflow.</p>
|
| 743 |
+
</div>
|
| 744 |
+
<div>
|
| 745 |
+
<div class="pipeline">
|
| 746 |
+
<div class="pipe-step">
|
| 747 |
+
<div class="pipe-num">1</div>
|
| 748 |
+
<div class="pipe-body">
|
| 749 |
+
<h4>Data Generation</h4>
|
| 750 |
+
<p>Synthetic telecom data modelling 12,500 customers across demographics, plans, network events, and usage patterns.</p>
|
| 751 |
+
</div>
|
| 752 |
+
</div>
|
| 753 |
+
<div class="pipe-step">
|
| 754 |
+
<div class="pipe-num">2</div>
|
| 755 |
+
<div class="pipe-body">
|
| 756 |
+
<h4>Feature Engineering</h4>
|
| 757 |
+
<p>75+ features engineered from raw logs — including RFM scores, usage deltas, complaint frequency, and loyalty tiers.</p>
|
| 758 |
+
</div>
|
| 759 |
+
</div>
|
| 760 |
+
<div class="pipe-step">
|
| 761 |
+
<div class="pipe-num">3</div>
|
| 762 |
+
<div class="pipe-body">
|
| 763 |
+
<h4>Model Training & Evaluation</h4>
|
| 764 |
+
<p>GBM, Random Forest, and Logistic Regression models trained & evaluated. Best model auto-selected by AUC-ROC.</p>
|
| 765 |
+
</div>
|
| 766 |
+
</div>
|
| 767 |
+
<div class="pipe-step">
|
| 768 |
+
<div class="pipe-num">4</div>
|
| 769 |
+
<div class="pipe-body">
|
| 770 |
+
<h4>Deployment & Scoring</h4>
|
| 771 |
+
<p>Flask API serves real-time predictions. All dashboards consume live scores and ML insights.</p>
|
| 772 |
+
</div>
|
| 773 |
+
</div>
|
| 774 |
+
</div>
|
| 775 |
+
</div>
|
| 776 |
+
</div>
|
| 777 |
+
</div>
|
| 778 |
+
</section>
|
| 779 |
+
|
| 780 |
+
<!-- ── TECH STACK ───────────────────────────────────────────── -->
|
| 781 |
+
<section>
|
| 782 |
+
<div class="section-inner">
|
| 783 |
+
<span class="section-label">Technology</span>
|
| 784 |
+
<h2 class="section-title">Built with best-in-class tools</h2>
|
| 785 |
+
<p class="section-sub">Every component is open-source and production-ready, running entirely on your local machine.</p>
|
| 786 |
+
|
| 787 |
+
<div class="tech-grid">
|
| 788 |
+
<div class="tech-chip"><span class="dot" style="background:#4b8bbe;"></span> Python 3.11</div>
|
| 789 |
+
<div class="tech-chip"><span class="dot" style="background:#f7931a;"></span> Scikit-Learn</div>
|
| 790 |
+
<div class="tech-chip"><span class="dot" style="background:#ee6c4d;"></span> XGBoost / GBM</div>
|
| 791 |
+
<div class="tech-chip"><span class="dot" style="background:#e10098;"></span> Flask</div>
|
| 792 |
+
<div class="tech-chip"><span class="dot" style="background:#56a0d3;"></span> Pandas & NumPy</div>
|
| 793 |
+
<div class="tech-chip"><span class="dot" style="background:#f9c74f;"></span> Matplotlib / Seaborn</div>
|
| 794 |
+
<div class="tech-chip"><span class="dot" style="background:#ff6384;"></span> Chart.js</div>
|
| 795 |
+
<div class="tech-chip"><span class="dot" style="background:#7952b3;"></span> Bootstrap 5</div>
|
| 796 |
+
<div class="tech-chip"><span class="dot" style="background:#38bdf8;"></span> Joblib</div>
|
| 797 |
+
<div class="tech-chip"><span class="dot" style="background:#a3e635;"></span> SHAP</div>
|
| 798 |
+
<div class="tech-chip"><span class="dot" style="background:#fb923c;"></span> Google Fonts (Inter)</div>
|
| 799 |
+
<div class="tech-chip"><span class="dot" style="background:#c4b5fd;"></span> MLflow-ready</div>
|
| 800 |
+
</div>
|
| 801 |
+
</div>
|
| 802 |
+
</section>
|
| 803 |
+
|
| 804 |
+
<!-- ── CTA BANNER ──────────────────────────────────────────── -->
|
| 805 |
+
<section class="cta-banner">
|
| 806 |
+
<div class="cta-inner">
|
| 807 |
+
<h2>Ready to explore the data?</h2>
|
| 808 |
+
<p>Dive straight into any of the 11 analytics modules and discover actionable intelligence for every layer of your telecom business.</p>
|
| 809 |
+
<div style="display:flex;gap:1rem;justify-content:center;flex-wrap:wrap;">
|
| 810 |
+
<a href="/executive" class="btn-primary-glow" id="cta-executive">
|
| 811 |
+
<i class="bi bi-speedometer2"></i> Executive Dashboard
|
| 812 |
+
</a>
|
| 813 |
+
<a href="/predictions" class="btn-ghost" id="cta-predictions">
|
| 814 |
+
<i class="bi bi-cpu"></i> ML Predictions
|
| 815 |
+
</a>
|
| 816 |
+
</div>
|
| 817 |
+
</div>
|
| 818 |
+
</section>
|
| 819 |
+
|
| 820 |
+
<!-- ── FOOTER ───────────────────────────────────────────────── -->
|
| 821 |
+
<footer>
|
| 822 |
+
<div class="footer-inner">
|
| 823 |
+
<span>© 2026 <strong>TelecomIQ Analytics Platform</strong> — Telecommunications Customer Intelligence</span>
|
| 824 |
+
<div class="footer-links">
|
| 825 |
+
<a href="/executive">Executive</a>
|
| 826 |
+
<a href="/network">Network</a>
|
| 827 |
+
<a href="/predictions">ML</a>
|
| 828 |
+
<a href="/forecasting">Forecast</a>
|
| 829 |
+
</div>
|
| 830 |
+
</div>
|
| 831 |
+
</footer>
|
| 832 |
+
|
| 833 |
+
<!-- Bootstrap JS -->
|
| 834 |
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
| 835 |
+
|
| 836 |
+
<script>
|
| 837 |
+
/* ── Particle / Neural Network Background ───────────────── */
|
| 838 |
+
(function() {
|
| 839 |
+
const canvas = document.getElementById('bg-canvas');
|
| 840 |
+
const ctx = canvas.getContext('2d');
|
| 841 |
+
let W, H, nodes = [], RAF;
|
| 842 |
+
|
| 843 |
+
const COLORS = ['#6366f1','#a855f7','#ec4899','#3b82f6'];
|
| 844 |
+
const N = 70;
|
| 845 |
+
|
| 846 |
+
function resize() {
|
| 847 |
+
W = canvas.width = window.innerWidth;
|
| 848 |
+
H = canvas.height = window.innerHeight;
|
| 849 |
+
}
|
| 850 |
+
|
| 851 |
+
function init() {
|
| 852 |
+
nodes = [];
|
| 853 |
+
for (let i = 0; i < N; i++) {
|
| 854 |
+
nodes.push({
|
| 855 |
+
x: Math.random() * W,
|
| 856 |
+
y: Math.random() * H,
|
| 857 |
+
vx: (Math.random() - 0.5) * 0.4,
|
| 858 |
+
vy: (Math.random() - 0.5) * 0.4,
|
| 859 |
+
r: Math.random() * 2 + 1.2,
|
| 860 |
+
color: COLORS[Math.floor(Math.random() * COLORS.length)]
|
| 861 |
+
});
|
| 862 |
+
}
|
| 863 |
+
}
|
| 864 |
+
|
| 865 |
+
function draw() {
|
| 866 |
+
ctx.clearRect(0, 0, W, H);
|
| 867 |
+
// Draw connections
|
| 868 |
+
for (let i = 0; i < nodes.length; i++) {
|
| 869 |
+
for (let j = i + 1; j < nodes.length; j++) {
|
| 870 |
+
const dx = nodes[i].x - nodes[j].x;
|
| 871 |
+
const dy = nodes[i].y - nodes[j].y;
|
| 872 |
+
const dist = Math.sqrt(dx*dx + dy*dy);
|
| 873 |
+
if (dist < 160) {
|
| 874 |
+
ctx.beginPath();
|
| 875 |
+
ctx.strokeStyle = `rgba(99,102,241,${0.18 * (1 - dist/160)})`;
|
| 876 |
+
ctx.lineWidth = 0.8;
|
| 877 |
+
ctx.moveTo(nodes[i].x, nodes[i].y);
|
| 878 |
+
ctx.lineTo(nodes[j].x, nodes[j].y);
|
| 879 |
+
ctx.stroke();
|
| 880 |
+
}
|
| 881 |
+
}
|
| 882 |
+
}
|
| 883 |
+
// Draw nodes
|
| 884 |
+
nodes.forEach(n => {
|
| 885 |
+
ctx.beginPath();
|
| 886 |
+
ctx.arc(n.x, n.y, n.r, 0, Math.PI * 2);
|
| 887 |
+
ctx.fillStyle = n.color;
|
| 888 |
+
ctx.fill();
|
| 889 |
+
});
|
| 890 |
+
}
|
| 891 |
+
|
| 892 |
+
function update() {
|
| 893 |
+
nodes.forEach(n => {
|
| 894 |
+
n.x += n.vx;
|
| 895 |
+
n.y += n.vy;
|
| 896 |
+
if (n.x < 0 || n.x > W) n.vx *= -1;
|
| 897 |
+
if (n.y < 0 || n.y > H) n.vy *= -1;
|
| 898 |
+
});
|
| 899 |
+
}
|
| 900 |
+
|
| 901 |
+
function loop() {
|
| 902 |
+
update();
|
| 903 |
+
draw();
|
| 904 |
+
RAF = requestAnimationFrame(loop);
|
| 905 |
+
}
|
| 906 |
+
|
| 907 |
+
resize();
|
| 908 |
+
init();
|
| 909 |
+
loop();
|
| 910 |
+
window.addEventListener('resize', () => { resize(); init(); });
|
| 911 |
+
})();
|
| 912 |
+
|
| 913 |
+
/* ── Scroll-reveal for pipeline steps ──────────────────── */
|
| 914 |
+
const observer = new IntersectionObserver((entries) => {
|
| 915 |
+
entries.forEach((e, i) => {
|
| 916 |
+
if (e.isIntersecting) {
|
| 917 |
+
e.target.style.animationDelay = (i * 0.12) + 's';
|
| 918 |
+
e.target.classList.add('visible');
|
| 919 |
+
}
|
| 920 |
+
});
|
| 921 |
+
}, { threshold: 0.15 });
|
| 922 |
+
|
| 923 |
+
document.querySelectorAll('.pipe-step, .feat-card, .module-card').forEach(el => {
|
| 924 |
+
el.style.opacity = '0';
|
| 925 |
+
el.style.transform = 'translateY(20px)';
|
| 926 |
+
el.style.transition = 'opacity 0.5s ease, transform 0.5s ease';
|
| 927 |
+
observer.observe(el);
|
| 928 |
+
});
|
| 929 |
+
|
| 930 |
+
// Use IntersectionObserver callback to trigger reveal
|
| 931 |
+
const revealObs = new IntersectionObserver((entries) => {
|
| 932 |
+
entries.forEach(e => {
|
| 933 |
+
if (e.isIntersecting) {
|
| 934 |
+
e.target.style.opacity = '1';
|
| 935 |
+
e.target.style.transform = 'translateY(0)';
|
| 936 |
+
}
|
| 937 |
+
});
|
| 938 |
+
}, { threshold: 0.1, rootMargin: '0px 0px -40px 0px' });
|
| 939 |
+
|
| 940 |
+
document.querySelectorAll('.pipe-step, .feat-card, .module-card').forEach(el => revealObs.observe(el));
|
| 941 |
+
|
| 942 |
+
/* ── Animated counter for hero stats ─────────────────────*/
|
| 943 |
+
function animateCount(el, target, suffix, duration = 1800) {
|
| 944 |
+
let start = null;
|
| 945 |
+
const step = (ts) => {
|
| 946 |
+
if (!start) start = ts;
|
| 947 |
+
const prog = Math.min((ts - start) / duration, 1);
|
| 948 |
+
const val = Math.floor(prog * target);
|
| 949 |
+
el.textContent = val.toLocaleString() + suffix;
|
| 950 |
+
if (prog < 1) requestAnimationFrame(step);
|
| 951 |
+
};
|
| 952 |
+
requestAnimationFrame(step);
|
| 953 |
+
}
|
| 954 |
+
const statObs = new IntersectionObserver((entries) => {
|
| 955 |
+
entries.forEach(e => {
|
| 956 |
+
if (e.isIntersecting) {
|
| 957 |
+
animateCount(document.getElementById('stat-customers'), 12500, '+');
|
| 958 |
+
statObs.disconnect();
|
| 959 |
+
}
|
| 960 |
+
});
|
| 961 |
+
}, { threshold: 0.5 });
|
| 962 |
+
const statEl = document.getElementById('stat-customers');
|
| 963 |
+
if (statEl) statObs.observe(statEl);
|
| 964 |
+
</script>
|
| 965 |
+
</body>
|
| 966 |
+
</html>
|
templates/network.html
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}Network Operations Command Center - TelecomIQ{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<h2 class="text-primary-custom mb-4">🗼 Network Operations Command Center</h2>
|
| 7 |
+
|
| 8 |
+
<div class="alert alert-success mb-4">
|
| 9 |
+
<strong>Real-Time Monitoring:</strong> Network health status across all regions
|
| 10 |
+
</div>
|
| 11 |
+
|
| 12 |
+
<!-- Network Health KPIs -->
|
| 13 |
+
<div class="row">
|
| 14 |
+
<div class="col-md-3">
|
| 15 |
+
<div class="metric-card">
|
| 16 |
+
<h6>CELL TOWERS
|
| 17 |
+
<button class="info-btn"
|
| 18 |
+
data-info-icon="🗼"
|
| 19 |
+
data-info-title="Total Cell Towers"
|
| 20 |
+
data-info-section="Network Operations · Infrastructure KPI"
|
| 21 |
+
data-info="The total number of active cell towers (base stations) in the network infrastructure. Each tower serves a geographic coverage cell and handles radio connections for nearby subscribers."
|
| 22 |
+
data-info-tips="More towers = better coverage density and capacity.|Typical urban deployment: 1 tower per 0.5–2 km².|High-utilisation towers (>90%) are candidates for capacity expansion or offloading.">ⓘ</button>
|
| 23 |
+
</h6>
|
| 24 |
+
<h3 class="text-primary-custom" id="kpi-towers">-</h3>
|
| 25 |
+
<small>Active infrastructure</small>
|
| 26 |
+
</div>
|
| 27 |
+
</div>
|
| 28 |
+
<div class="col-md-3">
|
| 29 |
+
<div class="metric-card">
|
| 30 |
+
<h6>NETWORK AVAILABILITY
|
| 31 |
+
<button class="info-btn"
|
| 32 |
+
data-info-icon="✅"
|
| 33 |
+
data-info-title="Network Availability"
|
| 34 |
+
data-info-section="Network Operations · Reliability KPI"
|
| 35 |
+
data-info="The percentage of time the network was fully operational across all towers. Measured as uptime ÷ total elapsed time. A core SLA metric for operator and enterprise contracts."
|
| 36 |
+
data-info-tips="Target: ≥99.9% (allows only ~53 minutes downtime/month).|99.5% = ~3.6 hours/month downtime — may trigger regulatory penalties.|Monitor per-region to identify geographic reliability hotspots.">ⓘ</button>
|
| 37 |
+
</h6>
|
| 38 |
+
<h3 class="text-success-custom" id="kpi-availability">-</h3>
|
| 39 |
+
<small>Target: 99.9%</small>
|
| 40 |
+
</div>
|
| 41 |
+
</div>
|
| 42 |
+
<div class="col-md-3">
|
| 43 |
+
<div class="metric-card">
|
| 44 |
+
<h6>AVG LATENCY
|
| 45 |
+
<button class="info-btn"
|
| 46 |
+
data-info-icon="⏱️"
|
| 47 |
+
data-info-title="Average Network Latency"
|
| 48 |
+
data-info-section="Network Operations · Performance KPI"
|
| 49 |
+
data-info="The mean round-trip time (in milliseconds) for a data packet to travel from subscriber to the network node and back. Lower is better — it directly impacts voice quality and data app responsiveness."
|
| 50 |
+
data-info-tips="4G LTE target latency: <50ms.|5G target: <10ms.|High latency (>100ms) causes noticeable voice degradation and poor video streaming.">ⓘ</button>
|
| 51 |
+
</h6>
|
| 52 |
+
<h3 class="text-warning-custom" id="kpi-latency">-</h3>
|
| 53 |
+
<small>Response time</small>
|
| 54 |
+
</div>
|
| 55 |
+
</div>
|
| 56 |
+
<div class="col-md-3">
|
| 57 |
+
<div class="metric-card">
|
| 58 |
+
<h6>THROUGHPUT
|
| 59 |
+
<button class="info-btn"
|
| 60 |
+
data-info-icon="🚀"
|
| 61 |
+
data-info-title="Average Network Throughput"
|
| 62 |
+
data-info-section="Network Operations · Capacity KPI"
|
| 63 |
+
data-info="The average data transfer rate across the network, measured in Megabits per second (Mbps). Represents the actual usable bandwidth being delivered to subscribers."
|
| 64 |
+
data-info-tips="4G LTE average: 20–50 Mbps.|5G mmWave peaks: 1,000+ Mbps.|Throughput drops during peak hours — monitor hourly patterns to plan capacity upgrades.">ⓘ</button>
|
| 65 |
+
</h6>
|
| 66 |
+
<h3 class="text-success-custom" id="kpi-throughput">-</h3>
|
| 67 |
+
<small>Network capacity</small>
|
| 68 |
+
</div>
|
| 69 |
+
</div>
|
| 70 |
+
</div>
|
| 71 |
+
|
| 72 |
+
<!-- Service Quality KPIs -->
|
| 73 |
+
<div class="row">
|
| 74 |
+
<div class="col-md-3">
|
| 75 |
+
<div class="metric-card">
|
| 76 |
+
<h6>UTILIZATION
|
| 77 |
+
<button class="info-btn"
|
| 78 |
+
data-info-icon="📶"
|
| 79 |
+
data-info-title="Bandwidth Utilization"
|
| 80 |
+
data-info-section="Network Operations · Capacity KPI"
|
| 81 |
+
data-info="The percentage of total available network bandwidth currently being consumed. High utilisation indicates the network is near capacity and may need expansion or traffic management."
|
| 82 |
+
data-info-tips="70–80% is the recommended operating range.|Above 90% risks congestion, packet loss, and QoS degradation.|Plan capacity upgrades when sustained utilisation exceeds 85%.">ⓘ</button>
|
| 83 |
+
</h6>
|
| 84 |
+
<h3 class="text-primary-custom" id="kpi-utilization">-</h3>
|
| 85 |
+
<small>Bandwidth usage</small>
|
| 86 |
+
</div>
|
| 87 |
+
</div>
|
| 88 |
+
<div class="col-md-3">
|
| 89 |
+
<div class="metric-card">
|
| 90 |
+
<h6>PACKET LOSS
|
| 91 |
+
<button class="info-btn"
|
| 92 |
+
data-info-icon="⚠️"
|
| 93 |
+
data-info-title="Average Packet Loss Rate"
|
| 94 |
+
data-info-section="Network Operations · Quality KPI"
|
| 95 |
+
data-info="The percentage of data packets that are lost in transit across the network. Packet loss causes retransmissions, reducing effective throughput and degrading real-time applications like voice calls and streaming."
|
| 96 |
+
data-info-tips="Acceptable threshold: <1% packet loss.|1–5%: Noticeable degradation of VoIP and streaming quality.|>5%: Severe network problems — investigate immediately.">ⓘ</button>
|
| 97 |
+
</h6>
|
| 98 |
+
<h3 class="text-danger-custom" id="kpi-packet-loss">-</h3>
|
| 99 |
+
<small>Average</small>
|
| 100 |
+
</div>
|
| 101 |
+
</div>
|
| 102 |
+
<div class="col-md-3">
|
| 103 |
+
<div class="metric-card">
|
| 104 |
+
<h6>HANDOVER SUCCESS
|
| 105 |
+
<button class="info-btn"
|
| 106 |
+
data-info-icon="🔄"
|
| 107 |
+
data-info-title="Handover Success Rate"
|
| 108 |
+
data-info-section="Network Operations · Mobility KPI"
|
| 109 |
+
data-info="The percentage of call/session handovers between cell towers that complete successfully without dropping the connection. A key mobility metric for subscribers moving between coverage zones."
|
| 110 |
+
data-info-tips="Target: ≥98% handover success rate.|Failed handovers cause dropped calls — a top driver of customer complaints.|Dense urban areas show higher handover frequency due to smaller cell sizes.">ⓘ</button>
|
| 111 |
+
</h6>
|
| 112 |
+
<h3 class="text-success-custom" id="kpi-handover">-</h3>
|
| 113 |
+
<small>Call continuity</small>
|
| 114 |
+
</div>
|
| 115 |
+
</div>
|
| 116 |
+
<div class="col-md-3">
|
| 117 |
+
<div class="metric-card">
|
| 118 |
+
<h6>ACTIVE USERS
|
| 119 |
+
<button class="info-btn"
|
| 120 |
+
data-info-icon="👤"
|
| 121 |
+
data-info-title="Active Users (Current Load)"
|
| 122 |
+
data-info-section="Network Operations · Load KPI"
|
| 123 |
+
data-info="The number of subscribers currently connected and actively using the network at this moment. This reflects real-time network load and is used for capacity planning."
|
| 124 |
+
data-info-tips="Compare to historical peak load for capacity buffer analysis.|Sudden drops may indicate a regional outage affecting connectivity.|Peak hours typically 8–10 AM and 6–9 PM local time.">ⓘ</button>
|
| 125 |
+
</h6>
|
| 126 |
+
<h3 class="text-primary-custom" id="kpi-active-users">-</h3>
|
| 127 |
+
<small>Current load</small>
|
| 128 |
+
</div>
|
| 129 |
+
</div>
|
| 130 |
+
</div>
|
| 131 |
+
|
| 132 |
+
<!-- Tower Performance Charts -->
|
| 133 |
+
<div class="row mt-4">
|
| 134 |
+
<div class="col-md-6">
|
| 135 |
+
<div class="chart-card">
|
| 136 |
+
<h5 class="text-primary-custom mb-3">📊 Tower Performance Distribution
|
| 137 |
+
<button class="info-btn"
|
| 138 |
+
data-info-icon="🗼"
|
| 139 |
+
data-info-title="Tower Performance Distribution"
|
| 140 |
+
data-info-section="Network Operations · Infrastructure Chart"
|
| 141 |
+
data-info="A bar chart categorising all cell towers into Excellent, Good, Fair, and Poor performance bands based on combined signal quality, throughput, and availability metrics."
|
| 142 |
+
data-info-tips="Aim for >70% of towers in Excellent or Good.|Fair towers should be scheduled for proactive maintenance.|Poor towers (<30% performance score) need urgent investigation.">ⓘ</button>
|
| 143 |
+
</h5>
|
| 144 |
+
<canvas id="towerPerformanceChart"></canvas>
|
| 145 |
+
</div>
|
| 146 |
+
</div>
|
| 147 |
+
<div class="col-md-6">
|
| 148 |
+
<div class="chart-card">
|
| 149 |
+
<h5 class="text-primary-custom mb-3">⚡ Network Traffic Patterns
|
| 150 |
+
<button class="info-btn"
|
| 151 |
+
data-info-icon="📈"
|
| 152 |
+
data-info-title="24-Hour Network Traffic Pattern"
|
| 153 |
+
data-info-section="Network Operations · Traffic Chart"
|
| 154 |
+
data-info="An hourly line chart showing network load (%) across a 24-hour period. Reveals peak usage times and periods of low demand useful for scheduling maintenance windows."
|
| 155 |
+
data-info-tips="Morning peak: 8–10 AM (commute + office start).|Evening peak: 6–9 PM (streaming, social media).|Maintenance windows should target 2–5 AM when load is lowest.">ⓘ</button>
|
| 156 |
+
</h5>
|
| 157 |
+
<canvas id="trafficPatternChart"></canvas>
|
| 158 |
+
</div>
|
| 159 |
+
</div>
|
| 160 |
+
</div>
|
| 161 |
+
|
| 162 |
+
<!-- Tower Status Table -->
|
| 163 |
+
<div class="row mt-4">
|
| 164 |
+
<div class="col-md-12">
|
| 165 |
+
<div class="chart-card">
|
| 166 |
+
<h5 class="text-primary-custom mb-3">🗼 Cell Tower Status & Maintenance
|
| 167 |
+
<button class="info-btn"
|
| 168 |
+
data-info-icon="📋"
|
| 169 |
+
data-info-title="Cell Tower Status Table"
|
| 170 |
+
data-info-section="Network Operations · Infrastructure Table"
|
| 171 |
+
data-info="A detailed table listing each cell tower with its operational status, current bandwidth utilisation, last maintenance date, and measured signal quality rating."
|
| 172 |
+
data-info-tips="Utilisation >90% = over-capacity, plan expansion or load balancing.|Maintenance gap >6 months = schedule preventive inspection.|Towers in 'Fair' quality with high utilisation are double-risk assets.">ⓘ</button>
|
| 173 |
+
</h5>
|
| 174 |
+
<div id="towerStatusTable"></div>
|
| 175 |
+
</div>
|
| 176 |
+
</div>
|
| 177 |
+
</div>
|
| 178 |
+
|
| 179 |
+
<!-- Network Incidents -->
|
| 180 |
+
<div class="row mt-4">
|
| 181 |
+
<div class="col-md-6">
|
| 182 |
+
<div class="chart-card">
|
| 183 |
+
<h5 class="text-primary-custom mb-3">🚨 Network Alerts & Incidents
|
| 184 |
+
<button class="info-btn"
|
| 185 |
+
data-info-icon="🚨"
|
| 186 |
+
data-info-title="Network Alerts & Incidents"
|
| 187 |
+
data-info-section="Network Operations · Incident Management"
|
| 188 |
+
data-info="Live feed of active network alerts, outages, and degradation events. Each incident shows affected tower, severity level, start time, and current resolution status."
|
| 189 |
+
data-info-tips="P1 (Critical): Full outage — resolve within 2 hours.|P2 (Major): Significant degradation — resolve within 4 hours.|P3 (Minor): Partial impact — resolve within 24 hours.">ⓘ</button>
|
| 190 |
+
</h5>
|
| 191 |
+
<div id="networkIncidents">
|
| 192 |
+
<div class="alert alert-success">
|
| 193 |
+
<strong>✓ All Systems Operational</strong><br>No active incidents detected
|
| 194 |
+
</div>
|
| 195 |
+
</div>
|
| 196 |
+
</div>
|
| 197 |
+
</div>
|
| 198 |
+
<div class="col-md-6">
|
| 199 |
+
<div class="chart-card">
|
| 200 |
+
<h5 class="text-primary-custom mb-3">🔧 Maintenance Schedule
|
| 201 |
+
<button class="info-btn"
|
| 202 |
+
data-info-icon="🔧"
|
| 203 |
+
data-info-title="Planned Maintenance Schedule"
|
| 204 |
+
data-info-section="Network Operations · Maintenance"
|
| 205 |
+
data-info="Upcoming scheduled maintenance windows for cell towers. Planned maintenance is proactively scheduled to prevent failures and ensure SLA compliance."
|
| 206 |
+
data-info-tips="Always schedule during low-traffic windows (2–5 AM).|Notify enterprise customers 48 hours in advance.|Post-maintenance: run automated performance tests before marking complete.">ⓘ</button>
|
| 207 |
+
</h5>
|
| 208 |
+
<div id="maintenanceSchedule"></div>
|
| 209 |
+
</div>
|
| 210 |
+
</div>
|
| 211 |
+
</div>
|
| 212 |
+
|
| 213 |
+
{% endblock %}
|
| 214 |
+
|
| 215 |
+
{% block extra_js %}
|
| 216 |
+
<script>
|
| 217 |
+
Chart.defaults.color = '#64748b';
|
| 218 |
+
Chart.defaults.borderColor = '#e2e8f0';
|
| 219 |
+
Chart.defaults.font.family = "'Inter', sans-serif";
|
| 220 |
+
const COLORS = { primary: '#4f46e5', success: '#10b981', warning: '#f59e0b', danger: '#ef4444' };
|
| 221 |
+
|
| 222 |
+
async function loadNetworkKPIs() {
|
| 223 |
+
try {
|
| 224 |
+
const response = await fetch('/api/network/comprehensive-kpis');
|
| 225 |
+
const data = await response.json();
|
| 226 |
+
document.getElementById('kpi-towers').textContent = data.cell_towers.toLocaleString();
|
| 227 |
+
document.getElementById('kpi-availability').textContent = `${data.network_availability}%`;
|
| 228 |
+
document.getElementById('kpi-latency').textContent = `${data.avg_latency} ms`;
|
| 229 |
+
document.getElementById('kpi-throughput').textContent = `${data.avg_throughput} Mbps`;
|
| 230 |
+
document.getElementById('kpi-utilization').textContent = `${data.avg_utilization}%`;
|
| 231 |
+
document.getElementById('kpi-packet-loss').textContent = `${data.avg_packet_loss}%`;
|
| 232 |
+
document.getElementById('kpi-handover').textContent = `${data.handover_success}%`;
|
| 233 |
+
document.getElementById('kpi-active-users').textContent = data.active_users.toLocaleString();
|
| 234 |
+
} catch (error) { console.error('Error loading network KPIs:', error); loadFallbackKPIs(); }
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
function loadFallbackKPIs() {
|
| 238 |
+
fetch('/api/network/kpis').then(r => r.json()).then(data => {
|
| 239 |
+
document.getElementById('kpi-towers').textContent = data.cell_towers.toLocaleString();
|
| 240 |
+
document.getElementById('kpi-latency').textContent = `${data.avg_latency} ms`;
|
| 241 |
+
document.getElementById('kpi-throughput').textContent = `${data.avg_throughput} Mbps`;
|
| 242 |
+
document.getElementById('kpi-utilization').textContent = `${data.avg_utilization}%`;
|
| 243 |
+
document.getElementById('kpi-availability').textContent = '99.7%';
|
| 244 |
+
document.getElementById('kpi-packet-loss').textContent = '0.8%';
|
| 245 |
+
document.getElementById('kpi-handover').textContent = '98.5%';
|
| 246 |
+
document.getElementById('kpi-active-users').textContent = '15,234';
|
| 247 |
+
});
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
function loadTowerPerformanceChart() {
|
| 251 |
+
const ctx = document.getElementById('towerPerformanceChart').getContext('2d');
|
| 252 |
+
new Chart(ctx, { type: 'bar', data: { labels: ['Excellent','Good','Fair','Poor'], datasets: [{ label: 'Number of Towers', data: [420,385,165,30], backgroundColor: [COLORS.success,COLORS.primary,COLORS.warning,COLORS.danger], borderRadius: 8 }] }, options: { responsive: true, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, grid: { color: '#f1f5f9' } } } } });
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
function loadTrafficPatternChart() {
|
| 256 |
+
const ctx = document.getElementById('trafficPatternChart').getContext('2d');
|
| 257 |
+
const hours = Array.from({length: 24}, (_, i) => `${i}:00`);
|
| 258 |
+
const traffic = hours.map((_, i) => i>=9&&i<=11?Math.random()*30+70:i>=17&&i<=21?Math.random()*35+75:i>=0&&i<=6?Math.random()*20+20:Math.random()*25+45);
|
| 259 |
+
new Chart(ctx, { type: 'line', data: { labels: hours, datasets: [{ label: 'Network Load (%)', data: traffic, borderColor: COLORS.primary, backgroundColor: 'rgba(79,70,229,0.1)', fill: true, tension: 0.4 }] }, options: { responsive: true, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, max: 100, grid: { color: '#f1f5f9' } } } } });
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
function loadTowerStatusTable() {
|
| 263 |
+
const towers = [
|
| 264 |
+
{ id: 'TOWER00001', city: 'New York', status: 'Active', utilization: '75%', lastMaintenance: '2024-11-15', quality: 'Excellent' },
|
| 265 |
+
{ id: 'TOWER00002', city: 'Los Angeles', status: 'Active', utilization: '82%', lastMaintenance: '2024-10-20', quality: 'Good' },
|
| 266 |
+
{ id: 'TOWER00003', city: 'Chicago', status: 'Active', utilization: '68%', lastMaintenance: '2024-12-01', quality: 'Excellent' },
|
| 267 |
+
{ id: 'TOWER00004', city: 'Houston', status: 'Maintenance', utilization: '0%', lastMaintenance: '2024-12-10', quality: 'N/A' },
|
| 268 |
+
{ id: 'TOWER00005', city: 'Phoenix', status: 'Active', utilization: '91%', lastMaintenance: '2024-09-15', quality: 'Fair' }
|
| 269 |
+
];
|
| 270 |
+
let html = '<div class="table-responsive"><table class="table table-dark table-striped table-hover"><thead><tr><th>Tower ID</th><th>City</th><th>Status</th><th>Utilization</th><th>Last Maintenance</th><th>Quality</th></tr></thead><tbody>';
|
| 271 |
+
towers.forEach(t => { const b = t.status === 'Active' ? 'badge-success-custom' : 'badge-warning-custom'; html += `<tr><td>${t.id}</td><td>${t.city}</td><td><span class="badge ${b}">${t.status}</span></td><td>${t.utilization}</td><td>${t.lastMaintenance}</td><td>${t.quality}</td></tr>`; });
|
| 272 |
+
html += '</tbody></table></div>';
|
| 273 |
+
document.getElementById('towerStatusTable').innerHTML = html;
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
function loadMaintenanceSchedule() {
|
| 277 |
+
const maintenance = [{ date: '2024-12-15', tower: 'TOWER00012', type: 'Routine Inspection' }, { date: '2024-12-18', tower: 'TOWER00027', type: 'Equipment Upgrade' }, { date: '2024-12-22', tower: 'TOWER00045', type: 'Preventive Maintenance' }];
|
| 278 |
+
let html = '<ul class="list-group">';
|
| 279 |
+
maintenance.forEach(item => { html += `<li class="list-group-item" style="background:var(--bg-card);border-color:var(--border-color);"><strong>${item.date}</strong> - ${item.tower}<br><small class="text-muted">${item.type}</small></li>`; });
|
| 280 |
+
html += '</ul>';
|
| 281 |
+
document.getElementById('maintenanceSchedule').innerHTML = html;
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
document.addEventListener('DOMContentLoaded', function() { loadNetworkKPIs(); loadTowerPerformanceChart(); loadTrafficPatternChart(); loadTowerStatusTable(); loadMaintenanceSchedule(); });
|
| 285 |
+
</script>
|
| 286 |
+
{% endblock %}
|
templates/network_enhanced.html
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}Network Operations Command Center - TelecomIQ{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<h2 class="text-primary-custom mb-4">🗼 Network Operations Command Center</h2>
|
| 7 |
+
|
| 8 |
+
<div class="alert alert-success mb-4">
|
| 9 |
+
<strong>Real-Time Monitoring:</strong> Network health status across all regions
|
| 10 |
+
</div>
|
| 11 |
+
|
| 12 |
+
<!-- Network Health KPIs -->
|
| 13 |
+
<div class="row">
|
| 14 |
+
<div class="col-md-3">
|
| 15 |
+
<div class="metric-card">
|
| 16 |
+
<h6>CELL TOWERS</h6>
|
| 17 |
+
<h3 class="text-primary-custom" id="kpi-towers">-</h3>
|
| 18 |
+
<small>Active infrastructure</small>
|
| 19 |
+
</div>
|
| 20 |
+
</div>
|
| 21 |
+
<div class="col-md-3">
|
| 22 |
+
<div class="metric-card">
|
| 23 |
+
<h6>NETWORK AVAILABILITY</h6>
|
| 24 |
+
<h3 class="text-success-custom" id="kpi-availability">-</h3>
|
| 25 |
+
<small>Target: 99.9%</small>
|
| 26 |
+
</div>
|
| 27 |
+
</div>
|
| 28 |
+
<div class="col-md-3">
|
| 29 |
+
<div class="metric-card">
|
| 30 |
+
<h6>AVG LATENCY</h6>
|
| 31 |
+
<h3 class="text-warning-custom" id="kpi-latency">-</h3>
|
| 32 |
+
<small>Response time</small>
|
| 33 |
+
</div>
|
| 34 |
+
</div>
|
| 35 |
+
<div class="col-md-3">
|
| 36 |
+
<div class="metric-card">
|
| 37 |
+
<h6>THROUGHPUT</h6>
|
| 38 |
+
<h3 class="text-success-custom" id="kpi-throughput">-</h3>
|
| 39 |
+
<small>Network capacity</small>
|
| 40 |
+
</div>
|
| 41 |
+
</div>
|
| 42 |
+
</div>
|
| 43 |
+
|
| 44 |
+
<!-- Service Quality KPIs -->
|
| 45 |
+
<div class="row">
|
| 46 |
+
<div class="col-md-3">
|
| 47 |
+
<div class="metric-card">
|
| 48 |
+
<h6>UTILIZATION</h6>
|
| 49 |
+
<h3 class="text-primary-custom" id="kpi-utilization">-</h3>
|
| 50 |
+
<small>Bandwidth usage</small>
|
| 51 |
+
</div>
|
| 52 |
+
</div>
|
| 53 |
+
<div class="col-md-3">
|
| 54 |
+
<div class="metric-card">
|
| 55 |
+
<h6>PACKET LOSS</h6>
|
| 56 |
+
<h3 class="text-danger-custom" id="kpi-packet-loss">-</h3>
|
| 57 |
+
<small>Average</small>
|
| 58 |
+
</div>
|
| 59 |
+
</div>
|
| 60 |
+
<div class="col-md-3">
|
| 61 |
+
<div class="metric-card">
|
| 62 |
+
<h6>HANDOVER SUCCESS</h6>
|
| 63 |
+
<h3 class="text-success-custom" id="kpi-handover">-</h3>
|
| 64 |
+
<small>Call continuity</small>
|
| 65 |
+
</div>
|
| 66 |
+
</div>
|
| 67 |
+
<div class="col-md-3">
|
| 68 |
+
<div class="metric-card">
|
| 69 |
+
<h6>ACTIVE USERS</h6>
|
| 70 |
+
<h3 class="text-primary-custom" id="kpi-active-users">-</h3>
|
| 71 |
+
<small>Current load</small>
|
| 72 |
+
</div>
|
| 73 |
+
</div>
|
| 74 |
+
</div>
|
| 75 |
+
|
| 76 |
+
<!-- Tower Performance Charts -->
|
| 77 |
+
<div class="row mt-4">
|
| 78 |
+
<div class="col-md-6">
|
| 79 |
+
<div class="chart-card">
|
| 80 |
+
<h5 class="text-primary-custom mb-3">📊 Tower Performance Distribution</h5>
|
| 81 |
+
<canvas id="towerPerformanceChart"></canvas>
|
| 82 |
+
</div>
|
| 83 |
+
</div>
|
| 84 |
+
<div class="col-md-6">
|
| 85 |
+
<div class="chart-card">
|
| 86 |
+
<h5 class="text-primary-custom mb-3">⚡ Network Traffic Patterns</h5>
|
| 87 |
+
<canvas id="trafficPatternChart"></canvas>
|
| 88 |
+
</div>
|
| 89 |
+
</div>
|
| 90 |
+
</div>
|
| 91 |
+
|
| 92 |
+
<!-- Tower Status Table -->
|
| 93 |
+
<div class="row mt-4">
|
| 94 |
+
<div class="col-md-12">
|
| 95 |
+
<div class="chart-card">
|
| 96 |
+
<h5 class="text-primary-custom mb-3">🗼 Cell Tower Status & Maintenance</h5>
|
| 97 |
+
<div id="towerStatusTable"></div>
|
| 98 |
+
</div>
|
| 99 |
+
</div>
|
| 100 |
+
</div>
|
| 101 |
+
|
| 102 |
+
<!-- Network Incidents -->
|
| 103 |
+
<div class="row mt-4">
|
| 104 |
+
<div class="col-md-6">
|
| 105 |
+
<div class="chart-card">
|
| 106 |
+
<h5 class="text-primary-custom mb-3">🚨 Network Alerts & Incidents</h5>
|
| 107 |
+
<div id="networkIncidents">
|
| 108 |
+
<div class="alert alert-success">
|
| 109 |
+
<strong>✓ All Systems Operational</strong><br>
|
| 110 |
+
No active incidents detected
|
| 111 |
+
</div>
|
| 112 |
+
</div>
|
| 113 |
+
</div>
|
| 114 |
+
</div>
|
| 115 |
+
<div class="col-md-6">
|
| 116 |
+
<div class="chart-card">
|
| 117 |
+
<h5 class="text-primary-custom mb-3">🔧 Maintenance Schedule</h5>
|
| 118 |
+
<div id="maintenanceSchedule"></div>
|
| 119 |
+
</div>
|
| 120 |
+
</div>
|
| 121 |
+
</div>
|
| 122 |
+
|
| 123 |
+
{% endblock %}
|
| 124 |
+
|
| 125 |
+
{% block extra_js %}
|
| 126 |
+
<script>
|
| 127 |
+
Chart.defaults.color = '#64748b';
|
| 128 |
+
Chart.defaults.borderColor = '#e2e8f0';
|
| 129 |
+
Chart.defaults.font.family = "'Inter', sans-serif";
|
| 130 |
+
|
| 131 |
+
const COLORS = {
|
| 132 |
+
primary: '#4f46e5',
|
| 133 |
+
success: '#10b981',
|
| 134 |
+
warning: '#f59e0b',
|
| 135 |
+
danger: '#ef4444'
|
| 136 |
+
};
|
| 137 |
+
|
| 138 |
+
async function loadNetworkKPIs() {
|
| 139 |
+
try {
|
| 140 |
+
const response = await fetch('/api/network/comprehensive-kpis');
|
| 141 |
+
const data = await response.json();
|
| 142 |
+
|
| 143 |
+
document.getElementById('kpi-towers').textContent = data.cell_towers.toLocaleString();
|
| 144 |
+
document.getElementById('kpi-availability').textContent = `${data.network_availability}%`;
|
| 145 |
+
document.getElementById('kpi-latency').textContent = `${data.avg_latency} ms`;
|
| 146 |
+
document.getElementById('kpi-throughput').textContent = `${data.avg_throughput} Mbps`;
|
| 147 |
+
document.getElementById('kpi-utilization').textContent = `${data.avg_utilization}%`;
|
| 148 |
+
document.getElementById('kpi-packet-loss').textContent = `${data.avg_packet_loss}%`;
|
| 149 |
+
document.getElementById('kpi-handover').textContent = `${data.handover_success}%`;
|
| 150 |
+
document.getElementById('kpi-active-users').textContent = data.active_users.toLocaleString();
|
| 151 |
+
} catch (error) {
|
| 152 |
+
console.error('Error loading network KPIs:', error);
|
| 153 |
+
// Use fallback data
|
| 154 |
+
loadFallbackKPIs();
|
| 155 |
+
}
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
function loadFallbackKPIs() {
|
| 159 |
+
fetch('/api/network/kpis')
|
| 160 |
+
.then(response => response.json())
|
| 161 |
+
.then(data => {
|
| 162 |
+
document.getElementById('kpi-towers').textContent = data.cell_towers.toLocaleString();
|
| 163 |
+
document.getElementById('kpi-latency').textContent = `${data.avg_latency} ms`;
|
| 164 |
+
document.getElementById('kpi-throughput').textContent = `${data.avg_throughput} Mbps`;
|
| 165 |
+
document.getElementById('kpi-utilization').textContent = `${data.avg_utilization}%`;
|
| 166 |
+
// Set default values for others
|
| 167 |
+
document.getElementById('kpi-availability').textContent = '99.7%';
|
| 168 |
+
document.getElementById('kpi-packet-loss').textContent = '0.8%';
|
| 169 |
+
document.getElementById('kpi-handover').textContent = '98.5%';
|
| 170 |
+
document.getElementById('kpi-active-users').textContent = '15,234';
|
| 171 |
+
});
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
// Tower Performance Chart
|
| 175 |
+
function loadTowerPerformanceChart() {
|
| 176 |
+
const ctx = document.getElementById('towerPerformanceChart').getContext('2d');
|
| 177 |
+
new Chart(ctx, {
|
| 178 |
+
type: 'bar',
|
| 179 |
+
data: {
|
| 180 |
+
labels: ['Excellent', 'Good', 'Fair', 'Poor'],
|
| 181 |
+
datasets: [{
|
| 182 |
+
label: 'Number of Towers',
|
| 183 |
+
data: [420, 385, 165, 30],
|
| 184 |
+
backgroundColor: [COLORS.success, COLORS.primary, COLORS.warning, COLORS.danger],
|
| 185 |
+
borderRadius: 8
|
| 186 |
+
}]
|
| 187 |
+
},
|
| 188 |
+
options: {
|
| 189 |
+
responsive: true,
|
| 190 |
+
plugins: {
|
| 191 |
+
legend: { display: false }
|
| 192 |
+
},
|
| 193 |
+
scales: {
|
| 194 |
+
y: {
|
| 195 |
+
beginAtZero: true,
|
| 196 |
+
grid: { color: '#f1f5f9' }
|
| 197 |
+
}
|
| 198 |
+
}
|
| 199 |
+
}
|
| 200 |
+
});
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
// Traffic Pattern Chart
|
| 204 |
+
function loadTrafficPatternChart() {
|
| 205 |
+
const ctx = document.getElementById('trafficPatternChart').getContext('2d');
|
| 206 |
+
const hours = Array.from({length: 24}, (_, i) => `${i}:00`);
|
| 207 |
+
const traffic = hours.map((_, i) => {
|
| 208 |
+
if (i >= 9 && i <= 11) return Math.random() * 30 + 70; // Morning peak
|
| 209 |
+
if (i >= 17 && i <= 21) return Math.random() * 35 + 75; // Evening peak
|
| 210 |
+
if (i >= 0 && i <= 6) return Math.random() * 20 + 20; // Night
|
| 211 |
+
return Math.random() * 25 + 45; // Regular hours
|
| 212 |
+
});
|
| 213 |
+
|
| 214 |
+
new Chart(ctx, {
|
| 215 |
+
type: 'line',
|
| 216 |
+
data: {
|
| 217 |
+
labels: hours,
|
| 218 |
+
datasets: [{
|
| 219 |
+
label: 'Network Load (%)',
|
| 220 |
+
data: traffic,
|
| 221 |
+
borderColor: COLORS.primary,
|
| 222 |
+
backgroundColor: 'rgba(79, 70, 229, 0.1)',
|
| 223 |
+
fill: true,
|
| 224 |
+
tension: 0.4
|
| 225 |
+
}]
|
| 226 |
+
},
|
| 227 |
+
options: {
|
| 228 |
+
responsive: true,
|
| 229 |
+
plugins: {
|
| 230 |
+
legend: { display: false }
|
| 231 |
+
},
|
| 232 |
+
scales: {
|
| 233 |
+
y: {
|
| 234 |
+
beginAtZero: true,
|
| 235 |
+
max: 100,
|
| 236 |
+
grid: { color: '#f1f5f9' }
|
| 237 |
+
}
|
| 238 |
+
}
|
| 239 |
+
}
|
| 240 |
+
});
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
// Tower Status Table
|
| 244 |
+
function loadTowerStatusTable() {
|
| 245 |
+
const towers = [
|
| 246 |
+
{ id: 'TOWER00001', city: 'New York', status: 'Active', utilization: '75%', lastMaintenance: '2024-11-15', quality: 'Excellent' },
|
| 247 |
+
{ id: 'TOWER00002', city: 'Los Angeles', status: 'Active', utilization: '82%', lastMaintenance: '2024-10-20', quality: 'Good' },
|
| 248 |
+
{ id: 'TOWER00003', city: 'Chicago', status: 'Active', utilization: '68%', lastMaintenance: '2024-12-01', quality: 'Excellent' },
|
| 249 |
+
{ id: 'TOWER00004', city: 'Houston', status: 'Maintenance', utilization: '0%', lastMaintenance: '2024-12-10', quality: 'N/A' },
|
| 250 |
+
{ id: 'TOWER00005', city: 'Phoenix', status: 'Active', utilization: '91%', lastMaintenance: '2024-09-15', quality: 'Fair' }
|
| 251 |
+
];
|
| 252 |
+
|
| 253 |
+
let html = '<div class="table-responsive"><table class="table table-dark table-striped table-hover">';
|
| 254 |
+
html += '<thead><tr><th>Tower ID</th><th>City</th><th>Status</th><th>Utilization</th><th>Last Maintenance</th><th>Quality</th></tr></thead><tbody>';
|
| 255 |
+
|
| 256 |
+
towers.forEach(tower => {
|
| 257 |
+
const statusBadge = tower.status === 'Active' ? 'badge-success-custom' : 'badge-warning-custom';
|
| 258 |
+
html += `<tr>
|
| 259 |
+
<td>${tower.id}</td>
|
| 260 |
+
<td>${tower.city}</td>
|
| 261 |
+
<td><span class="badge ${statusBadge}">${tower.status}</span></td>
|
| 262 |
+
<td>${tower.utilization}</td>
|
| 263 |
+
<td>${tower.lastMaintenance}</td>
|
| 264 |
+
<td>${tower.quality}</td>
|
| 265 |
+
</tr>`;
|
| 266 |
+
});
|
| 267 |
+
|
| 268 |
+
html += '</tbody></table></div>';
|
| 269 |
+
document.getElementById('towerStatusTable').innerHTML = html;
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
// Maintenance Schedule
|
| 273 |
+
function loadMaintenanceSchedule() {
|
| 274 |
+
const maintenance = [
|
| 275 |
+
{ date: '2024-12-15', tower: 'TOWER00012', type: 'Routine Inspection' },
|
| 276 |
+
{ date: '2024-12-18', tower: 'TOWER00027', type: 'Equipment Upgrade' },
|
| 277 |
+
{ date: '2024-12-22', tower: 'TOWER00045', type: 'Preventive Maintenance' }
|
| 278 |
+
];
|
| 279 |
+
|
| 280 |
+
let html = '<ul class="list-group">';
|
| 281 |
+
maintenance.forEach(item => {
|
| 282 |
+
html += `<li class="list-group-item" style="background: var(--bg-card); border-color: var(--border-color);">
|
| 283 |
+
<strong>${item.date}</strong> - ${item.tower}<br>
|
| 284 |
+
<small class="text-muted">${item.type}</small>
|
| 285 |
+
</li>`;
|
| 286 |
+
});
|
| 287 |
+
html += '</ul>';
|
| 288 |
+
document.getElementById('maintenanceSchedule').innerHTML = html;
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 292 |
+
loadNetworkKPIs();
|
| 293 |
+
loadTowerPerformanceChart();
|
| 294 |
+
loadTrafficPatternChart();
|
| 295 |
+
loadTowerStatusTable();
|
| 296 |
+
loadMaintenanceSchedule();
|
| 297 |
+
});
|
| 298 |
+
</script>
|
| 299 |
+
{% endblock %}
|
templates/predictions.html
ADDED
|
@@ -0,0 +1,906 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}ML Predictions Suite - TelecomIQ{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block extra_css %}
|
| 6 |
+
<style>
|
| 7 |
+
.model-tabs .nav-link { color: #000000 !important; font-weight: 600; font-size: 0.8rem; padding: 10px 16px !important; border: none; border-bottom: 3px solid transparent; text-transform: uppercase; letter-spacing: 0.5px; background: transparent !important; }
|
| 8 |
+
.model-tabs .nav-link.active { color: var(--primary-color) !important; border-bottom-color: var(--primary-color); background: transparent !important; }
|
| 9 |
+
.model-tabs .nav-link:hover { color: var(--primary-color) !important; background: rgba(79,70,229,0.05) !important; }
|
| 10 |
+
.model-card { background: var(--bg-card); border-radius: 12px; border: 1px solid var(--border-color); padding: 20px; margin-bottom: 16px; }
|
| 11 |
+
.model-card h5 { font-weight: 700; color: var(--primary-color); margin-bottom: 16px; }
|
| 12 |
+
.model-metric { text-align: center; padding: 12px; border-radius: 8px; background: #f8fafc; }
|
| 13 |
+
.model-metric .value { font-size: 1.5rem; font-weight: 800; }
|
| 14 |
+
.model-metric .label { font-size: 0.75rem; color: var(--text-secondary); text-transform: uppercase; }
|
| 15 |
+
.shap-bar { height: 20px; border-radius: 4px; margin-bottom: 6px; display: flex; align-items: center; }
|
| 16 |
+
.shap-label { font-size: 0.8rem; min-width: 140px; }
|
| 17 |
+
.shap-value { font-size: 0.75rem; font-weight: 600; margin-left: 8px; }
|
| 18 |
+
.prediction-table { width: 100%; border-collapse: separate; border-spacing: 0; }
|
| 19 |
+
.prediction-table th { background: var(--primary-color); color: white; padding: 10px 14px; font-size: 0.75rem; text-transform: uppercase; }
|
| 20 |
+
.prediction-table td { padding: 8px 14px; border-bottom: 1px solid var(--border-color); font-size: 0.85rem; }
|
| 21 |
+
.prediction-table tr:hover { background: #f1f5f9; }
|
| 22 |
+
.badge-model { padding: 4px 10px; border-radius: 20px; font-size: 0.7rem; font-weight: 600; }
|
| 23 |
+
.badge-high { background: #fecaca; color: #991b1b; }
|
| 24 |
+
.badge-medium { background: #fef3c7; color: #92400e; }
|
| 25 |
+
.badge-low { background: #dcfce7; color: #166534; }
|
| 26 |
+
.anomaly-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; margin-right: 6px; }
|
| 27 |
+
.pred-result-card { border-radius: 10px; padding: 16px; background: linear-gradient(135deg, #f8fafc 0%, #eef2ff 100%); border: 1px solid #e0e7ff; }
|
| 28 |
+
.pred-result-card h3 { font-size: 1.8rem; font-weight: 800; margin: 0; }
|
| 29 |
+
.pred-result-card small { font-size: 0.75rem; color: #64748b; text-transform: uppercase; letter-spacing: 0.5px; }
|
| 30 |
+
</style>
|
| 31 |
+
{% endblock %}
|
| 32 |
+
|
| 33 |
+
{% block content %}
|
| 34 |
+
<h2 class="text-primary-custom mb-3">ML Predictions Suite</h2>
|
| 35 |
+
<p class="text-muted mb-4">11 Machine Learning Models | Feature Importance & Explainability | Manual Prediction on Custom Data</p>
|
| 36 |
+
|
| 37 |
+
<ul class="nav model-tabs mb-4" id="modelTabs" role="tablist">
|
| 38 |
+
<li class="nav-item"><a class="nav-link active" data-bs-toggle="tab" href="#tab-churn">A. Churn <button class="info-btn" data-info-icon="🤖" data-info-title="Churn Prediction Model" data-info-section="ML Suite · Tab A" data-info="A Gradient Boosting classifier that predicts the probability of each customer churning. Trained on 100,000 customers with features like tenure, ARPU, data usage, CSAT, and service calls." data-info-tips="Precision = % of flagged-as-churn that actually churn.|Recall = % of actual churners that the model caught.|Optimise recall to maximise customer saves; optimise precision to reduce wasted retention spend.">ⓘ</button></a></li>
|
| 39 |
+
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-network">B. Network <button class="info-btn" data-info-icon="📶" data-info-title="Network Performance Prediction" data-info-section="ML Suite · Tab B" data-info="A regression model predicting network latency and congestion levels for each tower. Uses tower utilisation, active users, time-of-day, and bandwidth to forecast near-term network performance." data-info-tips="MAE = average prediction error in milliseconds.|R-squared = how much variance the model explains (closer to 1 is better).|Use forecasts to pre-emptively adjust traffic routing before congestion peaks.">ⓘ</button></a></li>
|
| 40 |
+
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-ltv">C. LTV <button class="info-btn" data-info-icon="💰" data-info-title="Customer Lifetime Value (LTV) Prediction" data-info-section="ML Suite · Tab C" data-info="A Gradient Boosted Regressor predicting the total revenue a customer will generate over their relationship lifetime. Key inputs: tenure, ARPU, plan type, referrals, device age." data-info-tips="High-LTV customers should receive premium retention investment.|LTV/CAC ratio >3x means each customer generates 3x what it cost to acquire them.|Use LTV tiers to segment and prioritise marketing budgets.">ⓘ</button></a></li>
|
| 41 |
+
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-quality">D. Quality <button class="info-btn" data-info-icon="⭐" data-info-title="Service Quality Impact Model" data-info-section="ML Suite · Tab D" data-info="A classifier predicting whether a customer will file a quality complaint based on network metrics. Inputs: download/upload speed, call drop rate, network reliability, coverage, and MOS score." data-info-tips="Predicted complaints help prioritise network fixes before customers escalate.|Use probability threshold to trigger proactive outreach.|High complaint probability + High churn risk = immediate intervention needed.">ⓘ</button></a></li>
|
| 42 |
+
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-capacity">E. Capacity <button class="info-btn" data-info-icon="📊" data-info-title="Network Capacity Planning Model" data-info-section="ML Suite · Tab E" data-info="A time-series forecasting model predicting bandwidth demand over the next 30 days per tower. Uses seasonal patterns, historical utilisation, subscriber growth trends, and day-of-week effects." data-info-tips="Use forecasts to pre-order hardware capacity 60–90 days in advance.|At-risk towers (>90% utilisation in forecast) need immediate capacity action.|Seasonal demand patterns help plan network topup for holidays and events.">ⓘ</button></a></li>
|
| 43 |
+
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-demand">F. Demand <button class="info-btn" data-info-icon="📞" data-info-title="Service Demand / Call Volume Forecasting" data-info-section="ML Suite · Tab F" data-info="A call-centre demand forecasting model that predicts daily and hourly service contact volumes, and the optimal staffing level needed to meet SLA targets." data-info-tips="Use 14-day forecast to schedule agent rosters in advance.|Over-staffing by >20% = unnecessary cost; Under-staffing = SLA breach.|Marketing campaigns and holidays create predictable demand spikes.">ⓘ</button></a></li>
|
| 44 |
+
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-offers">G. Offers <button class="info-btn" data-info-icon="🎯" data-info-title="Personalised Offer Recommendation Engine" data-info-section="ML Suite · Tab G" data-info="A multi-class classifier that recommends the best retention or upsell offer for each customer based on their churn risk, plan, spending pattern, and satisfaction score." data-info-tips="Targeted offers have 3–5x higher conversion vs. generic blanket discounts.|High-risk + High-LTV customers should get the most generous offers.|Track offer redemption rate to continuously improve recommendation accuracy.">ⓘ</button></a></li>
|
| 45 |
+
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-anomaly">H. Anomaly <button class="info-btn" data-info-icon="🚫" data-info-title="Network Anomaly Detection" data-info-section="ML Suite · Tab H" data-info="An Isolation Forest model that detects statistically abnormal behaviour in network metrics (latency, throughput, packet loss, CPU, temperature) per tower. Flags anomalies before they cause outages." data-info-tips="False positive rate <10% is needed to avoid alert fatigue.|Critical anomalies (score >90) require immediate NOC investigation.|Correlate anomaly timing with maintenance logs to find recurring hardware issues.">ⓘ</button></a></li>
|
| 46 |
+
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-sentiment">I. Sentiment <button class="info-btn" data-info-icon="💬" data-info-title="Customer Sentiment Analysis" data-info-section="ML Suite · Tab I" data-info="An NLP-based sentiment classifier that analyses the tone of customer service interactions (calls, chats, emails) — categorising them as Positive, Neutral, or Negative. Also classifies the complaint type." data-info-tips="High negative sentiment across a specific complaint type = systemic issue.|Combine sentiment score with CSAT to identify cases where scores diverge.|Use real-time sentiment to trigger supervisor escalation on live calls.">ⓘ</button></a></li>
|
| 47 |
+
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-device">J. Device <button class="info-btn" data-info-icon="📱" data-info-title="Device Upgrade Propensity Model" data-info-section="ML Suite · Tab J" data-info="A classifier predicting the likelihood that a customer will upgrade their device in the next 90 days. Uses device age, manufacturer, plan type, tenure, and data usage as features." data-info-tips="Customers with devices >24 months old are prime upgrade targets.|Combine upgrade propensity with plan upsell model for bundled offers.|Device upgrade campaigns generate 2–3x revenue uplift vs. plan-only campaigns.">ⓘ</button></a></li>
|
| 48 |
+
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-invest">K. Investment <button class="info-btn" data-info-icon="💸" data-info-title="Network Investment ROI Prediction" data-info-section="ML Suite · Tab K" data-info="A regression model predicting the 5-year ROI of network infrastructure investments (new towers, upgrades) in given cities. Factors in population, subscriber density, ARPU, and growth rate." data-info-tips="Prioritise cities where ROI model shows >150% 5-year return.|Balance ROI with coverage obligations (rural expansion may have lower ROI but regulatory requirement).|Update model annually as new subscriber and revenue data becomes available.">ⓘ</button></a></li>
|
| 49 |
+
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-shap">SHAP <button class="info-btn" data-info-icon="🔍" data-info-title="SHAP Explainability" data-info-section="ML Suite · Explainability Tab" data-info="SHAP (SHapley Additive exPlanations) provides model-agnostic explanations for each prediction. Shows which features push the prediction higher or lower, making black-box ML models interpretable and auditable." data-info-tips="Positive SHAP value = feature increases prediction (e.g., increases churn probability).|Negative SHAP value = feature decreases prediction.|Use SHAP for regulatory compliance, model auditing, and building trust in ML outputs.">ⓘ</button></a></li>
|
| 50 |
+
</ul>
|
| 51 |
+
|
| 52 |
+
<div class="tab-content" id="modelTabContent">
|
| 53 |
+
|
| 54 |
+
<!-- ============ TAB A: CHURN PREDICTION ============ -->
|
| 55 |
+
<div class="tab-pane fade show active" id="tab-churn">
|
| 56 |
+
<div class="row">
|
| 57 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-success-custom" id="churn-accuracy">-</div><div class="label">Accuracy</div></div></div>
|
| 58 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-primary-custom" id="churn-precision">-</div><div class="label">Precision</div></div></div>
|
| 59 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-warning-custom" id="churn-recall">-</div><div class="label">Recall</div></div></div>
|
| 60 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-danger-custom" id="churn-f1">-</div><div class="label">F1 Score</div></div></div>
|
| 61 |
+
</div>
|
| 62 |
+
<div class="row mt-3">
|
| 63 |
+
<div class="col-md-6"><div class="model-card"><h5>Churn Risk Distribution</h5><canvas id="churnRiskChart"></canvas></div></div>
|
| 64 |
+
<div class="col-md-6"><div class="model-card"><h5>Feature Importance (Top 10)</h5><canvas id="churnFeatureChart"></canvas></div></div>
|
| 65 |
+
</div>
|
| 66 |
+
<div class="row">
|
| 67 |
+
<div class="col-md-6"><div class="model-card"><h5>ROC Curve</h5><canvas id="churnROCChart"></canvas></div></div>
|
| 68 |
+
<div class="col-md-6"><div class="model-card"><h5>Top High-Risk Customers</h5><div style="max-height:280px;overflow-y:auto;" id="churnTopRisk"></div></div></div>
|
| 69 |
+
</div>
|
| 70 |
+
<!-- Manual Prediction -->
|
| 71 |
+
<div class="model-card">
|
| 72 |
+
<h5>Predict Churn on Custom Data</h5>
|
| 73 |
+
<form id="churnPredForm" class="row g-3">
|
| 74 |
+
<div class="col-md-2"><label class="form-label">Tenure (mo)</label><input type="number" class="form-control" id="p-tenure" value="12"></div>
|
| 75 |
+
<div class="col-md-2"><label class="form-label">Monthly Cost ($)</label><input type="number" class="form-control" id="p-cost" value="50"></div>
|
| 76 |
+
<div class="col-md-2"><label class="form-label">Data Usage (GB)</label><input type="number" class="form-control" id="p-data" value="10"></div>
|
| 77 |
+
<div class="col-md-2"><label class="form-label">Service Calls</label><input type="number" class="form-control" id="p-calls" value="0"></div>
|
| 78 |
+
<div class="col-md-2"><label class="form-label">Late Payments</label><input type="number" class="form-control" id="p-late" value="0"></div>
|
| 79 |
+
<div class="col-md-2"><label class="form-label">CSAT (1-10)</label><input type="number" class="form-control" id="p-csat" value="7" min="1" max="10"></div>
|
| 80 |
+
<div class="col-12 text-center mt-3"><button type="submit" class="btn btn-primary px-4">Predict Churn</button></div>
|
| 81 |
+
</form>
|
| 82 |
+
<div id="churnPredResult" class="mt-3"></div>
|
| 83 |
+
</div>
|
| 84 |
+
</div>
|
| 85 |
+
|
| 86 |
+
<!-- ============ TAB B: NETWORK PERFORMANCE ============ -->
|
| 87 |
+
<div class="tab-pane fade" id="tab-network">
|
| 88 |
+
<div class="row">
|
| 89 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-success-custom" id="net-mae">-</div><div class="label">MAE (ms)</div></div></div>
|
| 90 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-primary-custom" id="net-r2">-</div><div class="label">R-Squared</div></div></div>
|
| 91 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-warning-custom" id="net-rmse">-</div><div class="label">RMSE</div></div></div>
|
| 92 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-danger-custom" id="net-coverage">-</div><div class="label">Coverage %</div></div></div>
|
| 93 |
+
</div>
|
| 94 |
+
<div class="row mt-3">
|
| 95 |
+
<div class="col-md-6"><div class="model-card"><h5>Predicted vs Actual Latency</h5><canvas id="netPredActualChart"></canvas></div></div>
|
| 96 |
+
<div class="col-md-6"><div class="model-card"><h5>7-Day Traffic Forecast</h5><canvas id="netForecastChart"></canvas></div></div>
|
| 97 |
+
</div>
|
| 98 |
+
<div class="row">
|
| 99 |
+
<div class="col-md-12"><div class="model-card"><h5>Congestion Risk by Tower (Top 15)</h5><canvas id="netCongestionChart"></canvas></div></div>
|
| 100 |
+
</div>
|
| 101 |
+
<!-- Manual Prediction -->
|
| 102 |
+
<div class="model-card">
|
| 103 |
+
<h5>Predict Network Congestion on Custom Data</h5>
|
| 104 |
+
<form id="netPredForm" class="row g-3">
|
| 105 |
+
<div class="col-md-2"><label class="form-label">Tower ID</label><input type="text" class="form-control" id="np-tower" value="TOWER00042"></div>
|
| 106 |
+
<div class="col-md-2"><label class="form-label">Hour (0-23)</label><input type="number" class="form-control" id="np-hour" value="14" min="0" max="23"></div>
|
| 107 |
+
<div class="col-md-2"><label class="form-label">Active Users</label><input type="number" class="form-control" id="np-users" value="500"></div>
|
| 108 |
+
<div class="col-md-2"><label class="form-label">Bandwidth (Mbps)</label><input type="number" class="form-control" id="np-bw" value="800"></div>
|
| 109 |
+
<div class="col-md-2"><label class="form-label">Max Capacity</label><input type="number" class="form-control" id="np-cap" value="1000"></div>
|
| 110 |
+
<div class="col-md-2 d-flex align-items-end"><button type="submit" class="btn btn-primary w-100">Predict</button></div>
|
| 111 |
+
</form>
|
| 112 |
+
<div id="netPredResult" class="mt-3"></div>
|
| 113 |
+
</div>
|
| 114 |
+
</div>
|
| 115 |
+
|
| 116 |
+
<!-- ============ TAB C: CUSTOMER LTV ============ -->
|
| 117 |
+
<div class="tab-pane fade" id="tab-ltv">
|
| 118 |
+
<div class="row">
|
| 119 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-success-custom" id="ltv-avg">-</div><div class="label">Avg LTV</div></div></div>
|
| 120 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-primary-custom" id="ltv-median">-</div><div class="label">Median LTV</div></div></div>
|
| 121 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-warning-custom" id="ltv-high">-</div><div class="label">High-Value %</div></div></div>
|
| 122 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-danger-custom" id="ltv-r2">-</div><div class="label">Model R-Squared</div></div></div>
|
| 123 |
+
</div>
|
| 124 |
+
<div class="row mt-3">
|
| 125 |
+
<div class="col-md-6"><div class="model-card"><h5>LTV Distribution</h5><canvas id="ltvDistChart"></canvas></div></div>
|
| 126 |
+
<div class="col-md-6"><div class="model-card"><h5>LTV by Customer Segment</h5><canvas id="ltvSegmentChart"></canvas></div></div>
|
| 127 |
+
</div>
|
| 128 |
+
<div class="row">
|
| 129 |
+
<div class="col-md-12"><div class="model-card"><h5>LTV Prediction Features</h5><canvas id="ltvFeatureChart"></canvas></div></div>
|
| 130 |
+
</div>
|
| 131 |
+
<!-- Manual Prediction -->
|
| 132 |
+
<div class="model-card">
|
| 133 |
+
<h5>Predict Customer LTV on Custom Data</h5>
|
| 134 |
+
<form id="ltvPredForm" class="row g-3">
|
| 135 |
+
<div class="col-md-2"><label class="form-label">Tenure (mo)</label><input type="number" class="form-control" id="lp-tenure" value="24"></div>
|
| 136 |
+
<div class="col-md-2"><label class="form-label">Monthly Cost ($)</label><input type="number" class="form-control" id="lp-cost" value="65"></div>
|
| 137 |
+
<div class="col-md-2"><label class="form-label">Data Usage (GB)</label><input type="number" class="form-control" id="lp-data" value="15"></div>
|
| 138 |
+
<div class="col-md-2"><label class="form-label">Referrals</label><input type="number" class="form-control" id="lp-referrals" value="2"></div>
|
| 139 |
+
<div class="col-md-2"><label class="form-label">Plan Type</label>
|
| 140 |
+
<select class="form-select" id="lp-plan"><option>Basic</option><option selected>Standard</option><option>Premium</option><option>Unlimited</option><option>Family</option></select>
|
| 141 |
+
</div>
|
| 142 |
+
<div class="col-md-2"><label class="form-label">Device Age (mo)</label><input type="number" class="form-control" id="lp-device-age" value="18"></div>
|
| 143 |
+
<div class="col-12 text-center mt-3"><button type="submit" class="btn btn-primary px-4">Predict LTV</button></div>
|
| 144 |
+
</form>
|
| 145 |
+
<div id="ltvPredResult" class="mt-3"></div>
|
| 146 |
+
</div>
|
| 147 |
+
</div>
|
| 148 |
+
|
| 149 |
+
<!-- ============ TAB D: SERVICE QUALITY IMPACT ============ -->
|
| 150 |
+
<div class="tab-pane fade" id="tab-quality">
|
| 151 |
+
<div class="row">
|
| 152 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-success-custom" id="sq-accuracy">-</div><div class="label">Accuracy</div></div></div>
|
| 153 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-primary-custom" id="sq-precision">-</div><div class="label">Precision</div></div></div>
|
| 154 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-warning-custom" id="sq-recall">-</div><div class="label">Recall</div></div></div>
|
| 155 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-danger-custom" id="sq-predicted">-</div><div class="label">Predicted Complaints</div></div></div>
|
| 156 |
+
</div>
|
| 157 |
+
<div class="row mt-3">
|
| 158 |
+
<div class="col-md-6"><div class="model-card"><h5>Complaint Prediction by Type</h5><canvas id="sqTypeChart"></canvas></div></div>
|
| 159 |
+
<div class="col-md-6"><div class="model-card"><h5>Quality Risk Heatmap (by Plan)</h5><canvas id="sqHeatmapChart"></canvas></div></div>
|
| 160 |
+
</div>
|
| 161 |
+
<!-- Manual Prediction -->
|
| 162 |
+
<div class="model-card">
|
| 163 |
+
<h5>Predict Quality Impact on Custom Data</h5>
|
| 164 |
+
<form id="qualityPredForm" class="row g-3">
|
| 165 |
+
<div class="col-md-2"><label class="form-label">Download (Mbps)</label><input type="number" class="form-control" id="qp-download" value="45" step="0.1"></div>
|
| 166 |
+
<div class="col-md-2"><label class="form-label">Upload (Mbps)</label><input type="number" class="form-control" id="qp-upload" value="12" step="0.1"></div>
|
| 167 |
+
<div class="col-md-2"><label class="form-label">Drop Rate (%)</label><input type="number" class="form-control" id="qp-drop" value="2.5" step="0.1"></div>
|
| 168 |
+
<div class="col-md-2"><label class="form-label">Reliability (%)</label><input type="number" class="form-control" id="qp-reliability" value="98.5" step="0.1"></div>
|
| 169 |
+
<div class="col-md-2"><label class="form-label">Coverage (%)</label><input type="number" class="form-control" id="qp-coverage" value="92" step="0.1"></div>
|
| 170 |
+
<div class="col-md-2"><label class="form-label">Streaming MOS</label><input type="number" class="form-control" id="qp-mos" value="3.8" step="0.1" min="1" max="5"></div>
|
| 171 |
+
<div class="col-12 text-center mt-3"><button type="submit" class="btn btn-primary px-4">Predict Quality Impact</button></div>
|
| 172 |
+
</form>
|
| 173 |
+
<div id="qualityPredResult" class="mt-3"></div>
|
| 174 |
+
</div>
|
| 175 |
+
</div>
|
| 176 |
+
|
| 177 |
+
<!-- ============ TAB E: CAPACITY PLANNING ============ -->
|
| 178 |
+
<div class="tab-pane fade" id="tab-capacity">
|
| 179 |
+
<div class="row">
|
| 180 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-success-custom" id="cap-forecast-acc">-</div><div class="label">Forecast Accuracy</div></div></div>
|
| 181 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-primary-custom" id="cap-peak">-</div><div class="label">Peak Demand (Gbps)</div></div></div>
|
| 182 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-warning-custom" id="cap-util">-</div><div class="label">Avg Utilization</div></div></div>
|
| 183 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-danger-custom" id="cap-at-risk">-</div><div class="label">Towers at Risk</div></div></div>
|
| 184 |
+
</div>
|
| 185 |
+
<div class="row mt-3">
|
| 186 |
+
<div class="col-md-6"><div class="model-card"><h5>Bandwidth Demand Forecast (30 Days)</h5><canvas id="capForecastChart"></canvas></div></div>
|
| 187 |
+
<div class="col-md-6"><div class="model-card"><h5>Seasonal Demand Patterns</h5><canvas id="capSeasonalChart"></canvas></div></div>
|
| 188 |
+
</div>
|
| 189 |
+
<!-- Manual Prediction -->
|
| 190 |
+
<div class="model-card">
|
| 191 |
+
<h5>Predict Capacity Needs on Custom Data</h5>
|
| 192 |
+
<form id="capPredForm" class="row g-3">
|
| 193 |
+
<div class="col-md-2"><label class="form-label">Tower ID</label><input type="text" class="form-control" id="cp-tower" value="TOWER00100"></div>
|
| 194 |
+
<div class="col-md-2"><label class="form-label">Current Util (%)</label><input type="number" class="form-control" id="cp-util" value="72" step="0.1"></div>
|
| 195 |
+
<div class="col-md-2"><label class="form-label">Hour (0-23)</label><input type="number" class="form-control" id="cp-hour" value="18" min="0" max="23"></div>
|
| 196 |
+
<div class="col-md-2"><label class="form-label">Day of Week</label>
|
| 197 |
+
<select class="form-select" id="cp-day"><option>Monday</option><option>Tuesday</option><option>Wednesday</option><option>Thursday</option><option selected>Friday</option><option>Saturday</option><option>Sunday</option></select>
|
| 198 |
+
</div>
|
| 199 |
+
<div class="col-md-2"><label class="form-label">Season</label>
|
| 200 |
+
<select class="form-select" id="cp-season"><option>Spring</option><option selected>Summer</option><option>Autumn</option><option>Winter</option></select>
|
| 201 |
+
</div>
|
| 202 |
+
<div class="col-md-2 d-flex align-items-end"><button type="submit" class="btn btn-primary w-100">Predict</button></div>
|
| 203 |
+
</form>
|
| 204 |
+
<div id="capPredResult" class="mt-3"></div>
|
| 205 |
+
</div>
|
| 206 |
+
</div>
|
| 207 |
+
|
| 208 |
+
<!-- ============ TAB F: DEMAND FORECASTING ============ -->
|
| 209 |
+
<div class="tab-pane fade" id="tab-demand">
|
| 210 |
+
<div class="row">
|
| 211 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-success-custom" id="dem-acc">-</div><div class="label">Daily Accuracy</div></div></div>
|
| 212 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-primary-custom" id="dem-avg-vol">-</div><div class="label">Avg Daily Calls</div></div></div>
|
| 213 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-warning-custom" id="dem-peak-day">-</div><div class="label">Peak Day</div></div></div>
|
| 214 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-danger-custom" id="dem-staffing">-</div><div class="label">Optimal Staff</div></div></div>
|
| 215 |
+
</div>
|
| 216 |
+
<div class="row mt-3">
|
| 217 |
+
<div class="col-md-6"><div class="model-card"><h5>Call Volume Forecast (Next 14 Days)</h5><canvas id="demForecastChart"></canvas></div></div>
|
| 218 |
+
<div class="col-md-6"><div class="model-card"><h5>Hourly Call Patterns</h5><canvas id="demHourlyChart"></canvas></div></div>
|
| 219 |
+
</div>
|
| 220 |
+
<!-- Manual Prediction -->
|
| 221 |
+
<div class="model-card">
|
| 222 |
+
<h5>Predict Service Demand on Custom Data</h5>
|
| 223 |
+
<form id="demPredForm" class="row g-3">
|
| 224 |
+
<div class="col-md-2"><label class="form-label">Day of Week</label>
|
| 225 |
+
<select class="form-select" id="dp-day"><option>Monday</option><option selected>Tuesday</option><option>Wednesday</option><option>Thursday</option><option>Friday</option><option>Saturday</option><option>Sunday</option></select>
|
| 226 |
+
</div>
|
| 227 |
+
<div class="col-md-2"><label class="form-label">Hour (8-21)</label><input type="number" class="form-control" id="dp-hour" value="10" min="8" max="21"></div>
|
| 228 |
+
<div class="col-md-2"><label class="form-label">Current Staff</label><input type="number" class="form-control" id="dp-staff" value="35"></div>
|
| 229 |
+
<div class="col-md-2"><label class="form-label">Is Holiday?</label>
|
| 230 |
+
<select class="form-select" id="dp-holiday"><option value="0" selected>No</option><option value="1">Yes</option></select>
|
| 231 |
+
</div>
|
| 232 |
+
<div class="col-md-2"><label class="form-label">Marketing Push?</label>
|
| 233 |
+
<select class="form-select" id="dp-marketing"><option value="0" selected>No</option><option value="1">Yes</option></select>
|
| 234 |
+
</div>
|
| 235 |
+
<div class="col-md-2 d-flex align-items-end"><button type="submit" class="btn btn-primary w-100">Predict</button></div>
|
| 236 |
+
</form>
|
| 237 |
+
<div id="demPredResult" class="mt-3"></div>
|
| 238 |
+
</div>
|
| 239 |
+
</div>
|
| 240 |
+
|
| 241 |
+
<!-- ============ TAB G: OFFER RECOMMENDATION ============ -->
|
| 242 |
+
<div class="tab-pane fade" id="tab-offers">
|
| 243 |
+
<div class="row">
|
| 244 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-success-custom" id="off-acc">-</div><div class="label">Recommendation Accuracy</div></div></div>
|
| 245 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-primary-custom" id="off-conv">-</div><div class="label">Conversion Rate</div></div></div>
|
| 246 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-warning-custom" id="off-rev">-</div><div class="label">Revenue Uplift</div></div></div>
|
| 247 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-danger-custom" id="off-cust">-</div><div class="label">Eligible Customers</div></div></div>
|
| 248 |
+
</div>
|
| 249 |
+
<div class="row mt-3">
|
| 250 |
+
<div class="col-md-6"><div class="model-card"><h5>Top Recommended Upgrades</h5><canvas id="offUpgradeChart"></canvas></div></div>
|
| 251 |
+
<div class="col-md-6"><div class="model-card"><h5>Offer Response Rate by Segment</h5><canvas id="offResponseChart"></canvas></div></div>
|
| 252 |
+
</div>
|
| 253 |
+
<!-- Manual Prediction -->
|
| 254 |
+
<div class="model-card">
|
| 255 |
+
<h5>Get Personalized Offer Recommendation</h5>
|
| 256 |
+
<form id="offerPredForm" class="row g-3">
|
| 257 |
+
<div class="col-md-2"><label class="form-label">Current Plan</label>
|
| 258 |
+
<select class="form-select" id="op-plan"><option selected>Basic</option><option>Standard</option><option>Premium</option><option>Unlimited</option><option>Family</option></select>
|
| 259 |
+
</div>
|
| 260 |
+
<div class="col-md-2"><label class="form-label">Tenure (mo)</label><input type="number" class="form-control" id="op-tenure" value="18"></div>
|
| 261 |
+
<div class="col-md-2"><label class="form-label">Monthly Spend ($)</label><input type="number" class="form-control" id="op-spend" value="45"></div>
|
| 262 |
+
<div class="col-md-2"><label class="form-label">CSAT (1-10)</label><input type="number" class="form-control" id="op-csat" value="6" min="1" max="10"></div>
|
| 263 |
+
<div class="col-md-2"><label class="form-label">Churn Risk</label>
|
| 264 |
+
<select class="form-select" id="op-risk"><option>Low</option><option selected>Medium</option><option>High</option></select>
|
| 265 |
+
</div>
|
| 266 |
+
<div class="col-md-2 d-flex align-items-end"><button type="submit" class="btn btn-primary w-100">Get Offer</button></div>
|
| 267 |
+
</form>
|
| 268 |
+
<div id="offerPredResult" class="mt-3"></div>
|
| 269 |
+
</div>
|
| 270 |
+
</div>
|
| 271 |
+
|
| 272 |
+
<!-- ============ TAB H: ANOMALY DETECTION ============ -->
|
| 273 |
+
<div class="tab-pane fade" id="tab-anomaly">
|
| 274 |
+
<div class="row">
|
| 275 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-success-custom" id="anom-detected">-</div><div class="label">Anomalies Detected</div></div></div>
|
| 276 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-primary-custom" id="anom-fp">-</div><div class="label">False Positive Rate</div></div></div>
|
| 277 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-warning-custom" id="anom-critical">-</div><div class="label">Critical Alerts</div></div></div>
|
| 278 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-danger-custom" id="anom-towers">-</div><div class="label">Affected Towers</div></div></div>
|
| 279 |
+
</div>
|
| 280 |
+
<div class="row mt-3">
|
| 281 |
+
<div class="col-md-6"><div class="model-card"><h5>Anomaly Score Distribution</h5><canvas id="anomScoreChart"></canvas></div></div>
|
| 282 |
+
<div class="col-md-6"><div class="model-card"><h5>Anomaly Types Detected</h5><canvas id="anomTypeChart"></canvas></div></div>
|
| 283 |
+
</div>
|
| 284 |
+
<div class="row">
|
| 285 |
+
<div class="col-md-12"><div class="model-card"><h5>Recent Anomaly Alerts</h5><div id="anomAlerts" style="max-height:250px;overflow-y:auto;"></div></div></div>
|
| 286 |
+
</div>
|
| 287 |
+
<!-- Manual Prediction -->
|
| 288 |
+
<div class="model-card">
|
| 289 |
+
<h5>Detect Anomaly on Custom Data</h5>
|
| 290 |
+
<form id="anomPredForm" class="row g-3">
|
| 291 |
+
<div class="col-md-2"><label class="form-label">Tower ID</label><input type="text" class="form-control" id="ap-tower" value="TOWER00077"></div>
|
| 292 |
+
<div class="col-md-2"><label class="form-label">Metric Type</label>
|
| 293 |
+
<select class="form-select" id="ap-metric"><option selected>Latency</option><option>Throughput</option><option>Packet Loss</option><option>CPU Usage</option><option>Temperature</option></select>
|
| 294 |
+
</div>
|
| 295 |
+
<div class="col-md-2"><label class="form-label">Current Value</label><input type="number" class="form-control" id="ap-current" value="85" step="0.1"></div>
|
| 296 |
+
<div class="col-md-2"><label class="form-label">Historical Avg</label><input type="number" class="form-control" id="ap-avg" value="45" step="0.1"></div>
|
| 297 |
+
<div class="col-md-2"><label class="form-label">Threshold</label><input type="number" class="form-control" id="ap-threshold" value="70" step="0.1"></div>
|
| 298 |
+
<div class="col-md-2 d-flex align-items-end"><button type="submit" class="btn btn-primary w-100">Detect</button></div>
|
| 299 |
+
</form>
|
| 300 |
+
<div id="anomPredResult" class="mt-3"></div>
|
| 301 |
+
</div>
|
| 302 |
+
</div>
|
| 303 |
+
|
| 304 |
+
<!-- ============ TAB I: SENTIMENT ANALYSIS ============ -->
|
| 305 |
+
<div class="tab-pane fade" id="tab-sentiment">
|
| 306 |
+
<div class="row">
|
| 307 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-success-custom" id="sent-pos">-</div><div class="label">Positive %</div></div></div>
|
| 308 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-primary-custom" id="sent-neu">-</div><div class="label">Neutral %</div></div></div>
|
| 309 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-danger-custom" id="sent-neg">-</div><div class="label">Negative %</div></div></div>
|
| 310 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-warning-custom" id="sent-acc">-</div><div class="label">Model Accuracy</div></div></div>
|
| 311 |
+
</div>
|
| 312 |
+
<div class="row mt-3">
|
| 313 |
+
<div class="col-md-6"><div class="model-card"><h5>Sentiment Distribution</h5><canvas id="sentDistChart"></canvas></div></div>
|
| 314 |
+
<div class="col-md-6"><div class="model-card"><h5>Sentiment Trend Over Time</h5><canvas id="sentTrendChart"></canvas></div></div>
|
| 315 |
+
</div>
|
| 316 |
+
<div class="row">
|
| 317 |
+
<div class="col-md-12"><div class="model-card"><h5>Complaint Category Classification</h5><canvas id="sentCategoryChart"></canvas></div></div>
|
| 318 |
+
</div>
|
| 319 |
+
<!-- Manual Prediction -->
|
| 320 |
+
<div class="model-card">
|
| 321 |
+
<h5>Analyze Customer Sentiment</h5>
|
| 322 |
+
<form id="sentPredForm" class="row g-3">
|
| 323 |
+
<div class="col-md-6"><label class="form-label">Customer Message</label>
|
| 324 |
+
<textarea class="form-control" id="sp-text" rows="3" placeholder="Type customer feedback or complaint...">My internet has been very slow for the past week and nobody seems to care about fixing it. Very frustrated with the service.</textarea>
|
| 325 |
+
</div>
|
| 326 |
+
<div class="col-md-3"><label class="form-label">Channel</label>
|
| 327 |
+
<select class="form-select" id="sp-channel"><option>Phone</option><option>Email</option><option selected>Chat</option><option>Social Media</option><option>In-Store</option></select>
|
| 328 |
+
</div>
|
| 329 |
+
<div class="col-md-3"><label class="form-label">Complaint Type</label>
|
| 330 |
+
<select class="form-select" id="sp-type"><option selected>Data Speed</option><option>Network Coverage</option><option>Billing Issue</option><option>Call Quality</option><option>Customer Service</option><option>Plan Change</option></select>
|
| 331 |
+
</div>
|
| 332 |
+
<div class="col-12 text-center mt-3"><button type="submit" class="btn btn-primary px-4">Analyze Sentiment</button></div>
|
| 333 |
+
</form>
|
| 334 |
+
<div id="sentPredResult" class="mt-3"></div>
|
| 335 |
+
</div>
|
| 336 |
+
</div>
|
| 337 |
+
|
| 338 |
+
<!-- ============ TAB J: DEVICE UPGRADE ============ -->
|
| 339 |
+
<div class="tab-pane fade" id="tab-device">
|
| 340 |
+
<div class="row">
|
| 341 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-success-custom" id="dev-acc">-</div><div class="label">Accuracy</div></div></div>
|
| 342 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-primary-custom" id="dev-eligible">-</div><div class="label">Upgrade Eligible</div></div></div>
|
| 343 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-warning-custom" id="dev-likely">-</div><div class="label">Likely Upgraders</div></div></div>
|
| 344 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-danger-custom" id="dev-rev">-</div><div class="label">Revenue Opportunity</div></div></div>
|
| 345 |
+
</div>
|
| 346 |
+
<div class="row mt-3">
|
| 347 |
+
<div class="col-md-6"><div class="model-card"><h5>Upgrade Probability by Device Age</h5><canvas id="devAgeChart"></canvas></div></div>
|
| 348 |
+
<div class="col-md-6"><div class="model-card"><h5>Upgrade by Manufacturer</h5><canvas id="devMfgChart"></canvas></div></div>
|
| 349 |
+
</div>
|
| 350 |
+
<!-- Manual Prediction -->
|
| 351 |
+
<div class="model-card">
|
| 352 |
+
<h5>Predict Device Upgrade on Custom Data</h5>
|
| 353 |
+
<form id="devPredForm" class="row g-3">
|
| 354 |
+
<div class="col-md-2"><label class="form-label">Device Age (mo)</label><input type="number" class="form-control" id="jp-age" value="30"></div>
|
| 355 |
+
<div class="col-md-2"><label class="form-label">Manufacturer</label>
|
| 356 |
+
<select class="form-select" id="jp-mfg"><option selected>Samsung</option><option>Apple</option><option>OnePlus</option><option>Xiaomi</option><option>Google</option><option>Other</option></select>
|
| 357 |
+
</div>
|
| 358 |
+
<div class="col-md-2"><label class="form-label">Plan Type</label>
|
| 359 |
+
<select class="form-select" id="jp-plan"><option>Basic</option><option selected>Standard</option><option>Premium</option><option>Unlimited</option></select>
|
| 360 |
+
</div>
|
| 361 |
+
<div class="col-md-2"><label class="form-label">Tenure (mo)</label><input type="number" class="form-control" id="jp-tenure" value="24"></div>
|
| 362 |
+
<div class="col-md-2"><label class="form-label">Monthly Usage (GB)</label><input type="number" class="form-control" id="jp-usage" value="20"></div>
|
| 363 |
+
<div class="col-md-2 d-flex align-items-end"><button type="submit" class="btn btn-primary w-100">Predict</button></div>
|
| 364 |
+
</form>
|
| 365 |
+
<div id="devPredResult" class="mt-3"></div>
|
| 366 |
+
</div>
|
| 367 |
+
</div>
|
| 368 |
+
|
| 369 |
+
<!-- ============ TAB K: NETWORK INVESTMENT ============ -->
|
| 370 |
+
<div class="tab-pane fade" id="tab-invest">
|
| 371 |
+
<div class="row">
|
| 372 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-success-custom" id="inv-roi">-</div><div class="label">Projected ROI</div></div></div>
|
| 373 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-primary-custom" id="inv-budget">-</div><div class="label">Optimal Budget</div></div></div>
|
| 374 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-warning-custom" id="inv-towers">-</div><div class="label">New Towers Needed</div></div></div>
|
| 375 |
+
<div class="col-md-3"><div class="model-metric"><div class="value text-danger-custom" id="inv-upgrade">-</div><div class="label">Towers to Upgrade</div></div></div>
|
| 376 |
+
</div>
|
| 377 |
+
<div class="row mt-3">
|
| 378 |
+
<div class="col-md-6"><div class="model-card"><h5>Investment Priority by Region</h5><canvas id="invRegionChart"></canvas></div></div>
|
| 379 |
+
<div class="col-md-6"><div class="model-card"><h5>ROI Projection (5 Years)</h5><canvas id="invROIChart"></canvas></div></div>
|
| 380 |
+
</div>
|
| 381 |
+
<!-- Manual Prediction -->
|
| 382 |
+
<div class="model-card">
|
| 383 |
+
<h5>Predict Investment ROI on Custom Data</h5>
|
| 384 |
+
<form id="invPredForm" class="row g-3">
|
| 385 |
+
<div class="col-md-2"><label class="form-label">City/Region</label><input type="text" class="form-control" id="kp-city" value="Mumbai"></div>
|
| 386 |
+
<div class="col-md-2"><label class="form-label">Population (K)</label><input type="number" class="form-control" id="kp-population" value="500"></div>
|
| 387 |
+
<div class="col-md-2"><label class="form-label">Current Towers</label><input type="number" class="form-control" id="kp-towers" value="25"></div>
|
| 388 |
+
<div class="col-md-2"><label class="form-label">Growth Rate (%)</label><input type="number" class="form-control" id="kp-growth" value="8" step="0.1"></div>
|
| 389 |
+
<div class="col-md-2"><label class="form-label">Budget ($M)</label><input type="number" class="form-control" id="kp-budget" value="5" step="0.1"></div>
|
| 390 |
+
<div class="col-md-2 d-flex align-items-end"><button type="submit" class="btn btn-primary w-100">Predict ROI</button></div>
|
| 391 |
+
</form>
|
| 392 |
+
<div id="invPredResult" class="mt-3"></div>
|
| 393 |
+
</div>
|
| 394 |
+
</div>
|
| 395 |
+
|
| 396 |
+
<div class="tab-pane fade" id="tab-shap">
|
| 397 |
+
<div class="model-card">
|
| 398 |
+
<h5>SHAP Feature Importance - Churn Model <button class="info-btn" data-info-icon="🔍" data-info-title="SHAP Values — Churn Model" data-info-section="ML Suite · SHAP Explainability" data-info="SHAP values show how each input feature contributed to the churn probability prediction. A high positive SHAP value for a feature means that feature significantly increased the predicted churn probability for that customer." data-info-tips="Tenure SHAP: Negative = long tenure lowers churn risk.|Late Payment SHAP: Positive = payment defaults strongly increase churn risk.|Use the top drivers to design targeted interventions for each high-risk customer.">ⓘ</button></h5>
|
| 399 |
+
<p class="text-muted mb-3">SHAP (SHapley Additive exPlanations) values show how each feature contributes to the model prediction.</p>
|
| 400 |
+
<canvas id="shapChurnChart" height="300"></canvas>
|
| 401 |
+
</div>
|
| 402 |
+
<div class="row">
|
| 403 |
+
<div class="col-md-6"><div class="model-card"><h5>SHAP - LTV Model <button class="info-btn" data-info-icon="💰" data-info-title="SHAP Values — LTV Model" data-info-section="ML Suite · SHAP Explainability" data-info="Feature importance for the Customer LTV prediction model. Shows which customer attributes most strongly drive long-term revenue value." data-info-tips="Monthly cost is typically the strongest positive driver of LTV.|Short tenure with high cost = high ARPU customer worth protecting.|Referrals have a multiplier effect on LTV beyond just direct revenue.">ⓘ</button></h5><canvas id="shapLTVChart"></canvas></div></div>
|
| 404 |
+
<div class="col-md-6"><div class="model-card"><h5>SHAP - Quality Impact Model <button class="info-btn" data-info-icon="⭐" data-info-title="SHAP Values — Quality Impact Model" data-info-section="ML Suite · SHAP Explainability" data-info="Feature importance for the service quality complaint prediction model. Shows which network metrics most strongly predict customer complaint filings." data-info-tips="Call drop rate is usually the single strongest complaint driver.|MOS score below 3.0 has outsized SHAP impact on complaint probability.|Use this to prioritise which network metrics to improve first for the biggest CSAT gain.">ⓘ</button></h5><canvas id="shapQualityChart"></canvas></div></div>
|
| 405 |
+
</div>
|
| 406 |
+
<div class="model-card">
|
| 407 |
+
<h5>Model Performance Comparison <button class="info-btn" data-info-icon="🏆" data-info-title="ML Model Performance Comparison" data-info-section="ML Suite · Model Benchmarking" data-info="A side-by-side comparison of accuracy, precision, recall, and F1 scores across all classification models in the suite. Helps evaluate which models are production-ready vs. need retraining." data-info-tips="Target: >85% accuracy for classification models.|Low recall = model misses too many true positives — bad for churn detection.|Retrain any model with F1 score <0.75 on fresh data.">ⓘ</button></h5>
|
| 408 |
+
<canvas id="modelComparisonChart"></canvas>
|
| 409 |
+
</div>
|
| 410 |
+
</div>
|
| 411 |
+
|
| 412 |
+
</div><!-- end tab-content -->
|
| 413 |
+
{% endblock %}
|
| 414 |
+
|
| 415 |
+
{% block extra_js %}
|
| 416 |
+
<script>
|
| 417 |
+
Chart.defaults.color = '#64748b';
|
| 418 |
+
Chart.defaults.borderColor = '#e2e8f0';
|
| 419 |
+
Chart.defaults.font.family = "'Inter', sans-serif";
|
| 420 |
+
|
| 421 |
+
const C = { primary: '#4f46e5', success: '#10b981', warning: '#f59e0b', danger: '#ef4444', info: '#3b82f6', purple: '#8b5cf6' };
|
| 422 |
+
|
| 423 |
+
// === UTILITY ===
|
| 424 |
+
function barChart(id, labels, data, color, label='') {
|
| 425 |
+
new Chart(document.getElementById(id).getContext('2d'), {
|
| 426 |
+
type: 'bar', data: { labels, datasets: [{ label, data, backgroundColor: color, borderRadius: 6 }] },
|
| 427 |
+
options: { responsive: true, plugins: { legend: { display: !!label } }, scales: { y: { beginAtZero: true, grid: { color: '#f1f5f9' } } } }
|
| 428 |
+
});
|
| 429 |
+
}
|
| 430 |
+
function horizBar(id, labels, data, colors, label='') {
|
| 431 |
+
new Chart(document.getElementById(id).getContext('2d'), {
|
| 432 |
+
type: 'bar', data: { labels, datasets: [{ label, data, backgroundColor: colors, borderRadius: 6 }] },
|
| 433 |
+
options: { responsive: true, indexAxis: 'y', plugins: { legend: { display: false } }, scales: { x: { beginAtZero: true, grid: { color: '#f1f5f9' } } } }
|
| 434 |
+
});
|
| 435 |
+
}
|
| 436 |
+
function doughnut(id, labels, data, colors) {
|
| 437 |
+
new Chart(document.getElementById(id).getContext('2d'), {
|
| 438 |
+
type: 'doughnut', data: { labels, datasets: [{ data, backgroundColor: colors, borderWidth: 4, borderColor: '#fff' }] },
|
| 439 |
+
options: { responsive: true, plugins: { legend: { position: 'right' } } }
|
| 440 |
+
});
|
| 441 |
+
}
|
| 442 |
+
function lineChart(id, labels, datasets) {
|
| 443 |
+
new Chart(document.getElementById(id).getContext('2d'), {
|
| 444 |
+
type: 'line', data: { labels, datasets },
|
| 445 |
+
options: { responsive: true, scales: { y: { grid: { color: '#f1f5f9' } } } }
|
| 446 |
+
});
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
function showPredResult(containerId, items) {
|
| 450 |
+
let html = '<div class="row text-center">';
|
| 451 |
+
items.forEach(item => {
|
| 452 |
+
html += `<div class="col-md-${item.col || 3}"><div class="pred-result-card"><h3 style="color:${item.color}">${item.value}</h3><small>${item.label}</small></div></div>`;
|
| 453 |
+
});
|
| 454 |
+
html += '</div>';
|
| 455 |
+
if (items.some(i => i.detail)) {
|
| 456 |
+
html += '<div class="mt-3 p-3" style="background:#f8fafc;border-radius:8px;">';
|
| 457 |
+
items.filter(i => i.detail).forEach(i => { html += `<p class="mb-1"><strong>${i.label}:</strong> ${i.detail}</p>`; });
|
| 458 |
+
html += '</div>';
|
| 459 |
+
}
|
| 460 |
+
document.getElementById(containerId).innerHTML = html;
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
// === TAB A: CHURN ===
|
| 464 |
+
async function loadChurn() {
|
| 465 |
+
const res = await fetch('/api/ml/churn');
|
| 466 |
+
const d = await res.json();
|
| 467 |
+
document.getElementById('churn-accuracy').textContent = d.accuracy + '%';
|
| 468 |
+
document.getElementById('churn-precision').textContent = d.precision + '%';
|
| 469 |
+
document.getElementById('churn-recall').textContent = d.recall + '%';
|
| 470 |
+
document.getElementById('churn-f1').textContent = d.f1 + '%';
|
| 471 |
+
|
| 472 |
+
doughnut('churnRiskChart', d.risk_labels, d.risk_values, [C.success, C.warning, C.danger]);
|
| 473 |
+
horizBar('churnFeatureChart', d.feature_names, d.feature_importance, d.feature_importance.map(v => v > 0.1 ? C.danger : v > 0.06 ? C.warning : C.success));
|
| 474 |
+
lineChart('churnROCChart', d.roc_fpr, [{
|
| 475 |
+
label: `AUC = ${d.auc}`, data: d.roc_tpr, borderColor: C.primary, fill: false, tension: 0.3, borderWidth: 2
|
| 476 |
+
}, { label: 'Random', data: d.roc_fpr, borderColor: '#94a3b8', borderDash: [5,5], fill: false, borderWidth: 1 }]);
|
| 477 |
+
|
| 478 |
+
let html = '<table class="prediction-table"><thead><tr><th>Customer</th><th>Prob</th><th>Risk</th><th>Plan</th></tr></thead><tbody>';
|
| 479 |
+
d.top_risk.forEach(r => {
|
| 480 |
+
const badge = r.risk === 'Medium' ? 'badge-medium' : r.risk === 'High' ? 'badge-high' : 'badge-low';
|
| 481 |
+
html += `<tr><td>${r.id}</td><td>${r.prob}%</td><td><span class="badge-model ${badge}">${r.risk}</span></td><td>${r.plan}</td></tr>`;
|
| 482 |
+
});
|
| 483 |
+
html += '</tbody></table>';
|
| 484 |
+
document.getElementById('churnTopRisk').innerHTML = html;
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
// A: Manual prediction
|
| 488 |
+
document.getElementById('churnPredForm').addEventListener('submit', async function(e) {
|
| 489 |
+
e.preventDefault();
|
| 490 |
+
const res = await fetch('/api/ml/churn/predict', { method: 'POST', headers: {'Content-Type':'application/json'},
|
| 491 |
+
body: JSON.stringify({
|
| 492 |
+
tenure: +document.getElementById('p-tenure').value, cost: +document.getElementById('p-cost').value,
|
| 493 |
+
data_gb: +document.getElementById('p-data').value, calls: +document.getElementById('p-calls').value,
|
| 494 |
+
late: +document.getElementById('p-late').value, csat: +document.getElementById('p-csat').value
|
| 495 |
+
})
|
| 496 |
+
});
|
| 497 |
+
const d = await res.json();
|
| 498 |
+
const color = d.risk === 'High' ? C.danger : d.risk === 'Medium' ? C.warning : C.success;
|
| 499 |
+
showPredResult('churnPredResult', [
|
| 500 |
+
{ value: d.probability + '%', label: 'Churn Probability', color: color, col: 4 },
|
| 501 |
+
{ value: d.risk, label: 'Risk Level', color: color, col: 4 },
|
| 502 |
+
{ value: '$' + d.ltv.toLocaleString(), label: 'Predicted LTV', color: C.success, col: 4 }
|
| 503 |
+
]);
|
| 504 |
+
});
|
| 505 |
+
|
| 506 |
+
// === TAB B: NETWORK ===
|
| 507 |
+
async function loadNetwork() {
|
| 508 |
+
const res = await fetch('/api/ml/network');
|
| 509 |
+
const d = await res.json();
|
| 510 |
+
document.getElementById('net-mae').textContent = d.mae;
|
| 511 |
+
document.getElementById('net-r2').textContent = d.r2;
|
| 512 |
+
document.getElementById('net-rmse').textContent = d.rmse;
|
| 513 |
+
document.getElementById('net-coverage').textContent = d.coverage + '%';
|
| 514 |
+
lineChart('netPredActualChart', d.hours, [
|
| 515 |
+
{ label: 'Actual', data: d.actual, borderColor: C.primary, fill: false, tension: 0.3, borderWidth: 2 },
|
| 516 |
+
{ label: 'Predicted', data: d.predicted, borderColor: C.success, borderDash: [5,3], fill: false, tension: 0.3, borderWidth: 2 }
|
| 517 |
+
]);
|
| 518 |
+
lineChart('netForecastChart', d.forecast_days, [
|
| 519 |
+
{ label: 'Traffic (Gbps)', data: d.forecast_traffic, borderColor: C.info, backgroundColor: 'rgba(59,130,246,0.1)', fill: true, tension: 0.4, borderWidth: 2 }
|
| 520 |
+
]);
|
| 521 |
+
horizBar('netCongestionChart', d.congestion_towers, d.congestion_scores, d.congestion_scores.map(v => v > 80 ? C.danger : v > 60 ? C.warning : C.success));
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
// B: Manual prediction
|
| 525 |
+
document.getElementById('netPredForm').addEventListener('submit', async function(e) {
|
| 526 |
+
e.preventDefault();
|
| 527 |
+
const res = await fetch('/api/ml/network/predict', { method: 'POST', headers: {'Content-Type':'application/json'},
|
| 528 |
+
body: JSON.stringify({
|
| 529 |
+
tower: document.getElementById('np-tower').value, hour: +document.getElementById('np-hour').value,
|
| 530 |
+
users: +document.getElementById('np-users').value, bandwidth: +document.getElementById('np-bw').value,
|
| 531 |
+
max_capacity: +document.getElementById('np-cap').value
|
| 532 |
+
})
|
| 533 |
+
});
|
| 534 |
+
const d = await res.json();
|
| 535 |
+
const color = d.risk === 'High' ? C.danger : d.risk === 'Medium' ? C.warning : C.success;
|
| 536 |
+
showPredResult('netPredResult', [
|
| 537 |
+
{ value: d.predicted_latency + ' ms', label: 'Predicted Latency', color: C.primary, col: 3 },
|
| 538 |
+
{ value: d.congestion_level + '%', label: 'Congestion Level', color: color, col: 3 },
|
| 539 |
+
{ value: d.risk, label: 'Congestion Risk', color: color, col: 3 },
|
| 540 |
+
{ value: d.packet_loss + '%', label: 'Est. Packet Loss', color: d.packet_loss > 2 ? C.danger : C.success, col: 3,
|
| 541 |
+
detail: d.recommendation }
|
| 542 |
+
]);
|
| 543 |
+
});
|
| 544 |
+
|
| 545 |
+
// === TAB C: LTV ===
|
| 546 |
+
async function loadLTV() {
|
| 547 |
+
const res = await fetch('/api/ml/ltv');
|
| 548 |
+
const d = await res.json();
|
| 549 |
+
document.getElementById('ltv-avg').textContent = '$' + d.avg_ltv;
|
| 550 |
+
document.getElementById('ltv-median').textContent = '$' + d.median_ltv;
|
| 551 |
+
document.getElementById('ltv-high').textContent = d.high_pct + '%';
|
| 552 |
+
document.getElementById('ltv-r2').textContent = d.r2;
|
| 553 |
+
barChart('ltvDistChart', d.dist_labels, d.dist_values, C.primary, 'Customers');
|
| 554 |
+
barChart('ltvSegmentChart', d.segment_labels, d.segment_ltv, [C.danger, C.warning, C.info, C.success, C.primary, C.purple], 'Avg LTV ($)');
|
| 555 |
+
horizBar('ltvFeatureChart', d.feature_names, d.feature_importance, C.success);
|
| 556 |
+
}
|
| 557 |
+
|
| 558 |
+
// C: Manual prediction
|
| 559 |
+
document.getElementById('ltvPredForm').addEventListener('submit', async function(e) {
|
| 560 |
+
e.preventDefault();
|
| 561 |
+
const res = await fetch('/api/ml/ltv/predict', { method: 'POST', headers: {'Content-Type':'application/json'},
|
| 562 |
+
body: JSON.stringify({
|
| 563 |
+
tenure: +document.getElementById('lp-tenure').value, cost: +document.getElementById('lp-cost').value,
|
| 564 |
+
data_gb: +document.getElementById('lp-data').value, referrals: +document.getElementById('lp-referrals').value,
|
| 565 |
+
plan: document.getElementById('lp-plan').value, device_age: +document.getElementById('lp-device-age').value
|
| 566 |
+
})
|
| 567 |
+
});
|
| 568 |
+
const d = await res.json();
|
| 569 |
+
const color = d.tier === 'Platinum' ? C.purple : d.tier === 'Gold' ? C.warning : d.tier === 'Silver' ? C.info : '#64748b';
|
| 570 |
+
showPredResult('ltvPredResult', [
|
| 571 |
+
{ value: '$' + d.predicted_ltv.toLocaleString(), label: 'Predicted LTV', color: C.success, col: 3 },
|
| 572 |
+
{ value: d.tier, label: 'Value Tier', color: color, col: 3 },
|
| 573 |
+
{ value: '$' + d.monthly_value, label: 'Monthly Value', color: C.primary, col: 3 },
|
| 574 |
+
{ value: d.retention_years + ' yrs', label: 'Expected Retention', color: C.info, col: 3 }
|
| 575 |
+
]);
|
| 576 |
+
});
|
| 577 |
+
|
| 578 |
+
// === TAB D: QUALITY IMPACT ===
|
| 579 |
+
async function loadQualityImpact() {
|
| 580 |
+
const res = await fetch('/api/ml/quality-impact');
|
| 581 |
+
const d = await res.json();
|
| 582 |
+
document.getElementById('sq-accuracy').textContent = d.accuracy + '%';
|
| 583 |
+
document.getElementById('sq-precision').textContent = d.precision + '%';
|
| 584 |
+
document.getElementById('sq-recall').textContent = d.recall + '%';
|
| 585 |
+
document.getElementById('sq-predicted').textContent = d.predicted_complaints.toLocaleString();
|
| 586 |
+
barChart('sqTypeChart', d.type_labels, d.type_values, [C.danger, C.warning, C.primary, C.info, C.success], 'Predicted');
|
| 587 |
+
barChart('sqHeatmapChart', d.plan_labels, d.plan_risk, [C.success, C.info, C.warning, C.danger, C.primary], 'Risk Score');
|
| 588 |
+
}
|
| 589 |
+
|
| 590 |
+
// D: Manual prediction
|
| 591 |
+
document.getElementById('qualityPredForm').addEventListener('submit', async function(e) {
|
| 592 |
+
e.preventDefault();
|
| 593 |
+
const res = await fetch('/api/ml/quality-impact/predict', { method: 'POST', headers: {'Content-Type':'application/json'},
|
| 594 |
+
body: JSON.stringify({
|
| 595 |
+
download: +document.getElementById('qp-download').value, upload: +document.getElementById('qp-upload').value,
|
| 596 |
+
drop_rate: +document.getElementById('qp-drop').value, reliability: +document.getElementById('qp-reliability').value,
|
| 597 |
+
coverage: +document.getElementById('qp-coverage').value, mos: +document.getElementById('qp-mos').value
|
| 598 |
+
})
|
| 599 |
+
});
|
| 600 |
+
const d = await res.json();
|
| 601 |
+
const color = d.risk === 'High' ? C.danger : d.risk === 'Medium' ? C.warning : C.success;
|
| 602 |
+
showPredResult('qualityPredResult', [
|
| 603 |
+
{ value: d.complaint_prob + '%', label: 'Complaint Probability', color: color, col: 3 },
|
| 604 |
+
{ value: d.risk, label: 'Risk Level', color: color, col: 3 },
|
| 605 |
+
{ value: d.expected_type, label: 'Likely Complaint Type', color: C.primary, col: 3 },
|
| 606 |
+
{ value: d.quality_score + '/100', label: 'Quality Score', color: d.quality_score > 70 ? C.success : C.danger, col: 3,
|
| 607 |
+
detail: d.recommendation }
|
| 608 |
+
]);
|
| 609 |
+
});
|
| 610 |
+
|
| 611 |
+
// === TAB E: CAPACITY ===
|
| 612 |
+
async function loadCapacity() {
|
| 613 |
+
const res = await fetch('/api/ml/capacity');
|
| 614 |
+
const d = await res.json();
|
| 615 |
+
document.getElementById('cap-forecast-acc').textContent = d.accuracy + '%';
|
| 616 |
+
document.getElementById('cap-peak').textContent = d.peak_demand;
|
| 617 |
+
document.getElementById('cap-util').textContent = d.avg_util + '%';
|
| 618 |
+
document.getElementById('cap-at-risk').textContent = d.at_risk;
|
| 619 |
+
lineChart('capForecastChart', d.days, [
|
| 620 |
+
{ label: 'Demand', data: d.demand, borderColor: C.primary, fill: false, tension: 0.4, borderWidth: 2 },
|
| 621 |
+
{ label: 'Capacity', data: d.capacity, borderColor: C.danger, borderDash: [5,3], fill: false, borderWidth: 1 }
|
| 622 |
+
]);
|
| 623 |
+
barChart('capSeasonalChart', d.months, d.seasonal, C.info, 'Traffic Index');
|
| 624 |
+
}
|
| 625 |
+
|
| 626 |
+
// E: Manual prediction
|
| 627 |
+
document.getElementById('capPredForm').addEventListener('submit', async function(e) {
|
| 628 |
+
e.preventDefault();
|
| 629 |
+
const res = await fetch('/api/ml/capacity/predict', { method: 'POST', headers: {'Content-Type':'application/json'},
|
| 630 |
+
body: JSON.stringify({
|
| 631 |
+
tower: document.getElementById('cp-tower').value, util: +document.getElementById('cp-util').value,
|
| 632 |
+
hour: +document.getElementById('cp-hour').value, day: document.getElementById('cp-day').value,
|
| 633 |
+
season: document.getElementById('cp-season').value
|
| 634 |
+
})
|
| 635 |
+
});
|
| 636 |
+
const d = await res.json();
|
| 637 |
+
const color = d.status === 'Critical' ? C.danger : d.status === 'Warning' ? C.warning : C.success;
|
| 638 |
+
showPredResult('capPredResult', [
|
| 639 |
+
{ value: d.predicted_demand + ' Gbps', label: 'Predicted Demand', color: C.primary, col: 3 },
|
| 640 |
+
{ value: d.status, label: 'Capacity Status', color: color, col: 3 },
|
| 641 |
+
{ value: d.headroom + '%', label: 'Headroom', color: d.headroom < 20 ? C.danger : C.success, col: 3 },
|
| 642 |
+
{ value: d.hours_to_threshold + 'h', label: 'Time to Threshold', color: d.hours_to_threshold < 6 ? C.danger : C.info, col: 3,
|
| 643 |
+
detail: d.recommendation }
|
| 644 |
+
]);
|
| 645 |
+
});
|
| 646 |
+
|
| 647 |
+
// === TAB F: DEMAND FORECAST ===
|
| 648 |
+
async function loadDemand() {
|
| 649 |
+
const res = await fetch('/api/ml/demand');
|
| 650 |
+
const d = await res.json();
|
| 651 |
+
document.getElementById('dem-acc').textContent = d.accuracy + '%';
|
| 652 |
+
document.getElementById('dem-avg-vol').textContent = d.avg_volume.toLocaleString();
|
| 653 |
+
document.getElementById('dem-peak-day').textContent = d.peak_day;
|
| 654 |
+
document.getElementById('dem-staffing').textContent = d.optimal_staff;
|
| 655 |
+
lineChart('demForecastChart', d.days, [
|
| 656 |
+
{ label: 'Forecasted Calls', data: d.forecast, borderColor: C.primary, backgroundColor: 'rgba(79,70,229,0.1)', fill: true, tension: 0.4, borderWidth: 2 }
|
| 657 |
+
]);
|
| 658 |
+
barChart('demHourlyChart', d.hours, d.hourly, C.info, 'Avg Calls');
|
| 659 |
+
}
|
| 660 |
+
|
| 661 |
+
// F: Manual prediction
|
| 662 |
+
document.getElementById('demPredForm').addEventListener('submit', async function(e) {
|
| 663 |
+
e.preventDefault();
|
| 664 |
+
const res = await fetch('/api/ml/demand/predict', { method: 'POST', headers: {'Content-Type':'application/json'},
|
| 665 |
+
body: JSON.stringify({
|
| 666 |
+
day: document.getElementById('dp-day').value, hour: +document.getElementById('dp-hour').value,
|
| 667 |
+
staff: +document.getElementById('dp-staff').value, holiday: +document.getElementById('dp-holiday').value,
|
| 668 |
+
marketing: +document.getElementById('dp-marketing').value
|
| 669 |
+
})
|
| 670 |
+
});
|
| 671 |
+
const d = await res.json();
|
| 672 |
+
const color = d.staffing_status === 'Understaffed' ? C.danger : d.staffing_status === 'Optimal' ? C.success : C.warning;
|
| 673 |
+
showPredResult('demPredResult', [
|
| 674 |
+
{ value: d.predicted_calls, label: 'Predicted Calls', color: C.primary, col: 3 },
|
| 675 |
+
{ value: d.recommended_staff, label: 'Recommended Staff', color: C.info, col: 3 },
|
| 676 |
+
{ value: d.avg_wait + ' min', label: 'Expected Wait Time', color: d.avg_wait > 5 ? C.danger : C.success, col: 3 },
|
| 677 |
+
{ value: d.staffing_status, label: 'Staffing Status', color: color, col: 3,
|
| 678 |
+
detail: d.recommendation }
|
| 679 |
+
]);
|
| 680 |
+
});
|
| 681 |
+
|
| 682 |
+
// === TAB G: OFFERS ===
|
| 683 |
+
async function loadOffers() {
|
| 684 |
+
const res = await fetch('/api/ml/offers');
|
| 685 |
+
const d = await res.json();
|
| 686 |
+
document.getElementById('off-acc').textContent = d.accuracy + '%';
|
| 687 |
+
document.getElementById('off-conv').textContent = d.conversion + '%';
|
| 688 |
+
document.getElementById('off-rev').textContent = d.revenue_uplift;
|
| 689 |
+
document.getElementById('off-cust').textContent = d.eligible.toLocaleString();
|
| 690 |
+
barChart('offUpgradeChart', d.upgrade_labels, d.upgrade_values, C.success, 'Recommended');
|
| 691 |
+
barChart('offResponseChart', d.response_labels, d.response_rates, C.primary, 'Response Rate %');
|
| 692 |
+
}
|
| 693 |
+
|
| 694 |
+
// G: Manual prediction
|
| 695 |
+
document.getElementById('offerPredForm').addEventListener('submit', async function(e) {
|
| 696 |
+
e.preventDefault();
|
| 697 |
+
const res = await fetch('/api/ml/offers/predict', { method: 'POST', headers: {'Content-Type':'application/json'},
|
| 698 |
+
body: JSON.stringify({
|
| 699 |
+
plan: document.getElementById('op-plan').value, tenure: +document.getElementById('op-tenure').value,
|
| 700 |
+
spend: +document.getElementById('op-spend').value, csat: +document.getElementById('op-csat').value,
|
| 701 |
+
risk: document.getElementById('op-risk').value
|
| 702 |
+
})
|
| 703 |
+
});
|
| 704 |
+
const d = await res.json();
|
| 705 |
+
showPredResult('offerPredResult', [
|
| 706 |
+
{ value: d.best_offer, label: 'Recommended Offer', color: C.primary, col: 3 },
|
| 707 |
+
{ value: d.conversion_prob + '%', label: 'Conversion Probability', color: d.conversion_prob > 30 ? C.success : C.warning, col: 3 },
|
| 708 |
+
{ value: '+$' + d.revenue_uplift, label: 'Monthly Revenue Uplift', color: C.success, col: 3 },
|
| 709 |
+
{ value: d.urgency, label: 'Urgency', color: d.urgency === 'High' ? C.danger : d.urgency === 'Medium' ? C.warning : C.info, col: 3,
|
| 710 |
+
detail: d.reasoning }
|
| 711 |
+
]);
|
| 712 |
+
});
|
| 713 |
+
|
| 714 |
+
// === TAB H: ANOMALY ===
|
| 715 |
+
async function loadAnomaly() {
|
| 716 |
+
const res = await fetch('/api/ml/anomaly');
|
| 717 |
+
const d = await res.json();
|
| 718 |
+
document.getElementById('anom-detected').textContent = d.total_detected;
|
| 719 |
+
document.getElementById('anom-fp').textContent = d.false_positive + '%';
|
| 720 |
+
document.getElementById('anom-critical').textContent = d.critical;
|
| 721 |
+
document.getElementById('anom-towers').textContent = d.affected_towers;
|
| 722 |
+
barChart('anomScoreChart', d.score_labels, d.score_values, C.warning, 'Anomalies');
|
| 723 |
+
doughnut('anomTypeChart', d.type_labels, d.type_values, [C.danger, C.warning, C.info, C.primary, C.success]);
|
| 724 |
+
let html = '<table class="prediction-table"><thead><tr><th>Time</th><th>Tower</th><th>Type</th><th>Severity</th><th>Score</th></tr></thead><tbody>';
|
| 725 |
+
d.alerts.forEach(a => {
|
| 726 |
+
const badge = a.severity === 'Critical' ? 'badge-high' : a.severity === 'Warning' ? 'badge-medium' : 'badge-low';
|
| 727 |
+
html += `<tr><td>${a.time}</td><td>${a.tower}</td><td>${a.type}</td><td><span class="badge-model ${badge}">${a.severity}</span></td><td>${a.score}</td></tr>`;
|
| 728 |
+
});
|
| 729 |
+
html += '</tbody></table>';
|
| 730 |
+
document.getElementById('anomAlerts').innerHTML = html;
|
| 731 |
+
}
|
| 732 |
+
|
| 733 |
+
// H: Manual prediction
|
| 734 |
+
document.getElementById('anomPredForm').addEventListener('submit', async function(e) {
|
| 735 |
+
e.preventDefault();
|
| 736 |
+
const res = await fetch('/api/ml/anomaly/predict', { method: 'POST', headers: {'Content-Type':'application/json'},
|
| 737 |
+
body: JSON.stringify({
|
| 738 |
+
tower: document.getElementById('ap-tower').value, metric: document.getElementById('ap-metric').value,
|
| 739 |
+
current: +document.getElementById('ap-current').value, avg: +document.getElementById('ap-avg').value,
|
| 740 |
+
threshold: +document.getElementById('ap-threshold').value
|
| 741 |
+
})
|
| 742 |
+
});
|
| 743 |
+
const d = await res.json();
|
| 744 |
+
const color = d.severity === 'Critical' ? C.danger : d.severity === 'Warning' ? C.warning : C.success;
|
| 745 |
+
showPredResult('anomPredResult', [
|
| 746 |
+
{ value: d.anomaly_score, label: 'Anomaly Score', color: d.anomaly_score > 70 ? C.danger : C.info, col: 3 },
|
| 747 |
+
{ value: d.is_anomaly ? 'YES' : 'NO', label: 'Anomaly Detected', color: d.is_anomaly ? C.danger : C.success, col: 3 },
|
| 748 |
+
{ value: d.severity, label: 'Severity', color: color, col: 3 },
|
| 749 |
+
{ value: d.anomaly_type, label: 'Anomaly Type', color: C.primary, col: 3,
|
| 750 |
+
detail: d.recommendation }
|
| 751 |
+
]);
|
| 752 |
+
});
|
| 753 |
+
|
| 754 |
+
// === TAB I: SENTIMENT ===
|
| 755 |
+
async function loadSentiment() {
|
| 756 |
+
const res = await fetch('/api/ml/sentiment');
|
| 757 |
+
const d = await res.json();
|
| 758 |
+
document.getElementById('sent-pos').textContent = d.positive + '%';
|
| 759 |
+
document.getElementById('sent-neu').textContent = d.neutral + '%';
|
| 760 |
+
document.getElementById('sent-neg').textContent = d.negative + '%';
|
| 761 |
+
document.getElementById('sent-acc').textContent = d.accuracy + '%';
|
| 762 |
+
doughnut('sentDistChart', ['Positive', 'Neutral', 'Negative'], [d.positive, d.neutral, d.negative], [C.success, C.info, C.danger]);
|
| 763 |
+
lineChart('sentTrendChart', d.months, [
|
| 764 |
+
{ label: 'Positive', data: d.trend_pos, borderColor: C.success, fill: false, tension: 0.4, borderWidth: 2 },
|
| 765 |
+
{ label: 'Negative', data: d.trend_neg, borderColor: C.danger, fill: false, tension: 0.4, borderWidth: 2 }
|
| 766 |
+
]);
|
| 767 |
+
barChart('sentCategoryChart', d.cat_labels, d.cat_values, C.primary, 'Complaints');
|
| 768 |
+
}
|
| 769 |
+
|
| 770 |
+
// I: Manual prediction
|
| 771 |
+
document.getElementById('sentPredForm').addEventListener('submit', async function(e) {
|
| 772 |
+
e.preventDefault();
|
| 773 |
+
const res = await fetch('/api/ml/sentiment/predict', { method: 'POST', headers: {'Content-Type':'application/json'},
|
| 774 |
+
body: JSON.stringify({
|
| 775 |
+
text: document.getElementById('sp-text').value, channel: document.getElementById('sp-channel').value,
|
| 776 |
+
complaint_type: document.getElementById('sp-type').value
|
| 777 |
+
})
|
| 778 |
+
});
|
| 779 |
+
const d = await res.json();
|
| 780 |
+
const color = d.sentiment === 'Positive' ? C.success : d.sentiment === 'Negative' ? C.danger : C.info;
|
| 781 |
+
showPredResult('sentPredResult', [
|
| 782 |
+
{ value: d.sentiment, label: 'Sentiment', color: color, col: 3 },
|
| 783 |
+
{ value: d.confidence + '%', label: 'Confidence', color: d.confidence > 80 ? C.success : C.warning, col: 3 },
|
| 784 |
+
{ value: d.escalation_risk, label: 'Escalation Risk', color: d.escalation_risk === 'High' ? C.danger : d.escalation_risk === 'Medium' ? C.warning : C.success, col: 3 },
|
| 785 |
+
{ value: d.priority, label: 'Priority', color: d.priority === 'Urgent' ? C.danger : d.priority === 'High' ? C.warning : C.info, col: 3,
|
| 786 |
+
detail: 'Key topics: ' + d.topics.join(', ') }
|
| 787 |
+
]);
|
| 788 |
+
});
|
| 789 |
+
|
| 790 |
+
// === TAB J: DEVICE UPGRADE ===
|
| 791 |
+
async function loadDevice() {
|
| 792 |
+
const res = await fetch('/api/ml/device');
|
| 793 |
+
const d = await res.json();
|
| 794 |
+
document.getElementById('dev-acc').textContent = d.accuracy + '%';
|
| 795 |
+
document.getElementById('dev-eligible').textContent = d.eligible.toLocaleString();
|
| 796 |
+
document.getElementById('dev-likely').textContent = d.likely.toLocaleString();
|
| 797 |
+
document.getElementById('dev-rev').textContent = '$' + d.revenue;
|
| 798 |
+
barChart('devAgeChart', d.age_labels, d.age_probs, C.warning, 'Upgrade Prob %');
|
| 799 |
+
barChart('devMfgChart', d.mfg_labels, d.mfg_values, C.primary, 'Likely Upgraders');
|
| 800 |
+
}
|
| 801 |
+
|
| 802 |
+
// J: Manual prediction
|
| 803 |
+
document.getElementById('devPredForm').addEventListener('submit', async function(e) {
|
| 804 |
+
e.preventDefault();
|
| 805 |
+
const res = await fetch('/api/ml/device/predict', { method: 'POST', headers: {'Content-Type':'application/json'},
|
| 806 |
+
body: JSON.stringify({
|
| 807 |
+
device_age: +document.getElementById('jp-age').value, manufacturer: document.getElementById('jp-mfg').value,
|
| 808 |
+
plan: document.getElementById('jp-plan').value, tenure: +document.getElementById('jp-tenure').value,
|
| 809 |
+
usage: +document.getElementById('jp-usage').value
|
| 810 |
+
})
|
| 811 |
+
});
|
| 812 |
+
const d = await res.json();
|
| 813 |
+
const color = d.upgrade_prob > 60 ? C.success : d.upgrade_prob > 30 ? C.warning : '#64748b';
|
| 814 |
+
showPredResult('devPredResult', [
|
| 815 |
+
{ value: d.upgrade_prob + '%', label: 'Upgrade Probability', color: color, col: 3 },
|
| 816 |
+
{ value: d.recommended_device, label: 'Recommended Device', color: C.primary, col: 3 },
|
| 817 |
+
{ value: '$' + d.revenue_opportunity, label: 'Revenue Opportunity', color: C.success, col: 3 },
|
| 818 |
+
{ value: d.best_timing, label: 'Best Timing', color: C.info, col: 3,
|
| 819 |
+
detail: d.reasoning }
|
| 820 |
+
]);
|
| 821 |
+
});
|
| 822 |
+
|
| 823 |
+
// === TAB K: INVESTMENT ===
|
| 824 |
+
async function loadInvestment() {
|
| 825 |
+
const res = await fetch('/api/ml/investment');
|
| 826 |
+
const d = await res.json();
|
| 827 |
+
document.getElementById('inv-roi').textContent = d.roi + '%';
|
| 828 |
+
document.getElementById('inv-budget').textContent = '$' + d.budget;
|
| 829 |
+
document.getElementById('inv-towers').textContent = d.new_towers;
|
| 830 |
+
document.getElementById('inv-upgrade').textContent = d.upgrades;
|
| 831 |
+
horizBar('invRegionChart', d.region_labels, d.region_priority, d.region_priority.map(v => v > 70 ? C.danger : v > 50 ? C.warning : C.success));
|
| 832 |
+
lineChart('invROIChart', d.years, [
|
| 833 |
+
{ label: 'Cumulative ROI %', data: d.roi_projection, borderColor: C.success, backgroundColor: 'rgba(16,185,129,0.1)', fill: true, tension: 0.4, borderWidth: 3 }
|
| 834 |
+
]);
|
| 835 |
+
}
|
| 836 |
+
|
| 837 |
+
// K: Manual prediction
|
| 838 |
+
document.getElementById('invPredForm').addEventListener('submit', async function(e) {
|
| 839 |
+
e.preventDefault();
|
| 840 |
+
const res = await fetch('/api/ml/investment/predict', { method: 'POST', headers: {'Content-Type':'application/json'},
|
| 841 |
+
body: JSON.stringify({
|
| 842 |
+
city: document.getElementById('kp-city').value, population: +document.getElementById('kp-population').value,
|
| 843 |
+
towers: +document.getElementById('kp-towers').value, growth: +document.getElementById('kp-growth').value,
|
| 844 |
+
budget: +document.getElementById('kp-budget').value
|
| 845 |
+
})
|
| 846 |
+
});
|
| 847 |
+
const d = await res.json();
|
| 848 |
+
showPredResult('invPredResult', [
|
| 849 |
+
{ value: d.roi + '%', label: '5-Year ROI', color: d.roi > 100 ? C.success : d.roi > 50 ? C.warning : C.danger, col: 3 },
|
| 850 |
+
{ value: d.priority_score + '/100', label: 'Priority Score', color: d.priority_score > 70 ? C.success : C.warning, col: 3 },
|
| 851 |
+
{ value: d.payback + ' yrs', label: 'Payback Period', color: d.payback <= 3 ? C.success : C.warning, col: 3 },
|
| 852 |
+
{ value: d.new_towers + ' towers', label: 'New Towers Recommended', color: C.primary, col: 3,
|
| 853 |
+
detail: d.recommendation }
|
| 854 |
+
]);
|
| 855 |
+
});
|
| 856 |
+
|
| 857 |
+
// === TAB SHAP ===
|
| 858 |
+
async function loadSHAP() {
|
| 859 |
+
const res = await fetch('/api/ml/shap');
|
| 860 |
+
const d = await res.json();
|
| 861 |
+
horizBar('shapChurnChart', d.churn_features, d.churn_values, d.churn_values.map(v => v > 0 ? C.danger : C.success));
|
| 862 |
+
horizBar('shapLTVChart', d.ltv_features, d.ltv_values, d.ltv_values.map(v => v > 0 ? C.success : C.danger));
|
| 863 |
+
horizBar('shapQualityChart', d.quality_features, d.quality_values, d.quality_values.map(v => v > 0 ? C.danger : C.success));
|
| 864 |
+
new Chart(document.getElementById('modelComparisonChart').getContext('2d'), {
|
| 865 |
+
type: 'bar',
|
| 866 |
+
data: {
|
| 867 |
+
labels: d.model_names,
|
| 868 |
+
datasets: [
|
| 869 |
+
{ label: 'Accuracy', data: d.model_accuracy, backgroundColor: C.success, borderRadius: 4 },
|
| 870 |
+
{ label: 'Precision', data: d.model_precision, backgroundColor: C.primary, borderRadius: 4 },
|
| 871 |
+
{ label: 'Recall', data: d.model_recall, backgroundColor: C.warning, borderRadius: 4 }
|
| 872 |
+
]
|
| 873 |
+
},
|
| 874 |
+
options: { responsive: true, scales: { y: { beginAtZero: true, max: 100, grid: { color: '#f1f5f9' } } } }
|
| 875 |
+
});
|
| 876 |
+
}
|
| 877 |
+
|
| 878 |
+
// === LAZY LOAD ON TAB SWITCH ===
|
| 879 |
+
const loaded = {};
|
| 880 |
+
function lazyLoad(tabId, loadFn) {
|
| 881 |
+
if (!loaded[tabId]) { loadFn(); loaded[tabId] = true; }
|
| 882 |
+
}
|
| 883 |
+
|
| 884 |
+
document.querySelectorAll('#modelTabs .nav-link').forEach(tab => {
|
| 885 |
+
tab.addEventListener('shown.bs.tab', function(e) {
|
| 886 |
+
const target = e.target.getAttribute('href');
|
| 887 |
+
switch(target) {
|
| 888 |
+
case '#tab-network': lazyLoad('network', loadNetwork); break;
|
| 889 |
+
case '#tab-ltv': lazyLoad('ltv', loadLTV); break;
|
| 890 |
+
case '#tab-quality': lazyLoad('quality', loadQualityImpact); break;
|
| 891 |
+
case '#tab-capacity': lazyLoad('capacity', loadCapacity); break;
|
| 892 |
+
case '#tab-demand': lazyLoad('demand', loadDemand); break;
|
| 893 |
+
case '#tab-offers': lazyLoad('offers', loadOffers); break;
|
| 894 |
+
case '#tab-anomaly': lazyLoad('anomaly', loadAnomaly); break;
|
| 895 |
+
case '#tab-sentiment': lazyLoad('sentiment', loadSentiment); break;
|
| 896 |
+
case '#tab-device': lazyLoad('device', loadDevice); break;
|
| 897 |
+
case '#tab-invest': lazyLoad('invest', loadInvestment); break;
|
| 898 |
+
case '#tab-shap': lazyLoad('shap', loadSHAP); break;
|
| 899 |
+
}
|
| 900 |
+
});
|
| 901 |
+
});
|
| 902 |
+
|
| 903 |
+
// Load churn tab immediately
|
| 904 |
+
document.addEventListener('DOMContentLoaded', loadChurn);
|
| 905 |
+
</script>
|
| 906 |
+
{% endblock %}
|
templates/quality.html
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}Service Quality Monitoring - TelecomIQ{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<h2 class="text-primary-custom mb-4">Service Quality Monitoring</h2>
|
| 7 |
+
|
| 8 |
+
<!-- KPIs -->
|
| 9 |
+
<div class="row">
|
| 10 |
+
<div class="col-md-3">
|
| 11 |
+
<div class="metric-card">
|
| 12 |
+
<h6>AVG DOWNLOAD SPEED <button class="info-btn" data-info-icon="⬇️" data-info-title="Average Download Speed" data-info-section="Quality · Speed KPI" data-info="The mean download speed (Mbps) experienced across all customers. Download speed directly impacts streaming, browsing, and app performance." data-info-tips="4G LTE average: 20–50 Mbps. 5G: 100–1000 Mbps.|Speed below 5 Mbps causes buffering on HD video — leads to complaints.|Compare peak vs off-peak speeds to identify congestion.">ⓘ</button></h6>
|
| 13 |
+
<h3 class="text-primary-custom" id="kpi-download">-</h3>
|
| 14 |
+
<small>Mbps (all customers)</small>
|
| 15 |
+
</div>
|
| 16 |
+
</div>
|
| 17 |
+
<div class="col-md-3">
|
| 18 |
+
<div class="metric-card">
|
| 19 |
+
<h6>AVG UPLOAD SPEED <button class="info-btn" data-info-icon="⬆️" data-info-title="Average Upload Speed" data-info-section="Quality · Speed KPI" data-info="The mean upload speed (Mbps) across all customers. Upload speed matters for video calls, cloud syncing, remote work, and gaming." data-info-tips="Generally 30–50% of download speed on LTE networks.|Always-on upload use cases (video conferencing) are increasingly critical.|Symmetric speeds important for business/enterprise customers.">ⓘ</button></h6>
|
| 20 |
+
<h3 class="text-primary-custom" id="kpi-upload">-</h3>
|
| 21 |
+
<small>Mbps (all customers)</small>
|
| 22 |
+
</div>
|
| 23 |
+
</div>
|
| 24 |
+
<div class="col-md-3">
|
| 25 |
+
<div class="metric-card">
|
| 26 |
+
<h6>NETWORK RELIABILITY <button class="info-btn" data-info-icon="✅" data-info-title="Network Reliability (Uptime %)" data-info-section="Quality · Reliability KPI" data-info="The percentage of time the network is fully available and operational. Combines tower uptime, core network availability, and backhaul reliability." data-info-tips="Target: ≥99.9% reliability (3 nines).|0.1% downtime = ~8.7 hours/year permissible.|Below 99.5% = ~44 hours/year — SLA penalty risk for enterprise contracts.">ⓘ</button></h6>
|
| 27 |
+
<h3 class="text-success-custom" id="kpi-reliability">-</h3>
|
| 28 |
+
<small>Uptime percentage</small>
|
| 29 |
+
</div>
|
| 30 |
+
</div>
|
| 31 |
+
<div class="col-md-3">
|
| 32 |
+
<div class="metric-card">
|
| 33 |
+
<h6>AVG CALL DROP RATE <button class="info-btn" data-info-icon="📵" data-info-title="Average Call Drop Rate" data-info-section="Quality · Voice KPI" data-info="The percentage of established voice calls that are unintentionally disconnected before completion. A key voice quality metric and major driver of customer complaints." data-info-tips="Target: <2% call drop rate.|Above 3% = noticeable customer impact and likely NPS score drops.|Common causes: weak coverage, handover failure, interference.">ⓘ</button></h6>
|
| 34 |
+
<h3 class="text-warning-custom" id="kpi-drop-rate">-</h3>
|
| 35 |
+
<small>Target: < 2%</small>
|
| 36 |
+
</div>
|
| 37 |
+
</div>
|
| 38 |
+
</div>
|
| 39 |
+
|
| 40 |
+
<div class="row">
|
| 41 |
+
<div class="col-md-3"><div class="metric-card">
|
| 42 |
+
<h6>DATA SESSIONS/DAY <button class="info-btn" data-info-icon="📱" data-info-title="Daily Data Sessions per Customer" data-info-section="Quality · Usage KPI" data-info="Average number of distinct data sessions initiated per customer per day. Reflects how actively customers use mobile data services." data-info-tips="4+ sessions/day = heavy data user — at-risk if speeds are poor.|Declining sessions may indicate customers using WiFi instead.|Monitor by plan type to see premium vs basic usage patterns.">ⓘ</button></h6>
|
| 43 |
+
<h3 class="text-primary-custom" id="kpi-sessions">-</h3><small>Avg per customer</small>
|
| 44 |
+
</div></div>
|
| 45 |
+
<div class="col-md-3"><div class="metric-card">
|
| 46 |
+
<h6>STREAMING QUALITY <button class="info-btn" data-info-icon="🎬" data-info-title="Streaming MOS Score (1–5)" data-info-section="Quality · Streaming KPI" data-info="Mean Opinion Score (MOS) for streaming services, rated 1–5 where 5 is excellent. Combines buffering rate, startup time, and resolution quality into a single user experience score." data-info-tips="MOS ≥4.0 = Excellent streaming experience.|MOS 3.0–3.9 = Acceptable but some users notice quality.|MOS <3.0 = Poor — expect increased streaming complaints.">ⓘ</button></h6>
|
| 47 |
+
<h3 class="text-success-custom" id="kpi-streaming">-</h3><small>MOS score (1-5)</small>
|
| 48 |
+
</div></div>
|
| 49 |
+
<div class="col-md-3"><div class="metric-card">
|
| 50 |
+
<h6>COVERAGE SCORE <button class="info-btn" data-info-icon="🗺️" data-info-title="National Coverage Score" data-info-section="Quality · Coverage KPI" data-info="A composite score (%) representing the proportion of the national geography or population that receives adequate signal strength from the network." data-info-tips="Population coverage >95% is typical for major carriers.|Geographic coverage always lower than population coverage.|Coverage gaps in rural areas are common expansion targets.">ⓘ</button></h6>
|
| 51 |
+
<h3 class="text-success-custom" id="kpi-coverage">-</h3><small>National average</small>
|
| 52 |
+
</div></div>
|
| 53 |
+
<div class="col-md-3"><div class="metric-card">
|
| 54 |
+
<h6>QUALITY COMPLAINTS <button class="info-btn" data-info-icon="📢" data-info-title="Quality-Related Complaints" data-info-section="Quality · Complaint KPI" data-info="Total number of customer service contacts specifically citing a network or service quality issue — slow speeds, dropped calls, coverage gaps, or streaming problems." data-info-tips="Rising quality complaints = early warning of network degradation.|Cluster complaints geographically to find affected cell zones.|Quality complaints correlate strongly with CSAT drops and churn risk.">ⓘ</button></h6>
|
| 55 |
+
<h3 class="text-danger-custom" id="kpi-complaints">-</h3><small>This period</small>
|
| 56 |
+
</div></div>
|
| 57 |
+
</div>
|
| 58 |
+
|
| 59 |
+
<div class="row mt-4">
|
| 60 |
+
<div class="col-md-6"><div class="chart-card">
|
| 61 |
+
<h5 class="text-primary-custom mb-3">Speed Test Trends (Monthly) <button class="info-btn" data-info-icon="📶" data-info-title="Speed Test Trends" data-info-section="Quality · Speed Chart" data-info="A dual-line chart plotting average download and upload speeds month-over-month. Reveals network improvement trends and seasonal degradation patterns." data-info-tips="Consistent upward trend = successful capacity investments.|Speed drops in summer often indicate holiday traffic spikes.|Use to validate the ROI of network upgrade programmes.">ⓘ</button></h5>
|
| 62 |
+
<canvas id="speedTrendChart"></canvas>
|
| 63 |
+
</div></div>
|
| 64 |
+
<div class="col-md-6"><div class="chart-card">
|
| 65 |
+
<h5 class="text-primary-custom mb-3">Quality by Service Type <button class="info-btn" data-info-icon="🕸️" data-info-title="Quality by Service Type (Radar)" data-info-section="Quality · Multi-Dimension Chart" data-info="A radar chart comparing quality dimensions (speed, reliability, coverage, latency, MOS, drop rate) across different plan tiers — Basic, Standard, Premium, Enterprise." data-info-tips="Higher-tier plans should show consistently better quality scores.|Large gaps between plan tiers may indicate pricing justification.|Use to design SLA commitments for each plan level.">ⓘ</button></h5>
|
| 66 |
+
<canvas id="qualityByServiceChart"></canvas>
|
| 67 |
+
</div></div>
|
| 68 |
+
</div>
|
| 69 |
+
|
| 70 |
+
<div class="row">
|
| 71 |
+
<div class="col-md-6"><div class="chart-card">
|
| 72 |
+
<h5 class="text-primary-custom mb-3">Customer vs Network Quality Correlation <button class="info-btn" data-info-icon="🔗" data-info-title="Customer Satisfaction vs Network Quality Correlation" data-info-section="Quality · Scatter Chart" data-info="A scatter plot showing individual customers plotted by their network quality score (x-axis) vs their customer satisfaction score (y-axis). Helps prove the causal link between network quality and CSAT." data-info-tips="Strong upward trend = network quality directly drives satisfaction.|Outliers at high quality/low CSAT suggest billing or support issues.|Use R² value to quantify how much CSAT is driven by network vs other factors.">ⓘ</button></h5>
|
| 73 |
+
<canvas id="correlationChart"></canvas>
|
| 74 |
+
</div></div>
|
| 75 |
+
<div class="col-md-6"><div class="chart-card">
|
| 76 |
+
<h5 class="text-primary-custom mb-3">Quality Complaint Categories <button class="info-btn" data-info-icon="📢" data-info-title="Quality Complaint Category Breakdown" data-info-section="Quality · Complaint Chart" data-info="A doughnut chart splitting quality complaints into categories such as Call Drop, Slow Data, Coverage, Connection Failure, Network Timeout, and Poor Voice Quality." data-info-tips="Top category = primary network investment priority.|Slow Data + Low Coverage = capacity expansion needed.|Call Drop + Handover Failure = radio planning issue.">ⓘ</button></h5>
|
| 77 |
+
<canvas id="qualityComplaintChart"></canvas>
|
| 78 |
+
</div></div>
|
| 79 |
+
</div>
|
| 80 |
+
|
| 81 |
+
{% endblock %}
|
| 82 |
+
|
| 83 |
+
{% block extra_js %}
|
| 84 |
+
<script>
|
| 85 |
+
Chart.defaults.color = '#64748b';
|
| 86 |
+
Chart.defaults.borderColor = '#e2e8f0';
|
| 87 |
+
Chart.defaults.font.family = "'Inter', sans-serif";
|
| 88 |
+
|
| 89 |
+
const COLORS = {
|
| 90 |
+
primary: '#4f46e5',
|
| 91 |
+
success: '#10b981',
|
| 92 |
+
warning: '#f59e0b',
|
| 93 |
+
danger: '#ef4444',
|
| 94 |
+
info: '#3b82f6'
|
| 95 |
+
};
|
| 96 |
+
|
| 97 |
+
async function loadQualityKPIs() {
|
| 98 |
+
try {
|
| 99 |
+
const response = await fetch('/api/quality/kpis');
|
| 100 |
+
const data = await response.json();
|
| 101 |
+
|
| 102 |
+
document.getElementById('kpi-download').textContent = `${data.avg_download} Mbps`;
|
| 103 |
+
document.getElementById('kpi-upload').textContent = `${data.avg_upload} Mbps`;
|
| 104 |
+
document.getElementById('kpi-reliability').textContent = `${data.reliability}%`;
|
| 105 |
+
document.getElementById('kpi-drop-rate').textContent = `${data.drop_rate}%`;
|
| 106 |
+
document.getElementById('kpi-sessions').textContent = data.avg_sessions;
|
| 107 |
+
document.getElementById('kpi-streaming').textContent = `${data.streaming_mos}/5`;
|
| 108 |
+
document.getElementById('kpi-coverage').textContent = `${data.coverage_score}%`;
|
| 109 |
+
document.getElementById('kpi-complaints').textContent = data.quality_complaints.toLocaleString();
|
| 110 |
+
} catch (error) {
|
| 111 |
+
console.error('Error loading quality KPIs:', error);
|
| 112 |
+
}
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
async function loadSpeedTrend() {
|
| 116 |
+
try {
|
| 117 |
+
const response = await fetch('/api/quality/speed-trend');
|
| 118 |
+
const data = await response.json();
|
| 119 |
+
|
| 120 |
+
const ctx = document.getElementById('speedTrendChart').getContext('2d');
|
| 121 |
+
new Chart(ctx, {
|
| 122 |
+
type: 'line',
|
| 123 |
+
data: {
|
| 124 |
+
labels: data.months,
|
| 125 |
+
datasets: [
|
| 126 |
+
{
|
| 127 |
+
label: 'Download (Mbps)',
|
| 128 |
+
data: data.download,
|
| 129 |
+
borderColor: COLORS.primary,
|
| 130 |
+
backgroundColor: 'rgba(79, 70, 229, 0.1)',
|
| 131 |
+
fill: true,
|
| 132 |
+
tension: 0.4,
|
| 133 |
+
borderWidth: 3
|
| 134 |
+
},
|
| 135 |
+
{
|
| 136 |
+
label: 'Upload (Mbps)',
|
| 137 |
+
data: data.upload,
|
| 138 |
+
borderColor: COLORS.success,
|
| 139 |
+
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
| 140 |
+
fill: true,
|
| 141 |
+
tension: 0.4,
|
| 142 |
+
borderWidth: 3
|
| 143 |
+
}
|
| 144 |
+
]
|
| 145 |
+
},
|
| 146 |
+
options: {
|
| 147 |
+
responsive: true,
|
| 148 |
+
scales: {
|
| 149 |
+
y: { beginAtZero: true, grid: { color: '#f1f5f9' } }
|
| 150 |
+
}
|
| 151 |
+
}
|
| 152 |
+
});
|
| 153 |
+
} catch (error) {
|
| 154 |
+
console.error('Error loading speed trend:', error);
|
| 155 |
+
}
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
async function loadQualityByService() {
|
| 159 |
+
try {
|
| 160 |
+
const response = await fetch('/api/quality/by-service');
|
| 161 |
+
const data = await response.json();
|
| 162 |
+
|
| 163 |
+
const ctx = document.getElementById('qualityByServiceChart').getContext('2d');
|
| 164 |
+
new Chart(ctx, {
|
| 165 |
+
type: 'radar',
|
| 166 |
+
data: {
|
| 167 |
+
labels: data.metrics,
|
| 168 |
+
datasets: data.plans.map((plan, i) => ({
|
| 169 |
+
label: plan.name,
|
| 170 |
+
data: plan.scores,
|
| 171 |
+
borderColor: [COLORS.primary, COLORS.success, COLORS.warning, COLORS.danger][i],
|
| 172 |
+
backgroundColor: 'transparent',
|
| 173 |
+
borderWidth: 2,
|
| 174 |
+
pointBackgroundColor: [COLORS.primary, COLORS.success, COLORS.warning, COLORS.danger][i]
|
| 175 |
+
}))
|
| 176 |
+
},
|
| 177 |
+
options: {
|
| 178 |
+
responsive: true,
|
| 179 |
+
scales: {
|
| 180 |
+
r: { min: 0, max: 10, ticks: { stepSize: 2 }, grid: { color: '#e2e8f0' } }
|
| 181 |
+
}
|
| 182 |
+
}
|
| 183 |
+
});
|
| 184 |
+
} catch (error) {
|
| 185 |
+
console.error('Error loading quality by service:', error);
|
| 186 |
+
}
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
async function loadCorrelationChart() {
|
| 190 |
+
try {
|
| 191 |
+
const response = await fetch('/api/quality/correlation');
|
| 192 |
+
const data = await response.json();
|
| 193 |
+
|
| 194 |
+
const ctx = document.getElementById('correlationChart').getContext('2d');
|
| 195 |
+
new Chart(ctx, {
|
| 196 |
+
type: 'scatter',
|
| 197 |
+
data: {
|
| 198 |
+
datasets: [{
|
| 199 |
+
label: 'Customer Satisfaction vs Network Score',
|
| 200 |
+
data: data.points,
|
| 201 |
+
backgroundColor: COLORS.primary,
|
| 202 |
+
pointRadius: 4
|
| 203 |
+
}]
|
| 204 |
+
},
|
| 205 |
+
options: {
|
| 206 |
+
responsive: true,
|
| 207 |
+
plugins: { legend: { display: false } },
|
| 208 |
+
scales: {
|
| 209 |
+
x: { title: { display: true, text: 'Network Quality Score' }, grid: { color: '#f1f5f9' } },
|
| 210 |
+
y: { title: { display: true, text: 'Customer Satisfaction' }, grid: { color: '#f1f5f9' } }
|
| 211 |
+
}
|
| 212 |
+
}
|
| 213 |
+
});
|
| 214 |
+
} catch (error) {
|
| 215 |
+
console.error('Error loading correlation chart:', error);
|
| 216 |
+
}
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
async function loadQualityComplaintChart() {
|
| 220 |
+
try {
|
| 221 |
+
const response = await fetch('/api/quality/complaint-categories');
|
| 222 |
+
const data = await response.json();
|
| 223 |
+
|
| 224 |
+
const ctx = document.getElementById('qualityComplaintChart').getContext('2d');
|
| 225 |
+
new Chart(ctx, {
|
| 226 |
+
type: 'doughnut',
|
| 227 |
+
data: {
|
| 228 |
+
labels: data.labels,
|
| 229 |
+
datasets: [{
|
| 230 |
+
data: data.values,
|
| 231 |
+
backgroundColor: [COLORS.danger, COLORS.warning, COLORS.info, COLORS.primary, COLORS.success],
|
| 232 |
+
borderWidth: 4,
|
| 233 |
+
borderColor: '#ffffff'
|
| 234 |
+
}]
|
| 235 |
+
},
|
| 236 |
+
options: {
|
| 237 |
+
responsive: true,
|
| 238 |
+
plugins: { legend: { position: 'right' } }
|
| 239 |
+
}
|
| 240 |
+
});
|
| 241 |
+
} catch (error) {
|
| 242 |
+
console.error('Error loading quality complaint chart:', error);
|
| 243 |
+
}
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 247 |
+
loadQualityKPIs();
|
| 248 |
+
loadSpeedTrend();
|
| 249 |
+
loadQualityByService();
|
| 250 |
+
loadCorrelationChart();
|
| 251 |
+
loadQualityComplaintChart();
|
| 252 |
+
});
|
| 253 |
+
</script>
|
| 254 |
+
{% endblock %}
|
templates/segmentation.html
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}Customer Segmentation - TelecomIQ{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block extra_css %}
|
| 6 |
+
<style>
|
| 7 |
+
.segment-table { width: 100%; border-collapse: separate; border-spacing: 0; }
|
| 8 |
+
.segment-table th { background: var(--primary-color); color: white; padding: 12px 16px; font-size: 0.8rem; text-transform: uppercase; }
|
| 9 |
+
.segment-table td { padding: 10px 16px; border-bottom: 1px solid var(--border-color); font-size: 0.9rem; }
|
| 10 |
+
.segment-table tr:hover { background: #f1f5f9; }
|
| 11 |
+
.migration-arrow { font-size: 1.2rem; color: var(--primary-color); }
|
| 12 |
+
</style>
|
| 13 |
+
{% endblock %}
|
| 14 |
+
|
| 15 |
+
{% block content %}
|
| 16 |
+
<h2 class="text-primary-custom mb-4">Customer Segmentation Analytics</h2>
|
| 17 |
+
|
| 18 |
+
<div class="row">
|
| 19 |
+
<div class="col-md-3"><div class="metric-card">
|
| 20 |
+
<h6>PREMIUM CUSTOMERS <button class="info-btn" data-info-icon="💎" data-info-title="Premium Segment Count" data-info-section="Segmentation · Value KPI" data-info="Number of customers classified in the high-value segment based on ARPU, tenure, and loyalty score. These are your most profitable and loyal customers." data-info-tips="Typically top 15–25% of customers by revenue contribution.|Assign dedicated account managers or VIP lines.|High-value customers have 3× higher LTV than average.">ⓘ</button></h6>
|
| 21 |
+
<h3 class="text-success-custom" id="kpi-premium">-</h3><small>High-value segment</small>
|
| 22 |
+
</div></div>
|
| 23 |
+
<div class="col-md-3"><div class="metric-card">
|
| 24 |
+
<h6>AT-RISK SEGMENT <button class="info-btn" data-info-icon="⚠️" data-info-title="At-Risk Customer Segment" data-info-section="Segmentation · Risk KPI" data-info="Customers who show early indicators of churn risk such as declining usage, increasing complaints, or approaching contract end. Requires proactive intervention." data-info-tips="Target this segment with win-back incentives before they churn.|Usage drop >30% MoM is a strong leading indicator of churn.|At-risk customers who receive an offer are 45% more likely to stay.">ⓘ</button></h6>
|
| 25 |
+
<h3 class="text-danger-custom" id="kpi-at-risk">-</h3><small>Need attention</small>
|
| 26 |
+
</div></div>
|
| 27 |
+
<div class="col-md-3"><div class="metric-card">
|
| 28 |
+
<h6>UPSELL OPPORTUNITY <button class="info-btn" data-info-icon="📈" data-info-title="Upsell Opportunity Segment" data-info-section="Segmentation · Growth KPI" data-info="Customers on lower-tier plans who exhibit usage patterns consistent with the next plan tier up. Likely to accept an upgrade offer without significant churn risk." data-info-tips="Customers using >90% of their data allowance each month are prime upsell targets.|Upsell conversion rates: 15–25% with targeted offers.|Upselling is 5× cheaper than acquiring a new customer.">ⓘ</button></h6>
|
| 29 |
+
<h3 class="text-warning-custom" id="kpi-upsell">-</h3><small>Ready for upgrade</small>
|
| 30 |
+
</div></div>
|
| 31 |
+
<div class="col-md-3"><div class="metric-card">
|
| 32 |
+
<h6>AVG TENURE <button class="info-btn" data-info-icon="📅" data-info-title="Average Customer Tenure" data-info-section="Segmentation · Loyalty KPI" data-info="The mean number of months customers across all segments have been subscribed. Longer tenure indicates higher loyalty and typically higher LTV." data-info-tips="Target average tenure: >24 months across the base.|Rising tenure = improving retention programme effectiveness.|New customer cohorts (0–6 months) need most attention to survive to 12 months.">ⓘ</button></h6>
|
| 33 |
+
<h3 class="text-primary-custom" id="kpi-tenure">-</h3><small>Months (all segments)</small>
|
| 34 |
+
</div></div>
|
| 35 |
+
</div>
|
| 36 |
+
|
| 37 |
+
<div class="row mt-4">
|
| 38 |
+
<div class="col-md-6"><div class="chart-card">
|
| 39 |
+
<h5 class="text-primary-custom mb-3">Value vs. Tenure Segmentation Matrix <button class="info-btn" data-info-icon="🔮" data-info-title="Value vs Tenure Segmentation Matrix" data-info-section="Segmentation · Matrix Chart" data-info="A bubble chart plotting customer segments by average tenure (X-axis) vs average monthly revenue (Y-axis). Bubble size represents the number of customers in each segment." data-info-tips="Top-right = Champions (high tenure + high revenue) — protect these.|Bottom-left = New/Low-value — invest in onboarding and upsell.|Use quadrant strategy: different retention approaches per quadrant.">ⓘ</button></h5>
|
| 40 |
+
<canvas id="segmentMatrixChart"></canvas>
|
| 41 |
+
</div></div>
|
| 42 |
+
<div class="col-md-6"><div class="chart-card">
|
| 43 |
+
<h5 class="text-primary-custom mb-3">Customer Lifecycle Stages <button class="info-btn" data-info-icon="🔄" data-info-title="Customer Lifecycle Stage Distribution" data-info-section="Segmentation · Lifecycle Chart" data-info="A bar chart showing how many customers are in each stage of the customer lifecycle: New (0–6m), Growing (6–18m), Mature (18–36m), and Champion (36m+)." data-info-tips="A healthy base has a large Mature segment (reduces overall churn risk).|High New segment = growth momentum but also highest churn risk window.|Plan lifecycle-specific communications for each stage.">ⓘ</button></h5>
|
| 44 |
+
<canvas id="lifecycleChart"></canvas>
|
| 45 |
+
</div></div>
|
| 46 |
+
</div>
|
| 47 |
+
|
| 48 |
+
<div class="row">
|
| 49 |
+
<div class="col-md-6"><div class="chart-card">
|
| 50 |
+
<h5 class="text-primary-custom mb-3">Plan Migration Patterns <button class="info-btn" data-info-icon="⇅" data-info-title="Plan Migration Patterns" data-info-section="Segmentation · Migration Chart" data-info="A grouped bar chart showing the number of customers who upgraded vs downgraded their plan in each period, broken down by source plan type." data-info-tips="More upgrades than downgrades = healthy revenue expansion.|Frequent downgrades from Premium = value perception problem.|Customers who downgrade are 2× more likely to churn within 6 months.">ⓘ</button></h5>
|
| 51 |
+
<canvas id="migrationChart"></canvas>
|
| 52 |
+
</div></div>
|
| 53 |
+
<div class="col-md-6"><div class="chart-card">
|
| 54 |
+
<h5 class="text-primary-custom mb-3">Demographic Profiling <button class="info-btn" data-info-icon="👥" data-info-title="Customer Demographic Profiling" data-info-section="Segmentation · Demographics Chart" data-info="A bar chart showing the age distribution of the customer base. Demographic breakdown helps tailor marketing messaging, plan design, and channel strategy." data-info-tips="18–34 age group: digital-first, higher data usage, price-sensitive.|35–54: family plan segment and multi-line opportunities.|55+: typically loyal, lower churn, voice-heavy usage patterns.">ⓘ</button></h5>
|
| 55 |
+
<canvas id="demographicChart"></canvas>
|
| 56 |
+
</div></div>
|
| 57 |
+
</div>
|
| 58 |
+
|
| 59 |
+
<div class="row">
|
| 60 |
+
<div class="col-md-12"><div class="chart-card">
|
| 61 |
+
<h5 class="text-primary-custom mb-3">Segment Performance Detail <button class="info-btn" data-info-icon="📋" data-info-title="Segment Performance Detail Table" data-info-section="Segmentation · Detail Table" data-info="A detailed data table showing each plan segment's customer count, average tenure, average monthly cost, churn rate, and upsell score. Provides an at-a-glance view to compare segment health." data-info-tips="Sort by churn rate to find the most vulnerable segments.|High upsell score + low churn = safe to run upgrade campaigns.|Low tenure + high churn rate = new customer onboarding failure.">ⓘ</button></h5>
|
| 62 |
+
<div style="max-height: 350px; overflow-y: auto;">
|
| 63 |
+
<table class="segment-table">
|
| 64 |
+
<thead>
|
| 65 |
+
<tr><th>Plan Type</th><th>Customers</th><th>Avg Tenure</th><th>Avg Monthly Cost</th><th>Churn Rate</th><th>Upsell Score</th></tr>
|
| 66 |
+
</thead>
|
| 67 |
+
<tbody id="segmentTableBody"></tbody>
|
| 68 |
+
</table>
|
| 69 |
+
</div>
|
| 70 |
+
</div></div>
|
| 71 |
+
</div>
|
| 72 |
+
|
| 73 |
+
{% endblock %}
|
| 74 |
+
|
| 75 |
+
{% block extra_js %}
|
| 76 |
+
<script>
|
| 77 |
+
Chart.defaults.color = '#64748b';
|
| 78 |
+
Chart.defaults.borderColor = '#e2e8f0';
|
| 79 |
+
Chart.defaults.font.family = "'Inter', sans-serif";
|
| 80 |
+
|
| 81 |
+
const COLORS = {
|
| 82 |
+
primary: '#4f46e5',
|
| 83 |
+
success: '#10b981',
|
| 84 |
+
warning: '#f59e0b',
|
| 85 |
+
danger: '#ef4444',
|
| 86 |
+
info: '#3b82f6',
|
| 87 |
+
purple: '#8b5cf6'
|
| 88 |
+
};
|
| 89 |
+
|
| 90 |
+
async function loadSegmentationData() {
|
| 91 |
+
try {
|
| 92 |
+
const response = await fetch('/api/segmentation/overview');
|
| 93 |
+
const data = await response.json();
|
| 94 |
+
|
| 95 |
+
document.getElementById('kpi-premium').textContent = data.premium_count.toLocaleString();
|
| 96 |
+
document.getElementById('kpi-at-risk').textContent = data.at_risk_count.toLocaleString();
|
| 97 |
+
document.getElementById('kpi-upsell').textContent = data.upsell_count.toLocaleString();
|
| 98 |
+
document.getElementById('kpi-tenure').textContent = `${data.avg_tenure} mo`;
|
| 99 |
+
|
| 100 |
+
// Segment table
|
| 101 |
+
const tbody = document.getElementById('segmentTableBody');
|
| 102 |
+
tbody.innerHTML = '';
|
| 103 |
+
data.segments.forEach(seg => {
|
| 104 |
+
tbody.innerHTML += `<tr>
|
| 105 |
+
<td><strong>${seg.plan}</strong></td>
|
| 106 |
+
<td>${seg.count.toLocaleString()}</td>
|
| 107 |
+
<td>${seg.avg_tenure} mo</td>
|
| 108 |
+
<td>$${seg.avg_cost}</td>
|
| 109 |
+
<td style="color: ${seg.churn_rate > 15 ? '#ef4444' : seg.churn_rate > 10 ? '#f59e0b' : '#10b981'}">${seg.churn_rate}%</td>
|
| 110 |
+
<td>${seg.upsell_score}</td>
|
| 111 |
+
</tr>`;
|
| 112 |
+
});
|
| 113 |
+
|
| 114 |
+
} catch (error) {
|
| 115 |
+
console.error('Error loading segmentation data:', error);
|
| 116 |
+
}
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
async function loadSegmentMatrix() {
|
| 120 |
+
try {
|
| 121 |
+
const response = await fetch('/api/segmentation/matrix');
|
| 122 |
+
const data = await response.json();
|
| 123 |
+
|
| 124 |
+
const ctx = document.getElementById('segmentMatrixChart').getContext('2d');
|
| 125 |
+
new Chart(ctx, {
|
| 126 |
+
type: 'bubble',
|
| 127 |
+
data: {
|
| 128 |
+
datasets: data.segments.map((seg, i) => ({
|
| 129 |
+
label: seg.label,
|
| 130 |
+
data: [{ x: seg.avg_tenure, y: seg.avg_revenue, r: Math.max(seg.count / 500, 5) }],
|
| 131 |
+
backgroundColor: [COLORS.primary, COLORS.success, COLORS.warning, COLORS.danger, COLORS.info, COLORS.purple][i % 6]
|
| 132 |
+
}))
|
| 133 |
+
},
|
| 134 |
+
options: {
|
| 135 |
+
responsive: true,
|
| 136 |
+
scales: {
|
| 137 |
+
x: { title: { display: true, text: 'Avg Tenure (months)' }, grid: { color: '#f1f5f9' } },
|
| 138 |
+
y: { title: { display: true, text: 'Avg Monthly Revenue ($)' }, grid: { color: '#f1f5f9' } }
|
| 139 |
+
}
|
| 140 |
+
}
|
| 141 |
+
});
|
| 142 |
+
} catch (error) {
|
| 143 |
+
console.error('Error loading segment matrix:', error);
|
| 144 |
+
}
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
async function loadLifecycleChart() {
|
| 148 |
+
try {
|
| 149 |
+
const response = await fetch('/api/segmentation/lifecycle');
|
| 150 |
+
const data = await response.json();
|
| 151 |
+
|
| 152 |
+
const ctx = document.getElementById('lifecycleChart').getContext('2d');
|
| 153 |
+
new Chart(ctx, {
|
| 154 |
+
type: 'bar',
|
| 155 |
+
data: {
|
| 156 |
+
labels: data.labels,
|
| 157 |
+
datasets: [{
|
| 158 |
+
label: 'Customers',
|
| 159 |
+
data: data.values,
|
| 160 |
+
backgroundColor: [COLORS.info, COLORS.success, COLORS.primary, COLORS.warning],
|
| 161 |
+
borderRadius: 8
|
| 162 |
+
}]
|
| 163 |
+
},
|
| 164 |
+
options: {
|
| 165 |
+
responsive: true,
|
| 166 |
+
plugins: { legend: { display: false } },
|
| 167 |
+
scales: { y: { beginAtZero: true, grid: { color: '#f1f5f9' } } }
|
| 168 |
+
}
|
| 169 |
+
});
|
| 170 |
+
} catch (error) {
|
| 171 |
+
console.error('Error loading lifecycle chart:', error);
|
| 172 |
+
}
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
async function loadMigrationChart() {
|
| 176 |
+
try {
|
| 177 |
+
const response = await fetch('/api/segmentation/migration');
|
| 178 |
+
const data = await response.json();
|
| 179 |
+
|
| 180 |
+
const ctx = document.getElementById('migrationChart').getContext('2d');
|
| 181 |
+
new Chart(ctx, {
|
| 182 |
+
type: 'bar',
|
| 183 |
+
data: {
|
| 184 |
+
labels: data.labels,
|
| 185 |
+
datasets: [
|
| 186 |
+
{ label: 'Upgrades', data: data.upgrades, backgroundColor: COLORS.success, borderRadius: 6 },
|
| 187 |
+
{ label: 'Downgrades', data: data.downgrades, backgroundColor: COLORS.danger, borderRadius: 6 }
|
| 188 |
+
]
|
| 189 |
+
},
|
| 190 |
+
options: {
|
| 191 |
+
responsive: true,
|
| 192 |
+
scales: { y: { beginAtZero: true, grid: { color: '#f1f5f9' } } }
|
| 193 |
+
}
|
| 194 |
+
});
|
| 195 |
+
} catch (error) {
|
| 196 |
+
console.error('Error loading migration chart:', error);
|
| 197 |
+
}
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
async function loadDemographicChart() {
|
| 201 |
+
try {
|
| 202 |
+
const response = await fetch('/api/segmentation/demographics');
|
| 203 |
+
const data = await response.json();
|
| 204 |
+
|
| 205 |
+
const ctx = document.getElementById('demographicChart').getContext('2d');
|
| 206 |
+
new Chart(ctx, {
|
| 207 |
+
type: 'bar',
|
| 208 |
+
data: {
|
| 209 |
+
labels: data.age_groups,
|
| 210 |
+
datasets: [{
|
| 211 |
+
label: 'Customers',
|
| 212 |
+
data: data.counts,
|
| 213 |
+
backgroundColor: COLORS.primary,
|
| 214 |
+
borderRadius: 8
|
| 215 |
+
}]
|
| 216 |
+
},
|
| 217 |
+
options: {
|
| 218 |
+
responsive: true,
|
| 219 |
+
plugins: { legend: { display: false } },
|
| 220 |
+
scales: { y: { beginAtZero: true, grid: { color: '#f1f5f9' } } }
|
| 221 |
+
}
|
| 222 |
+
});
|
| 223 |
+
} catch (error) {
|
| 224 |
+
console.error('Error loading demographic chart:', error);
|
| 225 |
+
}
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 229 |
+
loadSegmentationData();
|
| 230 |
+
loadSegmentMatrix();
|
| 231 |
+
loadLifecycleChart();
|
| 232 |
+
loadMigrationChart();
|
| 233 |
+
loadDemographicChart();
|
| 234 |
+
});
|
| 235 |
+
</script>
|
| 236 |
+
{% endblock %}
|