{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceType":"competition","sourceId":7163,"databundleVersionId":44582},{"sourceType":"kernelVersion","sourceId":306827609}],"dockerImageVersionId":31328,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"'''import pandas as pd\nimport numpy as np\nimport joblib\nimport os\nimport plotly.graph_objects as go\nimport plotly.express as px\nimport subprocess\n\n# 0. SETUP DỮ LIỆU\npath = '/kaggle/input/competitions/kkbox-churn-prediction-challenge/'\n\ndef extract_all_7z_to_csv(file_list):\n    for file_7z in file_list:\n        file_csv = file_7z.replace('.7z', '')\n        if not os.path.exists(f'/kaggle/working/{file_csv}'):\n            print(f\"Đang bắt đầu giải nén: {file_7z} ...\")\n            try:\n                subprocess.run(['7z', 'e', path + file_7z, '-o/kaggle/working/'], check=True)\n                print(f\"THÀNH CÔNG: Đã tạo file {file_csv}\")\n            except subprocess.CalledProcessError as e:\n                print(f\"LỖI khi xử lý {file_7z}: {e}\")\n        else:\n            print(f\"File {file_csv} đã tồn tại, bỏ qua giải nén.\")\n\nfiles_to_extract =[\n    'train_v2.csv.7z',\n    'members_v3.csv.7z',\n    'transactions_v2.csv.7z',\n    'user_logs_v2.csv.7z'\n]\n\nextract_all_7z_to_csv(files_to_extract)\n\nINPUT_PATH = '/kaggle/working/'\n\nprint(\"\\n--- ĐANG NẠP DỮ LIỆU THÔ ---\")\ntrain = pd.read_csv(f'{INPUT_PATH}train_v2.csv')\nmembers = pd.read_csv(f'{INPUT_PATH}members_v3.csv')\ntransactions = pd.read_csv(f'{INPUT_PATH}transactions_v2.csv')\n\nunique_users = train['msno'].unique()\nlogs = pd.read_csv(f'{INPUT_PATH}user_logs_v2.csv', nrows=5000000) \nlogs = logs[logs['msno'].isin(unique_users)]\n\nif not os.path.exists('/kaggle/input/notebooks/zile228/bi-project-predictive-analysis/kkbox_cox_model.pkl'):\n    class MockModel:\n        def __init__(self):\n            self.params_ = {\n                'Female': 0.01,\n                'Age_15-20': 0.27,\n                'Age_31-45': -0.06,\n                'Price_Deal (<4.5d)': -0.15,\n                'Pay_Manual': 0.48,\n                'Skip_High (>50%)': 0.03\n            }\n    joblib.dump(MockModel(), 'kkbox_cox_model.pkl')\n    print(\"-> Đã tạo file model giả lập để chạy test.\")\n\n\n# CLASS SIMULATION ENGINE \nclass ChurnSimulationEngine:\n    def __init__(self, data_path='streamlit_cohort_simulation.csv', model_path='kkbox_cox_model.pkl'):\n        self.df = pd.read_csv(data_path)\n        try:\n            cph_model = joblib.load(model_path)\n            self.coefs = cph_model.params_\n        except:\n            print(\"Cảnh báo: Không tìm thấy model Cox. Đang dùng hệ số mặc định.\")\n            self.coefs = {'Pay_Manual': 0.48, 'Price_Deal (<4.5d)': -0.15, 'Skip_High (>50%)': 0.03}\n            \n    # --- MODULE 1: WHAT-IF SIMULATION ---\n    def run_what_if_simulation(self, filters=None, shift_auto=0.0, shift_upsell=0.0, shift_skip=0.0):\n        df_sim = self.df.copy()\n        if filters:\n            for col, values in filters.items():\n                if values:\n                    df_sim = df_sim[df_sim[col].isin(values)]\n                    \n        if len(df_sim) == 0:\n            return None, \"Không có dữ liệu cho bộ lọc này.\"\n\n        # FIX WARNING: Ép kiểu rõ ràng thành float ngay từ đầu\n        df_sim['sim_revenue'] = df_sim['total_revenue'].astype(float)\n        df_sim['sim_survival_days'] = df_sim['avg_survival_days'].astype(float)\n        \n        # Chiến dịch 1: Auto-Renew\n        mask_manual = df_sim['payment_type'] == 'Pay_Manual'\n        if shift_auto > 0 and mask_manual.any():\n            shift_rate = shift_auto / 100.0\n            impacted_users = df_sim.loc[mask_manual, 'user_count'] * shift_rate\n            coef_manual = self.coefs.get('Pay_Manual', 0)\n            new_hr = df_sim.loc[mask_manual, 'baseline_hr'] / np.exp(coef_manual)\n            days_improvement_ratio = df_sim.loc[mask_manual, 'baseline_hr'] / new_hr\n            new_days = df_sim.loc[mask_manual, 'avg_survival_days'] * days_improvement_ratio\n            days_gained = (new_days - df_sim.loc[mask_manual, 'avg_survival_days'])\n            revenue_gained = impacted_users * days_gained * df_sim.loc[mask_manual, 'avg_paid_per_day']\n            df_sim.loc[mask_manual, 'sim_revenue'] += revenue_gained\n\n        # Chiến dịch 2: Upsell\n        mask_deal = df_sim['price_segment'] == 'Price_Deal (<4.5d)'\n        if shift_upsell > 0 and mask_deal.any():\n            shift_rate = shift_upsell / 100.0\n            impacted_users = df_sim.loc[mask_deal, 'user_count'] * shift_rate\n            coef_deal = self.coefs.get('Price_Deal (<4.5d)', 0)\n            new_hr = df_sim.loc[mask_deal, 'baseline_hr'] / np.exp(coef_deal)\n            days_improvement_ratio = df_sim.loc[mask_deal, 'baseline_hr'] / new_hr\n            new_days = df_sim.loc[mask_deal, 'avg_survival_days'] * days_improvement_ratio\n            rev_change = impacted_users * ( (new_days * 5.0) - (df_sim.loc[mask_deal, 'avg_survival_days'] * df_sim.loc[mask_deal, 'avg_paid_per_day']) )\n            df_sim.loc[mask_deal, 'sim_revenue'] += rev_change\n\n        # Chiến dịch 3: Tối ưu Suggestion\n        mask_skip = df_sim['skip_segment'] == 'Skip_High (>50%)'\n        if shift_skip > 0 and mask_skip.any():\n            shift_rate = shift_skip / 100.0\n            impacted_users = df_sim.loc[mask_skip, 'user_count'] * shift_rate\n            coef_skip = self.coefs.get('Skip_High (>50%)', 0)\n            new_hr = df_sim.loc[mask_skip, 'baseline_hr'] / np.exp(coef_skip)\n            days_improvement_ratio = df_sim.loc[mask_skip, 'baseline_hr'] / new_hr\n            new_days = df_sim.loc[mask_skip, 'avg_survival_days'] * days_improvement_ratio\n            days_gained = (new_days - df_sim.loc[mask_skip, 'avg_survival_days'])\n            revenue_gained = impacted_users * days_gained * df_sim.loc[mask_skip, 'avg_paid_per_day']\n            df_sim.loc[mask_skip, 'sim_revenue'] += revenue_gained\n\n        baseline_revenue = df_sim['total_revenue'].sum()\n        total_new_revenue = df_sim['sim_revenue'].sum()\n        revenue_delta = total_new_revenue - baseline_revenue\n        \n        kpis = {\n            \"Trạng thái gốc (Baseline)\": baseline_revenue,\n            \"Tiền chênh lệch (Delta)\": revenue_delta,\n            \"Kỳ vọng mới (Simulated)\": total_new_revenue\n        }\n        \n        fig = go.Figure(go.Waterfall(\n            name=\"Revenue\", orientation=\"v\",\n            measure=[\"absolute\", \"relative\", \"total\"],\n            x=[\"Doanh thu hiện tại\", \"Tác động giả lập\", \"Kỳ vọng tương lai\"],\n            y=[baseline_revenue, revenue_delta, total_new_revenue],\n            connector={\"line\":{\"color\":\"#BFC9CA\"}},\n            decreasing={\"marker\":{\"color\":\"#E74C3C\"}},\n            increasing={\"marker\":{\"color\":\"#2ECC71\"}},\n            totals={\"marker\":{\"color\":\"#2980B9\"}},\n            text=[f\"{v:,.0f} NTD\" for v in[baseline_revenue, revenue_delta, total_new_revenue]],\n            textposition=\"outside\"\n        ))\n        fig.update_layout(title=\"Mô phỏng Tác động Chiến dịch (What-If Analysis)\", plot_bgcolor='rgba(0,0,0,0)')\n\n        return kpis, fig\n\n    # --- MODULE 2: MONTE CARLO SIMULATION ---\n    def run_monte_carlo(self, expected_revenue: float, volatility: float = 8.0, iterations: int = 10000):\n        \"\"\"\n        Input: Doanh thu kỳ vọng từ What-If và độ biến động thị trường (%).\n        \"\"\"\n        std_dev = expected_revenue * (volatility / 100)\n        mc_results = np.random.normal(loc=expected_revenue, scale=std_dev, size=iterations)\n\n        p10 = np.percentile(mc_results, 10)\n        p50 = np.percentile(mc_results, 50)\n        p90 = np.percentile(mc_results, 90)\n\n        kpis = {\"P10_worst\": p10, \"P50_expected\": p50, \"P90_best\": p90}\n\n        # Vẽ Histogram phân phối\n        fig = px.histogram(\n            mc_results, nbins=100, \n            title=f\"Phân phối Doanh thu (Monte Carlo - Volatility: {volatility}%)\",\n            labels={'value': 'Doanh thu (NTD)'}, \n            color_discrete_sequence=['#143F6B']\n        )\n        # Thêm đường gióng P10, P90\n        fig.add_vline(x=p10, line_dash=\"dash\", line_color=\"red\", annotation_text=f\"Worst-case: P10\")\n        fig.add_vline(x=p90, line_dash=\"dash\", line_color=\"green\", annotation_text=f\"Best-case: P90\")\n\n        return kpis, fig\n\n\n# DEMO\nprint(\"\\n=== TỔNG QUAN CHẠY MÔ PHỎNG DÀNH CHO C-LEVEL ===\")\nengine = ChurnSimulationEngine()\n\n# 1. Chạy What-If\nprint(\"\\n[1] ĐANG CHẠY WHAT-IF ANALYSIS...\")\nkpis_whatif, fig_whatif = engine.run_what_if_simulation(\n    filters={'gender': ['Female']}, # Chỉ xem khách hàng Nữ\n    shift_auto=30.0, \n    shift_upsell=0.0, \n    shift_skip=50.0\n)\nfor k, v in kpis_whatif.items():\n    print(f\" - {k}: {v:,.0f} NTD\")\nfig_whatif.show()\n\n# 2. Chạy Monte Carlo dựa trên kết quả What-If\nprint(\"\\n[2] ĐANG CHẠY MONTE CARLO SIMULATION (Thị trường biến động 8%)...\")\nkpis_mc, fig_mc = engine.run_monte_carlo(\n    expected_revenue=kpis_whatif[\"Kỳ vọng mới (Simulated)\"], \n    volatility=8.0\n)\nfor k, v in kpis_mc.items():\n    print(f\" - {k}: {v:,.0f} NTD\")\nfig_mc.show()'''","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-04-06T03:33:49.113961Z","iopub.execute_input":"2026-04-06T03:33:49.114925Z","iopub.status.idle":"2026-04-06T03:33:49.132835Z","shell.execute_reply.started":"2026-04-06T03:33:49.114865Z","shell.execute_reply":"2026-04-06T03:33:49.131835Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport joblib\nimport os\nimport plotly.graph_objects as go\nimport plotly.express as px\nimport subprocess\n\n# ==============================================================================\n# 0. SETUP DỮ LIỆU & MODEL GIẢ LẬP\n# ==============================================================================\npath = '/kaggle/input/competitions/kkbox-churn-prediction-challenge/'\n\ndef extract_all_7z_to_csv(file_list):\n    for file_7z in file_list:\n        file_csv = file_7z.replace('.7z', '')\n        if not os.path.exists(f'/kaggle/working/{file_csv}'):\n            print(f\"Đang bắt đầu giải nén: {file_7z} ...\")\n            try:\n                subprocess.run(['7z', 'e', path + file_7z, '-o/kaggle/working/'], check=True)\n                print(f\"THÀNH CÔNG: Đã tạo file {file_csv}\")\n            except subprocess.CalledProcessError as e:\n                print(f\"LỖI khi xử lý {file_7z}: {e}\")\n        else:\n            print(f\"File {file_csv} đã tồn tại, bỏ qua giải nén.\")\n\nfiles_to_extract =[\n    'train_v2.csv.7z',\n    'members_v3.csv.7z',\n    'transactions_v2.csv.7z',\n    'user_logs_v2.csv.7z'\n]\n\nextract_all_7z_to_csv(files_to_extract)\n\nINPUT_PATH = '/kaggle/working/'\n\nprint(\"\\n--- ĐANG NẠP DỮ LIỆU THÔ ---\")\ntrain = pd.read_csv(f'{INPUT_PATH}train_v2.csv')\nmembers = pd.read_csv(f'{INPUT_PATH}members_v3.csv')\ntransactions = pd.read_csv(f'{INPUT_PATH}transactions_v2.csv')\n\nunique_users = train['msno'].unique()\nlogs = pd.read_csv(f'{INPUT_PATH}user_logs_v2.csv', nrows=5000000) \nlogs = logs[logs['msno'].isin(unique_users)]\n\n# FIX LỖI PATH: Chỉ kiểm tra file ở ngay thư mục hiện hành\nmodel_name = 'kkbox_cox_model.pkl'\nif not os.path.exists(model_name):\n    class MockModel:\n        def __init__(self):\n            self.params_ = {\n                'Female': 0.01,\n                'Age_15-20': 0.27,\n                'Age_31-45': -0.06,\n                'Price_Deal (<4.5d)': -0.15,\n                'Pay_Manual': 0.48,\n                'Skip_High (>50%)': 0.03\n            }\n    joblib.dump(MockModel(), model_name)\n    print(\"-> Đã tự động tạo file model giả lập (Mock Model) để chạy test.\")\nelse:\n    print(f\"-> Đã tìm thấy file model {model_name} sẵn có.\")\n\n\n# ==============================================================================\n# 1. HÀM BUILD DATASET (ĐÃ FIX LỖI PANDAS DATETIME)\n# ==============================================================================\ndef build_cohort_simulation_dataset(transactions, logs, members, model_path='kkbox_cox_model.pkl'):\n    print(\"\\n--- BẮT ĐẦU BUILD COHORT DATASET ---\")\n    cph_model = joblib.load(model_path)\n    coefs = cph_model.params_\n    CUT_OFF_DATE = pd.to_datetime('2017-03-31')\n    \n    trans_valid = transactions.copy()\n    trans_valid['transaction_date'] = pd.to_datetime(trans_valid['transaction_date'], format='%Y%m%d')\n    trans_valid['membership_expire_date'] = pd.to_datetime(trans_valid['membership_expire_date'], format='%Y%m%d')\n    \n    trans_agg = trans_valid.groupby('msno').agg(\n        total_revenue=('actual_amount_paid', 'sum'),\n        total_plan_days=('payment_plan_days', 'sum'),\n        auto_renew_ratio=('is_auto_renew', 'mean'),\n        first_date=('transaction_date', 'min'),\n        last_date=('membership_expire_date', 'max') \n    ).reset_index()\n\n    trans_agg['is_censored'] = trans_agg['last_date'] > CUT_OFF_DATE\n    \n    # FIX LỖI DATETIME Ở ĐÂY: Bọc kết quả np.where bằng pd.to_datetime\n    trans_agg['end_calc_date'] = pd.to_datetime(\n        np.where(trans_agg['is_censored'], CUT_OFF_DATE, trans_agg['last_date'])\n    )\n    \n    trans_agg['survival_days'] = (trans_agg['end_calc_date'] - trans_agg['first_date']).dt.days.clip(lower=1)\n    \n    trans_agg['paid_per_day'] = np.where(trans_agg['total_plan_days'] > 0, \n                                         trans_agg['total_revenue'] / trans_agg['total_plan_days'], 0)\n    trans_agg['payment_type'] = np.where(trans_agg['auto_renew_ratio'] > 0.5, 'Pay_Auto-Renew', 'Pay_Manual')\n    trans_agg['price_segment'] = pd.cut(trans_agg['paid_per_day'], bins=[-0.1, 0.1, 4.5, float('inf')],\n                                        labels=['Price_0d (Trial)', 'Price_Deal (<4.5d)', 'Price_Chuan (>=4.5d)'])\n\n    logs['total_songs'] = logs['num_25'] + logs['num_50'] + logs['num_75'] + logs['num_985'] + logs['num_100']\n    logs_agg = logs.groupby('msno').agg(\n        total_skip=('num_25', 'sum'),\n        total_songs=('total_songs', 'sum')\n    ).reset_index()\n    logs_agg['skip_ratio'] = np.where(logs_agg['total_songs'] > 0, logs_agg['total_skip'] / logs_agg['total_songs'], 0)\n    logs_agg['skip_segment'] = pd.cut(logs_agg['skip_ratio'], bins=[-0.1, 0.2, 0.5, float('inf')],\n                                      labels=['Skip_Low (<20%)', 'Skip_Med (20-50%)', 'Skip_High (>50%)'])\n\n    members_lean = members[['msno', 'city', 'gender', 'bd']].copy()\n    members_lean['gender'] = members_lean['gender'].fillna('Gender_Unknown')\n    members_lean['gender'] = members_lean['gender'].replace({'male':'Male', 'female': 'Female'})\n    members_lean['age_segment'] = pd.cut(members_lean['bd'], bins=[14, 20, 30, 45, 65], \n                                         labels=['Age_15-20', 'Age_21-30', 'Age_31-45', 'Age_46-65'])\n    members_lean['age_segment'] = members_lean['age_segment'].astype(str).replace('nan', 'Age_Unknown')\n\n    master = trans_agg.merge(logs_agg[['msno', 'skip_segment']], on='msno', how='inner')\n    master = master.merge(members_lean, on='msno', how='inner')\n\n    dimensions =['city', 'gender', 'age_segment', 'payment_type', 'price_segment', 'skip_segment']\n    cohort_df = master.groupby(dimensions, observed=True).agg(\n        user_count=('msno', 'count'),\n        total_revenue=('total_revenue', 'sum'),\n        avg_survival_days=('survival_days', 'mean'),\n        avg_paid_per_day=('paid_per_day', 'mean')\n    ).reset_index()\n    \n    def calc_cohort_hr(row):\n        log_hr = 0.0\n        if row['gender'] in coefs: log_hr += coefs[row['gender']]\n        if row['age_segment'] in coefs: log_hr += coefs[row['age_segment']]\n        if row['payment_type'] in coefs: log_hr += coefs[row['payment_type']]\n        if row['price_segment'] in coefs: log_hr += coefs[row['price_segment']]\n        if row['skip_segment'] in coefs: log_hr += coefs[row['skip_segment']]\n        return np.exp(log_hr)\n\n    cohort_df['baseline_hr'] = cohort_df.apply(calc_cohort_hr, axis=1)\n    cohort_df.to_csv('streamlit_cohort_simulation.csv', index=False)\n    print(f\"Hoàn tất Build! File tạo ra có: {len(cohort_df):,} cohorts.\")\n    return cohort_df\n\ncohort_data = build_cohort_simulation_dataset(transactions, logs, members)\n\n\n# ==============================================================================\n# 2. CLASS SIMULATION ENGINE (FULL PRD + MONTE CARLO)\n# ==============================================================================\nclass ChurnSimulationEngine:\n    def __init__(self, data_path='streamlit_cohort_simulation.csv', model_path='kkbox_cox_model.pkl'):\n        self.df = pd.read_csv(data_path)\n        try:\n            cph_model = joblib.load(model_path)\n            self.coefs = cph_model.params_\n        except:\n            self.coefs = {'Pay_Manual': 0.48, 'Price_Deal (<4.5d)': -0.15, 'Skip_High (>50%)': 0.03}\n            \n        self.colors = {\n            'baseline': '#95A5A6',   \n            'simulated': '#1ABC9C',  \n            'primary': '#2C3E50',    \n            'upsell': '#3498DB',     \n            'negative': '#E74C3C'    \n        }\n\n    def _calculate_scenario(self, df_input, shift_auto=0.0, shift_upsell=0.0, shift_skip=0.0):\n        df_sim = df_input.copy()\n        \n        retention_revenue = 0.0\n        upsell_revenue = 0.0\n        hr_distribution =[]\n        \n        for idx, row in df_sim.iterrows():\n            users = row['user_count']\n            base_hr = row['baseline_hr']\n            base_days = row['avg_survival_days']\n            base_price = row['avg_paid_per_day']\n            \n            unchanged_users = users\n            \n            if shift_auto > 0 and row['payment_type'] == 'Pay_Manual':\n                rate = shift_auto / 100.0\n                shifted_users = users * rate\n                unchanged_users -= shifted_users\n                \n                coef = self.coefs.get('Pay_Manual', 0)\n                new_hr = base_hr / np.exp(coef) \n                new_days = base_days * (base_hr / new_hr)\n                \n                retention_revenue += shifted_users * (new_days - base_days) * base_price\n                hr_distribution.append({'type': 'Simulated (Tương lai)', 'hr': new_hr, 'users': shifted_users})\n\n            elif shift_upsell > 0 and row['price_segment'] == 'Price_Deal (<4.5d)':\n                rate = shift_upsell / 100.0\n                shifted_users = users * rate\n                unchanged_users -= shifted_users\n                \n                coef = self.coefs.get('Price_Deal (<4.5d)', 0)\n                new_hr = base_hr / np.exp(coef) \n                new_days = base_days * (base_hr / new_hr)\n                \n                retention_revenue += shifted_users * (new_days - base_days) * base_price\n                target_price = 5.0 \n                upsell_revenue += shifted_users * new_days * (target_price - base_price)\n                \n                hr_distribution.append({'type': 'Simulated (Tương lai)', 'hr': new_hr, 'users': shifted_users})\n\n            elif shift_skip > 0 and row['skip_segment'] == 'Skip_High (>50%)':\n                rate = shift_skip / 100.0\n                shifted_users = users * rate\n                unchanged_users -= shifted_users\n                \n                coef = self.coefs.get('Skip_High (>50%)', 0)\n                new_hr = base_hr / np.exp(coef)\n                new_days = base_days * (base_hr / new_hr)\n                \n                retention_revenue += shifted_users * (new_days - base_days) * base_price\n                hr_distribution.append({'type': 'Simulated (Tương lai)', 'hr': new_hr, 'users': shifted_users})\n            \n            hr_distribution.append({'type': 'Baseline (Hiện tại)', 'hr': base_hr, 'users': users})\n            if unchanged_users > 0:\n                hr_distribution.append({'type': 'Simulated (Tương lai)', 'hr': base_hr, 'users': unchanged_users})\n\n        baseline_revenue = df_input['total_revenue'].sum()\n        total_projected = baseline_revenue + retention_revenue + upsell_revenue\n        \n        return {\n            'baseline_rev': baseline_revenue,\n            'retention_rev': retention_revenue,\n            'upsell_rev': upsell_revenue,\n            'projected_rev': total_projected,\n            'hr_dist_df': pd.DataFrame(hr_distribution)\n        }\n\n    def plot_hazard_shift(self, hr_dist_df):\n        hr_dist_df['hr_bin'] = hr_dist_df['hr'].round(2)\n        agg_dist = hr_dist_df.groupby(['type', 'hr_bin'])['users'].sum().reset_index()\n        \n        total_users = agg_dist[agg_dist['type'] == 'Baseline (Hiện tại)']['users'].sum()\n        agg_dist['user_pct'] = (agg_dist['users'] / total_users) * 100\n\n        fig = px.area(\n            agg_dist, x='hr_bin', y='user_pct', color='type',\n            color_discrete_map={'Baseline (Hiện tại)': self.colors['baseline'], 'Simulated (Tương lai)': self.colors['simulated']},\n            labels={'hr_bin': 'Hệ số Rủi ro rời bỏ (Hazard Ratio)', 'user_pct': 'Tỷ trọng Khách hàng (%)'},\n            title='Biểu đồ 1: Dịch chuyển Cấu trúc Rủi ro Hệ thống (Population Hazard Shift)'\n        )\n        fig.update_traces(opacity=0.6, line=dict(width=2))\n        fig.update_layout(\n            plot_bgcolor='white', hovermode='x unified',\n            legend=dict(orientation=\"h\", yanchor=\"bottom\", y=1.02, xanchor=\"right\", x=1)\n        )\n        fig.update_xaxes(showgrid=True, gridwidth=1, gridcolor='#F2F3F4')\n        fig.update_yaxes(showgrid=True, gridwidth=1, gridcolor='#F2F3F4')\n        return fig\n\n    def plot_waterfall(self, res):\n        fig = go.Figure(go.Waterfall(\n            name=\"Revenue\", orientation=\"v\",\n            measure=[\"absolute\", \"relative\", \"relative\", \"total\"],\n            x=[\"Doanh thu Hiện tại<br>(Baseline)\", \"Cứu vãn nhờ Giữ chân<br>(Retention)\", \n               \"Tăng thêm nhờ Ép giá<br>(Upsell)\", \"Kỳ vọng Tương lai<br>(Projected)\"],\n            y=[res['baseline_rev'], res['retention_rev'], res['upsell_rev'], res['projected_rev']],\n            connector={\"line\":{\"color\":\"#BDC3C7\", \"width\": 2, \"dash\": \"dot\"}},\n            decreasing={\"marker\":{\"color\": self.colors['negative']}},\n            increasing={\"marker\":{\"color\": self.colors['simulated']}},\n            totals={\"marker\":{\"color\": self.colors['primary']}},\n            text=[f\"{v/1e6:,.1f}M\" for v in [res['baseline_rev'], res['retention_rev'], res['upsell_rev'], res['projected_rev']]],\n            textposition=\"outside\",\n            textfont=dict(size=13, family=\"Arial\", color=\"black\")\n        ))\n        \n        fig.update_layout(\n            title='Biểu đồ 2: Phân rã Tác động Tài chính (Financial Impact Analysis)', \n            plot_bgcolor='white',\n            yaxis=dict(title=\"Doanh thu (NTD)\"),\n            margin=dict(t=70, l=50, r=50, b=50)\n        )\n        return fig\n\n    def plot_sensitivity(self, df_input):\n        res_auto = self._calculate_scenario(df_input, shift_auto=1.0)\n        res_upsell = self._calculate_scenario(df_input, shift_upsell=1.0)\n        res_skip = self._calculate_scenario(df_input, shift_skip=1.0)\n        \n        roi_auto = res_auto['projected_rev'] - res_auto['baseline_rev']\n        roi_upsell = res_upsell['projected_rev'] - res_upsell['baseline_rev']\n        roi_skip = res_skip['projected_rev'] - res_skip['baseline_rev']\n        \n        data = pd.DataFrame({\n            'Chiến lược':['Thuyết phục Auto-Renew', 'Giảm lượng Skip (Sản phẩm)', 'Ép gói Giá chuẩn (Upsell)'],\n            'ROI_per_1_percent':[roi_auto, roi_skip, roi_upsell]\n        }).sort_values('ROI_per_1_percent', ascending=True) \n\n        fig = px.bar(\n            data, x='ROI_per_1_percent', y='Chiến lược', orientation='h',\n            text_auto='.0f', color='ROI_per_1_percent', color_continuous_scale='Teal',\n            title='Biểu đồ 3: Phân tích Độ nhạy (Giá trị Doanh thu mang lại từ mỗi 1% Nỗ lực)'\n        )\n        fig.update_layout(\n            plot_bgcolor='white', coloraxis_showscale=False,\n            xaxis=dict(title=\"Lợi nhuận tăng thêm trên mỗi 1% Chuyển đổi (NTD)\"),\n            yaxis=dict(title=\"\"),\n            font=dict(size=14)\n        )\n        fig.update_traces(texttemplate='%{x:,.0f} NTD', textposition='outside')\n        return fig\n\n    def run_monte_carlo(self, expected_revenue: float, volatility: float = 8.0, iterations: int = 10000):\n        std_dev = expected_revenue * (volatility / 100)\n        mc_results = np.random.normal(loc=expected_revenue, scale=std_dev, size=iterations)\n\n        p10 = np.percentile(mc_results, 10)\n        p50 = np.percentile(mc_results, 50)\n        p90 = np.percentile(mc_results, 90)\n\n        df_mc = pd.DataFrame({'Doanh thu': mc_results})\n\n        fig = px.histogram(\n            df_mc, x='Doanh thu', nbins=100, \n            title=f'Biểu đồ 4: Phân phối Rủi ro Doanh thu - Monte Carlo (Độ biến động: {volatility}%)',\n            labels={'Doanh thu': 'Doanh thu Kỳ vọng (NTD)'}, \n            color_discrete_sequence=[self.colors['primary']]\n        )\n        \n        fig.update_layout(\n            showlegend=False, \n            plot_bgcolor='white', \n            yaxis=dict(title=\"Tần suất (Số Kịch bản)\")\n        )\n\n        fig.add_vline(x=p10, line_dash=\"dash\", line_color=self.colors['negative'], \n                      annotation_text=f\"Trường hợp Xấu (P10): {p10/1e6:.1f}M\")\n        fig.add_vline(x=p90, line_dash=\"dash\", line_color=self.colors['simulated'], \n                      annotation_text=f\"Trường hợp Tốt (P90): {p90/1e6:.1f}M\")\n        \n        return {\"P10_worst\": p10, \"P50_expected\": p50, \"P90_best\": p90}, fig\n\n    def run_dashboard(self, filters=None, shift_auto=0.0, shift_upsell=0.0, shift_skip=0.0):\n        df_sim = self.df.copy()\n        if filters:\n            for col, values in filters.items():\n                if values:\n                    df_sim = df_sim[df_sim[col].isin(values)]\n                    \n        if len(df_sim) == 0:\n            print(\"Không có dữ liệu cho bộ lọc này.\")\n            return None, None, None, None\n\n        res = self._calculate_scenario(df_sim, shift_auto, shift_upsell, shift_skip)\n        \n        fig_hazard = self.plot_hazard_shift(res['hr_dist_df'])\n        fig_waterfall = self.plot_waterfall(res)\n        fig_sensitivity = self.plot_sensitivity(df_sim)\n        \n        return res, fig_hazard, fig_waterfall, fig_sensitivity\n\n\n# ==============================================================================\n# 3. THỰC THI TRÊN KAGGLE VÀ HIỂN THỊ KẾT QUẢ\n# ==============================================================================\nprint(\"\\n\" + \"=\"*60)\nprint(\" KHỞI ĐỘNG HỆ THỐNG MÔ PHỎNG CHIẾN LƯỢC K-CID (TAB 3)\")\nprint(\"=\"*60)\n\nengine = ChurnSimulationEngine()\n\n# CHỈ ĐỊNH THAM SỐ TỪ C-LEVEL\nFILTERS = {'gender': ['Female']}\nSHIFT_AUTO = 40.0   \nSHIFT_UPSELL = 30.0 \nSHIFT_SKIP = 15.0   \n\nprint(\"\\n[1] ĐANG TÍNH TOÁN CÁC BIỂU ĐỒ PRD CHÍNH...\")\nres_dict, fig_1, fig_2, fig_3 = engine.run_dashboard(\n    filters=FILTERS, \n    shift_auto=SHIFT_AUTO, \n    shift_upsell=SHIFT_UPSELL, \n    shift_skip=SHIFT_SKIP\n)\n\nprint(f\" -> Tóm tắt Nhanh:\")\nprint(f\"    - Doanh thu Gốc: {res_dict['baseline_rev']:,.0f} NTD\")\nprint(f\"    - Tiền Giữ chân (Retention): {res_dict['retention_rev']:,.0f} NTD\")\nprint(f\"    - Tiền Tăng thêm (Upsell): {res_dict['upsell_rev']:,.0f} NTD\")\nprint(f\"    - KỲ VỌNG MỚI: {res_dict['projected_rev']:,.0f} NTD\")\n\nfig_1.show()\nfig_2.show()\nfig_3.show()\n\nprint(\"\\n[2] ĐANG CHẠY 10,000 KỊCH BẢN MONTE CARLO (CFO VIEW)...\")\nmc_kpis, fig_4 = engine.run_monte_carlo(expected_revenue=res_dict['projected_rev'], volatility=8.0)\n\nprint(f\" -> P10 (Xấu nhất): {mc_kpis['P10_worst']:,.0f} NTD\")\nprint(f\" -> P50 (Kỳ vọng):  {mc_kpis['P50_expected']:,.0f} NTD\")\nprint(f\" -> P90 (Tốt nhất): {mc_kpis['P90_best']:,.0f} NTD\")\n\nfig_4.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-06T03:44:08.725186Z","iopub.execute_input":"2026-04-06T03:44:08.726127Z","iopub.status.idle":"2026-04-06T03:45:00.870837Z","shell.execute_reply.started":"2026-04-06T03:44:08.726071Z","shell.execute_reply":"2026-04-06T03:45:00.869419Z"}},"outputs":[],"execution_count":null}]}