{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":50160,"databundleVersionId":7921029,"sourceType":"competition"}],"dockerImageVersionId":31040,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# THƯ VIỆN CHO DATA SCIENCE & MODELING","metadata":{}},{"cell_type":"code","source":"# KẾT NỐI VÀ XỬ LÝ DỮ LIỆU \nimport pyodbc  # Kết nối cơ sở dữ liệu (SQL Server, v.v.)\nimport pandas as pd  # Xử lý dữ liệu dạng bảng (DataFrame)\n\n# === BIỂU ĐỒ VÀ ĐỒ HỌA ===\nimport matplotlib.pyplot as plt  # Vẽ biểu đồ (line, bar, histogram,...)\nimport matplotlib.cm as cm  # Dùng để lấy colormap (phối màu)\nimport matplotlib.colors as mcolors  # Quản lý màu sắc (RGB, HEX,...)\nimport matplotlib.patches as mpatches  # Tạo mẫu màu dùng cho chú thích (legend)\n\n# === HỆ THỐNG VÀ TOÁN HỌC ===\nimport sys  # Truy cập thông tin hệ thống (ví dụ: sys.path)\nimport numpy as np  # Thư viện toán học, xử lý mảng và thống kê\n\n# === XUẤT FILE EXCEL ===\nimport xlsxwriter  # Ghi dữ liệu và biểu đồ ra file Excel (.xlsx)\n\n# === TỔ HỢP VÀ KẾT HỢP BIẾN ===\nfrom itertools import combinations  # Sinh các tổ hợp biến để so sánh/cross feature\n\n# === LƯU ẢNH HOẶC FILE VÀO BỘ NHỚ TẠM ===\nfrom io import BytesIO  # Dùng để lưu ảnh Excel/chart vào RAM thay vì file\n\n# === MÔ HÌNH THỐNG KÊ ===\nimport statsmodels.api as sm  # Hồi quy logistic, OLS, thống kê mô hình\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# FUNCTION: BINNING BIẾN ","metadata":{}},{"cell_type":"code","source":"# FUNCTION: BINNING BIẾN\ndef binning_all_variables(df, threshold=0.05, custom_bins=None, feature=None):\n    df = df.copy()  # Sao chép DataFrame gốc để tránh thay đổi ngoài ý muốn\n\n    columns_to_process = [feature] if feature else df.columns  # Nếu có truyền feature thì chỉ xử lý biến đó, nếu không thì xử lý toàn bộ\n\n    for column in columns_to_process:\n        if column in ['case_id','date_decision', 'WEEK_NUM', 'MONTH', 'target']:\n            continue  # Bỏ qua các biến đặc biệt không cần binning\n\n        missing_mask = df[column].isna()  # Xác định vị trí các giá trị missing\n        add_missing = False  # Cờ để ghi nhớ xem có missing không (để thêm vào danh sách nhóm sau)\n\n        # --- Xử lý biến phân loại ---\n        if df[column].dtype == object or df[column].dtype.name == 'category':\n            grp_col = 'GRP_' + column  # Tên cột mới sau khi gộp nhóm\n            df[grp_col] = df[column].astype(object)  # Copy giá trị gốc sang cột nhóm\n            if missing_mask.any():\n                df.loc[missing_mask, grp_col] = 'Missing'  # Gán label 'Missing' cho các giá trị thiếu\n                add_missing = True\n\n        else:\n            # --- Xử lý biến số ---\n            if custom_bins and column in custom_bins:\n                bins = [-np.inf] + custom_bins[column] + [np.inf]  # Nếu có custom bins thì dùng\n            else:\n                value_counts = df[column].value_counts(normalize=True)  # Tính phân bố tỷ lệ giá trị\n                sorted_vals = np.sort(df[column].dropna().unique())  # Sắp xếp giá trị không missing\n                cumulative = 0  # Tích lũy để gộp nhóm nhỏ lại\n                bins = [-np.inf]  # Khởi tạo mốc dưới đầu tiên\n                for v in sorted_vals:\n                    prop = value_counts.get(v, 0)  # Lấy tỷ lệ xuất hiện của v\n                    if prop > threshold:\n                        bins.append(v)  # Nếu đủ lớn thì đưa vào nhóm mới\n                    else:\n                        cumulative += prop  # Gộp vào nhóm hiện tại nếu nhỏ\n                        if cumulative >= threshold:\n                            bins.append(v)\n                            cumulative = 0\n                bins.append(np.inf)  # Mốc trên cùng\n                bins = sorted(set(bins))  # Loại bỏ trùng và sắp xếp\n\n        \n            grp_col = 'GRP_' + column\n            df[grp_col] = pd.cut(df[column], bins=bins, include_lowest=True, duplicates='drop')  # Phân nhóm\n            if missing_mask.any():\n                df[grp_col] = df[grp_col].astype(object)  # Ép kiểu để gán 'Missing'\n                df.loc[missing_mask, grp_col] = 'Missing'\n                add_missing = True\n\n        # --- Sắp xếp lại các nhóm theo thứ tự hợp lý ---\n        def extract_lb(x): return x.left if isinstance(x, pd.Interval) else np.inf  # Trích mốc dưới nếu là khoảng\n\n        if df[column].dtype == object or df[column].dtype.name == 'category':\n            categories = [x for x in df['GRP_' + column].unique() if x != 'Missing']  # Nhóm không missing\n        else:\n            ivals = [c for c in df['GRP_' + column].unique() if isinstance(c, pd.Interval)]\n            categories = sorted(ivals, key=extract_lb)  # Sắp theo mốc dưới\n\n        if add_missing:\n            categories.append('Missing')  # Đưa 'Missing' vào cuối danh sách nhóm\n\n        df['GRP_' + column] = pd.Categorical(df['GRP_' + column], categories=categories, ordered=True)  # Gán lại với nhóm đã sắp xếp\n\n    return df  # Trả về DataFrame sau khi binning\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T16:42:14.798801Z","iopub.execute_input":"2025-11-09T16:42:14.799118Z","iopub.status.idle":"2025-11-09T16:42:14.812836Z","shell.execute_reply.started":"2025-11-09T16:42:14.799095Z","shell.execute_reply":"2025-11-09T16:42:14.811843Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# LOGIC CODE\n\nMục tiêu:\nTự động chia các biến (cột) trong bảng dữ liệu thành các nhóm (bins) để phục vụ phân tích (VD: xây scorecard). Có xử lý riêng cho biến phân loại và biến số, đồng thời xử lý giá trị thiếu (missing).  \n\n1 Bước lặp qua từng biến cần xử lý\n- Nếu feature được chỉ định → xử lý biến đó.  \nNếu không → xử lý toàn bộ biến trong DataFrame (trừ các cột:'case_id','date_decision', 'WEEK_NUM', 'MONTH', 'target').\n- Tạo mặt nạ missing:  \nDùng isna() để đánh dấu các dòng bị thiếu.  \nTạo cờ add_missing = False để ghi nhớ xem biến này có missing không.\n\n\n2 Xử lý theo loại biến   \n2.1 Biến phân loại (object / category):  \n- Tạo cột mới GRP_<biến>, copy y nguyên dữ liệu.  \n- Nếu có null:  \nGán giá trị 'Missing' cho các dòng bị thiếu.  \nĐánh dấu add_missing = True để ghi nhớ cho bước sắp xếp nhóm sau này.  \n\n2.2 Biến số (numeric):  \n- Nếu có custom_bins truyền vào:  \nDùng mốc do người dùng chỉ định, thêm -∞ và +∞.\n  \n- Nếu không có custom_bins:  \nTính phân bố xuất hiện của từng giá trị (value_counts).  \nDuyệt các giá trị tăng dần:  \nNếu tỷ lệ ≥ threshold → cho vào nhóm riêng.  \nNếu tỷ lệ < threshold → cộng dồn đến khi đủ thì mới tách nhóm.  \nĐảm bảo luôn có -∞ và +∞ để bao phủ toàn bộ giá trị.  \nDùng pd.cut() để cắt thành nhóm (intervals).  \n\n- Nếu có missing:  \nÉp cột nhóm sang object.  \nGán 'Missing' cho các dòng thiếu.  \nCập nhật add_missing = True.  \n\n3 Sắp xếp thứ tự nhóm (categories):\n- Với biến phân loại:  \nLấy tất cả nhóm khác 'Missing'.  \nNếu có add_missing == True → thêm 'Missing' vào cuối.\n- Với biến số:  \nTrích các nhóm dạng khoảng Interval(...).  \nSắp xếp theo mốc dưới (left) của mỗi khoảng.  \nNếu có add_missing == True → thêm 'Missing' vào cuối.  \n\n4 Gán lại cột nhóm:\n- Gán lại df['GRP_<biến>'] với pd.Categorical(..., ordered=True) để:\n- Nhóm có thứ tự rõ ràng, 'Missing' luôn nằm cuối (nếu có).\n","metadata":{}},{"cell_type":"code","source":"# VÍ DỤ TẠO DATAFRAME ỨNG DỤNG CÁC FUNCTION (kích thước data phân tích lớn sử dụng trình bày sẽ khó tối ưu)\n\nnp.random.seed(42)\nn = 10000\n\n# 1. case_id duy nhất\ncase_id = np.arange(n)\n\n# 2. days90_310L ngẫu nhiên từ 0–10, thêm 20% NaN\ndays_values = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, np.nan]\nprob = np.array([0.34, 0.18, 0.12, 0.06, 0.03, 0.03, 0.06, 0.05, 0.04, 0.04, 0.05, 0.20])\n\n# Chuẩn hóa xác suất để tổng đúng 1.0\nprob = prob / prob.sum()\n\ndays90_310L = np.random.choice(days_values, size=n, p=prob)\n\n# 3. sex_738L: F 61%, M 39%\nsex_738L = np.random.choice(['F', 'M'], size=n, p=[0.61, 0.39])\n\n# 4. target: 0 chiếm 97%, 1 chiếm 3%\ntarget = np.random.choice([0, 1], size=n, p=[0.97, 0.03])\n\n# 5. Gộp lại\ndf = pd.DataFrame({\n    'case_id': case_id,\n    'days90_310L': days90_310L,\n    'sex_738L': sex_738L,\n    'target': target\n})\n\ndf.head()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T16:37:42.912611Z","iopub.execute_input":"2025-11-09T16:37:42.912956Z","iopub.status.idle":"2025-11-09T16:37:42.933164Z","shell.execute_reply.started":"2025-11-09T16:37:42.912933Z","shell.execute_reply":"2025-11-09T16:37:42.932004Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# SỬ DỤNG FUNCTION BINNING VỚI BIẾN days90_310L\ndf = binning_all_variables(df, threshold=0.05, custom_bins=None, feature='days90_310L')\ndf","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T16:42:57.339341Z","iopub.execute_input":"2025-11-09T16:42:57.340372Z","iopub.status.idle":"2025-11-09T16:42:57.397306Z","shell.execute_reply.started":"2025-11-09T16:42:57.340332Z","shell.execute_reply":"2025-11-09T16:42:57.396378Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# FUNCTION TÍNH WOE VÀ IV\n","metadata":{}},{"cell_type":"code","source":"# FUNCTION TÍNH WOE VÀ IV\ndef caculate_WOE_IV(df, feature, target):\n    # Nhóm theo giá trị của biến 'feature' và tính tổng số quan sát và tổng số event (target = 1)\n    df = df.groupby(feature, observed=False)[target].agg(['count', 'sum']).reset_index()\n    \n    # Đặt lại tên cột: bin (giá trị của feature, binning), num_of_obs (số quan sát của bin), num_of_event (số event = 1, tính tổng)\n    df.columns = ['bin', 'num_of_obs', 'num_of_event']\n    \n    # Tính số non-event (target = 0)\n    df['num_of_non_event'] = df['num_of_obs'] - df['num_of_event']\n    \n    # Ép kiểu về float để tránh lỗi khi chia\n    df['num_of_event'] = df['num_of_event'].astype(float)\n    df['num_of_non_event'] = df['num_of_non_event'].astype(float)\n\n    # Tạo 2 cột sao lưu dùng để điều chỉnh nếu có event hoặc non-event bằng 0\n    df['num_of_non_event_c'] = df['num_of_non_event'].copy()\n    df['num_of_event_c'] = df['num_of_event'].copy()\n\n    # Tổng số event và non-event toàn bộ tập\n    total_non_event = df['num_of_non_event'].sum()\n    total_event = df['num_of_event'].sum()\n\n    # Nếu bất kỳ bin nào có số event hoặc non-event = 0 → cộng 0.5 để tránh chia cho 0 hoặc log(0)\n    mask = (df['num_of_event'] == 0) | (df['num_of_non_event'] == 0)\n    df.loc[mask, 'num_of_event_c'] += 0.5\n    df.loc[mask, 'num_of_non_event_c'] += 0.5\n\n    # Tính tỉ lệ non-event và event trong mỗi bin\n    df['prct_non_event'] = df['num_of_non_event_c'] / total_non_event\n    df['prct_event'] = df['num_of_event_c'] / total_event\n\n    # Tính WOE: log(tỉ lệ non-event / tỉ lệ event)\n    df['WOE'] = np.log(df['prct_non_event'] / df['prct_event'])\n\n    # Tính IV: (tỉ lệ non-event - tỉ lệ event) * WOE\n    df['IV'] = (df['prct_non_event'] - df['prct_event']) * df['WOE']\n    \n    # Tổng IV của biến → dùng để đo mức độ phân biệt của biến\n    IV = df['IV'].sum()\n\n    # Đổi bin về dạng chuỗi để dễ biểu diễn hoặc vẽ biểu đồ\n    df['bin'] = df['bin'].astype(str)\n\n    # Xoá các cột trung gian đã dùng để tính toán\n    df = df.drop(columns=['num_of_non_event_c', 'num_of_event_c'])\n\n    # Tính tỷ lệ event trong mỗi bin\n    df['event_rate'] = df['num_of_event'] / df['num_of_obs']\n\n    # Tính tỷ lệ quan sát trong mỗi bin (dạng phần trăm)\n    df['prct_obs'] = 100 * df['num_of_obs'] / df['num_of_obs'].sum()\n\n    # Trả về dataframe kết quả và giá trị IV\n    return df, IV\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T16:43:13.458725Z","iopub.execute_input":"2025-11-09T16:43:13.459096Z","iopub.status.idle":"2025-11-09T16:43:13.469792Z","shell.execute_reply.started":"2025-11-09T16:43:13.459069Z","shell.execute_reply":"2025-11-09T16:43:13.468538Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# LOGIC CODE\n\nMục tiêu: Tính các chỉ số WOE (Weight of Evidence), IV (Information Value) trong bảng phân nhóm với 1 biến  \n\n1. Nhóm dữ liệu theo feature  \nGom nhóm dữ liệu theo từng giá trị (bin) của biến feature.  \nTính:  \nSố lượng quan sát (num_of_obs)  \nSố lượng event (num_of_event, tức là target = 1)  \nTừ đó suy ra số lượng non-event (num_of_non_event = num_of_obs - num_of_event)  \n\n2. Xử lý để tránh chia cho 0  \nĐể tránh lỗi log(0) khi một bin có event = 0 hoặc non-event = 0, cộng thêm 0.5 cho cả hai trong trường hợp đó.  \n\n3. Tính tỉ lệ phần trăm  \nVới mỗi bin, tính:  \nTỉ lệ event: prct_event = num_of_event / tổng số event  \nTỉ lệ non-event: prct_non_event = num_of_non_event / tổng số non-event  \n\n4. Tính WOE  \nVới mỗi bin, tính Weight of Evidence:  \nWOE = ln(prct_non_event/prct_event)  \n\n5. Tính IV  \nVới mỗi bin, tính đóng góp vào Information Value:  \nIV = (prct_non_event − prct_event) × WOE  \nSau đó cộng tất cả các IV lại để ra IV tổng của biến.\n​\n6. Tính thêm một số chỉ số mô tả  \nevent_rate: tỷ lệ event trong từng bin.  \nprct_obs: phần trăm quan sát trong từng bin so với toàn bộ.  \n\n7. Trả về kết quả  \nHàm trả về:  \nBảng chi tiết theo từng bin (có WOE, IV, event rate, …)  \nGiá trị IV tổng để đánh giá mức độ phân biệt của biến.","metadata":{}},{"cell_type":"code","source":"# SỬ DỤNG FUNCTION TÍNH WOE VÀ IV VỚI days90_310L\ndf_woe = caculate_WOE_IV(df, 'GRP_days90_310L', 'target')\ndf_woe","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T16:45:17.842022Z","iopub.execute_input":"2025-11-09T16:45:17.842437Z","iopub.status.idle":"2025-11-09T16:45:17.871875Z","shell.execute_reply.started":"2025-11-09T16:45:17.842415Z","shell.execute_reply":"2025-11-09T16:45:17.870942Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# FUNCTION TẠO BẢNG WOE (kết hợp binning và tạo bảng trình bày) \n","metadata":{}},{"cell_type":"code","source":"#FUNCTION TẠO BẢNG WOE \ndef woe_feature_table(data, custom_bins, feature):\n    # Binning dữ liệu theo biến 'feature' với ngưỡng tối thiểu cho mỗi bin là 5% và sử dụng custom_bins\n    df_binned = binning_all_variables(data, threshold=0.05, custom_bins=custom_bins, feature=feature)\n\n    # Tính bảng WOE và giá trị IV với biến đã bin là 'GRP_<feature>' và biến mục tiêu là 'target'\n    WOE_TABLE, IV = caculate_WOE_IV(df_binned, f'GRP_{feature}', 'target')\n\n    # Chọn các cột cần thiết để trình bày bảng WOE\n    df = WOE_TABLE[['bin', 'num_of_obs', 'prct_obs', 'num_of_non_event', 'num_of_event', 'event_rate', 'WOE', 'IV']]\n\n    # Đặt lại tên cột cho dễ hiểu và phù hợp với báo cáo\n    df.columns = ['bin', 'count', 'count(%)', 'non_event', 'event', 'event_rate', 'WOE', 'IV']\n\n    # Tạo dòng tổng cộng để tổng hợp tất cả các bin\n    total_row = pd.DataFrame({\n        'bin': [''],\n        'count': [df['count'].sum()],\n        'count(%)': [100],\n        'non_event': [df['non_event'].sum()],\n        'event': [df['event'].sum()],\n        'event_rate': [df['event'].sum() / df['count'].sum()],\n        'WOE': [''],  # Không tính WOE tổng\n        'IV': [df['IV'].sum()]  # Tổng IV\n    })\n\n    # Gộp bảng chính và dòng tổng lại\n    df = pd.concat([df, total_row], ignore_index=True)\n\n    # Đổi index thành chuỗi để dễ hiển thị, đặt tên dòng cuối là 'TOTAL'\n    df.index = df.index.astype(str)\n    df.index.values[-1] = \"TOTAL\"\n\n    return df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T16:47:44.314111Z","iopub.execute_input":"2025-11-09T16:47:44.314449Z","iopub.status.idle":"2025-11-09T16:47:44.323062Z","shell.execute_reply.started":"2025-11-09T16:47:44.314415Z","shell.execute_reply":"2025-11-09T16:47:44.322038Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# LOGIC CODE\n\nMục tiêu: Tạo bảng hiển thị chi tiết thông tin WOE, IV và thống kê theo từng bin của một biến đầu vào.  \n\n1. Binning dữ liệu  \nGọi binning_all_variables() để chia các giá trị của feature thành các nhóm (bin) theo ngưỡng tối thiểu và quy tắc custom.  \n\n2. Tính WOE và IV  \nGọi hàm caculate_WOE_IV() để tính toán bảng WOE và giá trị IV cho biến đã bin (GRP_<feature>).\n\n3. Lấy các cột quan trọng  \nLấy các cột thống kê cần thiết để trình bày: số quan sát, số event, non-event, tỷ lệ event, WOE và IV.  \n\n4. Chuẩn hóa tên cột  \nĐổi tên cột sang dạng dễ hiểu và trực quan hơn cho báo cáo.  \n\n5. Tạo dòng tổng (TOTAL)  \nTính tổng các chỉ số chính như tổng count, tổng event, tổng non-event, tổng IV.  \nTính event rate tổng = tổng event / tổng quan sát.  \n\n6. Ghép dòng tổng vào bảng chính  \nDùng concat để ghép dòng TOTAL vào cuối bảng.  \n\n7. Đặt lại chỉ số dòng  Đổi index thành chuỗi và đặt dòng cuối tên là \"TOTAL\" để dễ hiển thị hoặc xuất file.","metadata":{}},{"cell_type":"code","source":"# SỬ DỤNG FUNCTION VỚI BIẾN days90_310L\ndf_woe_days90_310L = woe_feature_table(df, custom_bins = None, feature = 'days90_310L')\ndf_woe_days90_310L","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T16:48:10.205108Z","iopub.execute_input":"2025-11-09T16:48:10.205488Z","iopub.status.idle":"2025-11-09T16:48:10.247265Z","shell.execute_reply.started":"2025-11-09T16:48:10.205450Z","shell.execute_reply":"2025-11-09T16:48:10.246100Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# FUNCTION CROSS 2 BIẾN","metadata":{}},{"cell_type":"code","source":"# FUNCTION CROSS 2 BIẾN\n# Hàm tạo biến kết hợp giữa 2 biến đầu vào và tính bảng WOE + pivot table\n\ndef cross_variables(df, feature1, feature2, custom_bins=None):\n    # Thực hiện binning (chia nhóm) cho cả 2 biến đầu vào\n    df = binning_all_variables(df, threshold=0.05, custom_bins=custom_bins, feature=feature1)\n    df = binning_all_variables(df, threshold=0.05, custom_bins=custom_bins, feature=feature2)\n\n    # Tạo tên cột group sau khi binning\n    grp1 = 'GRP_' + feature1\n    grp2 = 'GRP_' + feature2\n    cross_feature = f\"{feature1}_{feature2}_cross\"\n\n    # Tạo biến mới kết hợp giữa 2 biến đã binning (dạng chuỗi nối với '*')\n    df[cross_feature] = df[grp1].astype(str) + '*' + df[grp2].astype(str)\n\n    # Tính bảng WOE và IV cho biến kết hợp\n    df_woe, _ = caculate_WOE_IV(df, cross_feature, 'target')\n\n    # Tách lại tên bin của 2 biến ban đầu từ biến kết hợp\n    df_woe[[grp1, grp2]] = df_woe['bin'].str.split('*', expand=True)\n\n    # HÀM PARSE: Chuyển đổi chuỗi dạng '(a, b]' về kiểu pd.Interval để giữ thứ tự logic\n    def parse_interval(x):\n        try:\n            if x == 'Missing':\n                return x\n            x = x.replace('(', '[')  # Đồng nhất dấu ngoặc\n            bounds = x.strip('[]').split(',')\n            return pd.Interval(float(bounds[0]), float(bounds[1]), closed='right')\n        except:\n            return x\n\n    # Áp dụng chuẩn hóa lại kiểu dữ liệu cho cả 2 biến đã tách\n    df_woe[grp1] = df_woe[grp1].apply(parse_interval)\n    df_woe[grp2] = df_woe[grp2].apply(parse_interval)\n\n    # Sau khi đã sử dụng, loại bỏ cột bin gốc khỏi df ban đầu để tránh dư thừa\n    df.drop(columns=[grp1, grp2], inplace=True)\n\n    # Tạo bảng pivot để thể hiện phân tích theo 2 chiều (2 biến)\n    pivot_df = df_woe.pivot(index=grp1, columns=grp2, values=[\"event_rate\", \"num_of_obs\", \"prct_obs\", \"WOE\"])\n\n    # Đổi tên trục hàng và cột trong bảng pivot theo tên biến gốc\n    pivot_df = pivot_df.rename_axis(index={f\"GRP_{feature1}\": feature1}, columns={f\"GRP_{feature2}\": feature2})\n\n    # Sắp xếp lại các cột theo thứ tự logic của nhóm\n    pivot_df = pivot_df.sort_index(axis=1, level=1)\n\n    # Trả ra: df gốc đã xóa group, bảng WOE chi tiết, và bảng pivot theo 2 biến\n    return df, df_woe, pivot_df\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T16:48:59.672864Z","iopub.execute_input":"2025-11-09T16:48:59.673228Z","iopub.status.idle":"2025-11-09T16:48:59.683269Z","shell.execute_reply.started":"2025-11-09T16:48:59.673203Z","shell.execute_reply":"2025-11-09T16:48:59.682339Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# LOGIC CODE\n\n1. Binning từng biến đầu vào  \nDùng binning_all_variables() để chia nhóm theo custom bins nếu có.  \n\n2. Tạo biến tổ hợp (cross feature)  \nNối 2 bin lại thành một chuỗi, ví dụ: (0, 10]*(-inf, 1000]\n\n3. Tính WOE và IV cho biến tổ hợp  \nGọi caculate_WOE_IV() với biến tổ hợp.  \n\n4. Tách lại 2 phần từ biến tổ hợp  \nDùng str.split() để tách feature1 và feature2 ra từ chuỗi tổ hợp.\n\n5. Chuẩn hóa kiểu Interval để giữ thứ tự  \nDùng hàm parse_interval() để chuyển lại dạng pd.Interval.  \n\n6. Xóa các cột nhóm gốc trong DataFrame đầu vào  \nTránh làm rối hoặc trùng cột.  \n\n7. Tạo bảng pivot  \nGồm các chỉ số như event_rate, num_of_obs, WOE, prct_obs theo dạng bảng chéo 2 chiều.  \n\n8. Đổi nhãn cho trục  \nĐổi tên GRP_feature1, GRP_feature2 thành tên biến gốc.  \n\n9. Trả kết quả  \nTrả ra 3 đối tượng: df đã xử lý, bảng chi tiết WOE (df_woe), và bảng phân tích chéo (pivot_df).\n","metadata":{}},{"cell_type":"code","source":"# SỬ DỤNG FUNCTION\ndf, df_woe_cross, df_pivot = cross_variables(df, 'days90_310L', 'sex_738L', custom_bins=None)\ndf","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T16:51:11.005799Z","iopub.execute_input":"2025-11-09T16:51:11.006158Z","iopub.status.idle":"2025-11-09T16:51:11.070852Z","shell.execute_reply.started":"2025-11-09T16:51:11.006136Z","shell.execute_reply":"2025-11-09T16:51:11.069990Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_woe_cross","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T16:51:25.321285Z","iopub.execute_input":"2025-11-09T16:51:25.321638Z","iopub.status.idle":"2025-11-09T16:51:25.341990Z","shell.execute_reply.started":"2025-11-09T16:51:25.321615Z","shell.execute_reply":"2025-11-09T16:51:25.341018Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_pivot","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T16:51:42.685285Z","iopub.execute_input":"2025-11-09T16:51:42.686067Z","iopub.status.idle":"2025-11-09T16:51:42.702436Z","shell.execute_reply.started":"2025-11-09T16:51:42.686035Z","shell.execute_reply":"2025-11-09T16:51:42.701593Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# FUNCTION VẼ BIỂU ĐỒ WOE CHO TỪNG BIẾN\n","metadata":{}},{"cell_type":"code","source":"#FUNCTION VẼ BIỂU ĐỒ WOE CHO TỪNG BIẾN\n# Hàm vẽ biểu đồ WOE stacked bar chart + đường line WOE cho một biến đã qua binning\nimport matplotlib.pyplot as plt  # Vẽ biểu đồ (line, bar, histogram,...)\nimport matplotlib.pyplot as plt  # Vẽ biểu đồ (line, bar, histogram,...)\nimport matplotlib.cm as cm  # Dùng để lấy colormap (phối màu)\nimport matplotlib.colors as mcolors  # Quản lý màu sắc (RGB, HEX,...)\nimport matplotlib.patches as mpatches  # Tạo mẫu màu dùng cho chú thích (legend)\ndef woe_plot(df, feature):\n    # Loại bỏ dòng 'TOTAL' nếu có trong bảng dữ liệu WOE\n    df = df[df.index != \"TOTAL\"]\n\n    # Tạo colormap từ đỏ -> vàng -> xanh theo giá trị WOE\n    cmap = plt.colormaps[\"RdYlGn\"]\n    norm = mcolors.Normalize(vmin=df['WOE'].min(), vmax=df['WOE'].max())  # Chuẩn hóa giá trị WOE để gán màu tương ứng\n\n    # Tạo biểu đồ chính\n    fig, ax1 = plt.subplots(figsize=(9, 7))\n    bins = np.arange(len(df))  # Trục x là các bin thứ tự\n\n    # Xác định màu của từng bin theo giá trị WOE\n    colors = [cmap(norm(woe)) for woe in df['WOE']]\n\n    # Vẽ stacked bar: event + non-event, dùng cùng màu (tùy theo giá trị WOE)\n    ax1.bar(bins, df['event'], label='Event', color=colors, alpha=1)\n    ax1.bar(bins, df['non_event'], label='Non-event', bottom=df['event'], color=colors, alpha=1)\n\n    # Cấu hình trục y bên trái\n    ax1.set_xlabel('Bin')\n    ax1.set_ylabel('Count')\n    ax1.set_xticks(bins)\n    ax1.set_xticklabels(df['bin'], rotation=45, ha='right')\n\n    # Tạo trục y bên phải và vẽ đường WOE\n    ax2 = ax1.twinx()\n    ax2.plot(bins, df['WOE'], color='black', marker='o', markersize=6, linestyle='-', linewidth=2, label='WOE')\n    ax2.set_ylabel('WOE')\n\n    # Thêm tiêu đề và lưới nhẹ\n    ax1.grid(axis='y', linestyle='--', alpha=0.7)\n    plt.title(f'WOE Plot for {feature}', fontsize=12, fontweight='bold')\n\n    # Định nghĩa các chú thích màu đại diện cho rủi ro\n    high_risk_patch = mpatches.Patch(facecolor='red', label='Rủi ro cao')\n    medium_risk_patch = mpatches.Patch(facecolor='yellow', label='Rủi ro trung bình')\n    low_risk_patch = mpatches.Patch(facecolor='green', label='Rủi ro thấp')\n\n    # Hiển thị chú thích theo dạng hàng ngang phía dưới biểu đồ\n    ax1.legend(\n        handles=[high_risk_patch, medium_risk_patch, low_risk_patch],\n        bbox_to_anchor=(0.9, -0.5),\n        frameon=True,\n        ncol=3\n    )\n\n    plt.tight_layout()\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T16:55:19.971551Z","iopub.execute_input":"2025-11-09T16:55:19.971961Z","iopub.status.idle":"2025-11-09T16:55:19.986155Z","shell.execute_reply.started":"2025-11-09T16:55:19.971935Z","shell.execute_reply":"2025-11-09T16:55:19.985181Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# LOGIC CODE\n\n1. Loại bỏ dòng 'TOTAL'  \nĐảm bảo biểu đồ chỉ vẽ các bin thực tế.  \n\n2. Chuẩn hóa giá trị WOE và tô màu  \nDùng colormap \"RdYlGn\" để tô màu từ đỏ (WOE thấp) đến xanh (WOE cao).  \nTính màu từng bin dựa trên giá trị WOE.  \n\n3. Tạo biểu đồ cột (stacked bar chart)  \nVẽ cột event.  \nChồng cột non-event lên phía trên để tạo stacked bar.  \n\n4. Vẽ trục phụ với đường WOE  \nTạo trục y thứ hai và vẽ đường biểu diễn giá trị WOE (line chart) kèm marker.  \n\n5. Thêm lưới, tiêu đề, nhãn  \nXoay nhãn trục x để dễ đọc.  \nThêm tiêu đề và nhãn cho 2 trục.  \n\n6. Hiển thị chú thích về rủi ro  \nDựa trên colormap, thêm các Patch tượng trưng cho rủi ro cao, trung bình, thấp.  \n\n7. Hiển thị biểu đồ  \nSử dụng tight_layout() để tránh tràn lề.  \nGọi plt.show() để hiển thị kết quả.","metadata":{}},{"cell_type":"code","source":"# SỬ DỤNG FUNCTION\nPLOT = woe_plot(df_woe_days90_310L, 'days90_310L')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T16:55:24.417606Z","iopub.execute_input":"2025-11-09T16:55:24.417929Z","iopub.status.idle":"2025-11-09T16:55:25.053809Z","shell.execute_reply.started":"2025-11-09T16:55:24.417908Z","shell.execute_reply":"2025-11-09T16:55:25.052802Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# FUNCTION XUẤT FILE EXCEL BẢNG VÀ BIỂU ĐỒ","metadata":{}},{"cell_type":"code","source":"def export_all_to_excel_1(df, custom_bins, features, output_file, df_TH):\n    # Khởi tạo writer và workbook\n    writer = pd.ExcelWriter(output_file, engine='xlsxwriter')\n    workbook = writer.book\n    worksheet = workbook.add_worksheet(\"WOE_All\")  # Tạo sheet\n    writer.sheets[\"WOE_All\"] = worksheet\n\n    current_row = 0  # Dòng bắt đầu ghi dữ liệu cho mỗi biến\n\n    cmap = plt.colormaps[\"RdYlGn\"]  # Colormap dùng cho biểu đồ WOE\n\n    # Duyệt qua từng biến trong danh sách features\n    for feature in tqdm(features, desc=\"Xuất biến\"):\n        metadata_start_row = current_row  # Lưu vị trí dòng đầu tiên cho metadata (cũng là nơi chèn biểu đồ)\n        # Lấy dòng metadata tương ứng trong df_TH theo tên biến\n        \n        row = df_TH[df_TH[\"Variable\"] == feature].squeeze()\n\n# Tạo metadata từ df_TH\n        metadata = [\n           [\"Tên biến\", feature],\n            [\"Nguồn\", row.get(\"Bảng\", \"\")],\n            [\"Loại thông tin\", row.get(\"Loại thông tin\", \"\")],\n            [\"Description gốc\", row.get(\"Description gốc\", \"\")],\n            [\"Mô tả\", row.get(\"Mô tả\", \"\")],\n            [\"Unique Values\", row.get(\"Unique Values\", \"\")],\n            [\"Clean\", row.get(\"Clean\", \"\")],\n            [\"Phân tích\", row.get(\"Phân tích\", \"\")],\n            [\"trend WOE\", \"\"],\n            [\"Note\", row.get(\"Note\", \"\")],\n            [\"Splits\", row.get(\"Splits\", \"\")]\n        ]\n        \n\n        # Chuyển metadata sang DataFrame để ghi nhanh vào Excel\n        metadata_df = pd.DataFrame(metadata)\n        metadata_df.to_excel(writer, sheet_name=\"WOE_All\", startrow=current_row, startcol=0, header=False, index=False)\n\n        current_row += len(metadata)  # Di chuyển dòng xuống sau metadata\n\n        # Tính bảng WOE cho biến hiện tại\n        woe_df = woe_feature_table(df, custom_bins, feature)\n\n        # Ghi bảng WOE vào Excel\n        woe_df.to_excel(writer, sheet_name=\"WOE_All\", startrow=current_row, startcol=0, index=False)\n\n        # Vẽ biểu đồ WOE cho biến hiện tại\n        fig = plt.figure(figsize=(6, 4))\n        temp_df = woe_df[woe_df.index != \"TOTAL\"]  # Bỏ dòng TOTAL\n        norm = mcolors.Normalize(vmin=temp_df['WOE'].min(), vmax=temp_df['WOE'].max())\n        colors = [cmap(norm(woe)) for woe in temp_df['WOE']]\n\n        bins = np.arange(len(temp_df))\n        ax1 = fig.add_subplot(111)\n        ax1.bar(bins, temp_df['event'], color=colors)\n        ax1.bar(bins, temp_df['non_event'], bottom=temp_df['event'], color=colors)\n        ax1.set_xticks(bins)\n        ax1.set_xticklabels(temp_df['bin'], rotation=45, ha='right')\n        fig.subplots_adjust(bottom=0.25)  # Tránh tràn nhãn dưới\n\n        ax2 = ax1.twinx()\n        ax2.plot(bins, temp_df['WOE'], color='black', marker='o', linewidth=1.5)\n        plt.title(f'WOE Plot for {feature}')\n        plt.tight_layout()\n\n        # Lưu biểu đồ vào bộ nhớ và chèn vào Excel\n        imgdata = BytesIO()\n        plt.savefig(imgdata, format='png')\n        imgdata.seek(0)\n        plt.close()\n\n        # Chèn hình ảnh vào Excel tại cột L (cột 11), dòng metadata_start_row\n        worksheet.insert_image(metadata_start_row, 11, f\"{feature}_plot.png\", {\n            'image_data': imgdata,\n            'x_scale': 1.2,\n            'y_scale': 1.2\n        })\n\n        # Cập nhật current_row: số dòng bảng WOE + khoảng trắng để cách biệt\n        current_row += len(woe_df) + 15\n\n    writer.close()  # Đóng file Excel","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# LOGIC CODE\n\n1. Khởi tạo writer và worksheet  \nDùng xlsxwriter để ghi dữ liệu + biểu đồ vào cùng một sheet Excel.\n\n2. Tạo colormap  \nDùng \"RdYlGn\" cho biểu đồ WOE (xanh = an toàn, đỏ = rủi ro).\n\n3. Vòng lặp qua từng biến  \nGhi phần metadata đầu tiên bằng DataFrame.to_excel() để nhanh và gọn.  \nTính WOE bảng bằng woe_feature_table().  \nGhi bảng WOE ra sau metadata.\n\n4. Vẽ biểu đồ  \nVẽ stacked bar chart event + non_event theo màu.  \nVẽ đường biểu diễn WOE trùng trục X.  \nDùng BytesIO để lưu ảnh vào bộ nhớ và chèn trực tiếp vào Excel.\n\n5. Cập nhật dòng tiếp theo  \nĐảm bảo mỗi biến có khoảng trắng giữa các phần để dễ đọc.\n\n6. Đóng writer  \nGhi file Excel hoàn chỉnh.\n\n","metadata":{}},{"cell_type":"markdown","source":"# FUNCTION CHUYỂN RAW_DATA VỀ WOE_DATA","metadata":{}},{"cell_type":"code","source":"def transform_to_woe(df, features, target, custom_bins=None, feature=None):\n    # Tạo bản sao để tránh thay đổi dữ liệu gốc\n    df_transformed = df.copy()\n\n    # Binning biến (nếu cung cấp 1 biến riêng lẻ) để thêm cột GRP_<feature>\n    df_transformed = binning_all_variables(df, threshold=0.05, custom_bins=custom_bins, feature=feature)\n\n    # Xác định danh sách biến cần xử lý\n    features_to_process = [feature] if feature else features\n\n    # Lặp qua từng biến để tạo biến WOE tương ứng\n    for feature in features_to_process:\n        # Kiểm tra biến đã được binning chưa (phải có cột GRP_<feature>)\n        if 'GRP_' + feature not in df_transformed.columns:\n            print(f'Warning: GRP_{feature} không tồn tại trong dữ liệu sau binning!')\n            continue\n\n        # Tính bảng WOE và IV từ biến đã binning\n        woe_iv, iv = caculate_WOE_IV(df_transformed, 'GRP_' + feature, target)\n\n        # Đảm bảo kiểu dữ liệu bin là chuỗi (tránh lỗi khi map)\n        woe_iv['bin'] = woe_iv['bin'].astype(str)\n        df_transformed['GRP_' + feature] = df_transformed['GRP_' + feature].astype(str)\n\n        # Tạo mapping từ bin sang WOE\n        woe_mapping = woe_iv.set_index('bin')['WOE'].rename('WOE_' + feature)\n\n        # Gán giá trị WOE tương ứng cho từng dòng\n        df_transformed['WOE_' + feature] = df_transformed['GRP_' + feature].map(woe_mapping)\n\n        # Đảm bảo kết quả là kiểu float\n        df_transformed['WOE_' + feature] = df_transformed['WOE_' + feature].astype(float)\n\n    # Trả về DataFrame đã thêm cột WOE_<feature> tương ứng\n    return df_transformed\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T17:01:42.520179Z","iopub.execute_input":"2025-11-09T17:01:42.520562Z","iopub.status.idle":"2025-11-09T17:01:42.529001Z","shell.execute_reply.started":"2025-11-09T17:01:42.520535Z","shell.execute_reply":"2025-11-09T17:01:42.528168Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# LOGIC CODE\n\n1. Sao chép dữ liệu đầu vào  \nĐảm bảo không làm thay đổi df gốc.\n\n2. Binning biến  \nNếu có biến đơn lẻ (feature) → chỉ binning 1 biến.  \nNếu không → sẽ xử lý tất cả biến trong danh sách features.\n\n3. Lặp qua từng biến cần xử lý  \nKiểm tra đã có cột GRP_ chưa.  \nTính WOE theo nhóm đã binning (GRP_<feature>).  \nChuyển kiểu bin và giá trị về str để đảm bảo map chính xác.  \nGán giá trị WOE vào cột mới WOE_<feature>.\n\n4. Trả kết quả  \nTrả về df_transformed với các cột mới chứa WOE.","metadata":{}},{"cell_type":"code","source":"sl = ['days90_310L', 'sex_738L']\ndf_transformed = transform_to_woe(df, sl, 'target', custom_bins=None, feature=None)\ndf_transformed","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T17:02:15.736438Z","iopub.execute_input":"2025-11-09T17:02:15.736762Z","iopub.status.idle":"2025-11-09T17:02:15.799726Z","shell.execute_reply.started":"2025-11-09T17:02:15.736741Z","shell.execute_reply":"2025-11-09T17:02:15.798884Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# FUNCTION LIÊN KẾT SQL LẤY DỮ LIỆU","metadata":{}},{"cell_type":"code","source":"def get_data_from_sql(server, database, table):\n    \"\"\"\n    Kết nối SQL Server và lấy dữ liệu từ bảng chỉ định.\n\n    Args:\n        server (str): Tên server SQL.\n        database (str): Tên database trong SQL Server.\n        table (str): Tên bảng cần lấy dữ liệu.\n\n    Returns:\n        pd.DataFrame: Dữ liệu bảng dưới dạng DataFrame.\n    \"\"\"\n    # Tạo chuỗi kết nối sử dụng Trusted_Connection (dùng Windows Authentication)\n    conn_str = (\n        f\"DRIVER={{SQL Server}};\"\n        f\"SERVER={server};\"\n        f\"DATABASE={database};\"\n        f\"Trusted_Connection=yes;\"\n    )\n\n    # Thiết lập kết nối\n    conn = pyodbc.connect(conn_str)\n\n    # Câu lệnh truy vấn SQL để lấy toàn bộ bảng\n    query = f\"SELECT * FROM {table}\"\n\n    # Đọc dữ liệu vào DataFrame\n    df = pd.read_sql(query, conn)\n\n    # Đóng kết nối\n    conn.close()\n\n    # Trả lại DataFrame chứa dữ liệu\n    return df\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# LOGIC CODE\n1. Tạo chuỗi kết nối (conn_str)  \nDùng pyodbc, đăng nhập bằng Trusted_Connection (Windows).\n\n2. Kết nối tới SQL Server  \nGọi pyodbc.connect(conn_str).\n\n3. Thực thi truy vấn SQL  \nLệnh \"SELECT * FROM <table>\".\n\n4. Đọc kết quả vào DataFrame  \nDùng pd.read_sql(query, conn).\n\n5. Đóng kết nối SQL  \nGiải phóng tài nguyên.\n\n6. Trả về dữ liệu  \nDạng pandas.DataFrame.","metadata":{}},{"cell_type":"markdown","source":"# CHẠY THUẬT TOÁN ĐỂ CHỌN BỘ BIẾN","metadata":{}},{"cell_type":"code","source":"# Khởi tạo từ điển rỗng để lưu IV của từng biến\niv_dict = {}\n\n# Duyệt qua từng cột trong dữ liệu huấn luyện\nfor feature in df_train_iv.columns:\n    # Bỏ qua các cột target và case_id vì không tính IV cho chúng\n    if feature in ['target', 'case_id']:\n        continue\n\n    try:\n        # Tính bảng WOE cho biến hiện tại (đã có hàm woe_feature_table xử lý binning & WOE)\n        woe_table = woe_feature_table(df_train_iv, custom_bins=ctb_df, feature=feature)\n        \n        # Lấy giá trị IV ở dòng 'TOTAL' từ bảng kết quả\n        iv = woe_table.loc['TOTAL', 'IV']\n\n        # Ghi lại IV vào từ điển\n        iv_dict[feature] = iv\n\n    except Exception as e:\n        # Nếu có lỗi xảy ra, in thông báo lỗi kèm tên biến\n        print(f\"Lỗi khi tính IV cho biến {feature}: {e}\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Hàm thực hiện lựa chọn biến theo phương pháp forward stepwise greedy\n\ndef forward_variable_selection(X_woe, y, iv_dict, \n                                iv_threshold=0.02, corr_threshold=0.5, \n                                verbose=True):\n    # Danh sách biến được chọn\n    selected_vars = []\n\n    # Lọc các biến còn lại có IV đủ lớn\n    remaining_vars = [var for var in X_woe.columns if iv_dict.get(var, 0) > iv_threshold]\n\n    # Lưu lịch sử chọn biến: tên, GINI, số biến đã chọn\n    history = []\n\n    # Khởi tạo ma trận X hiện tại rỗng (chỉ index đúng)\n    current_X = pd.DataFrame(index=X_woe.index)\n\n    while True:\n        best_var = None       # Biến tốt nhất trong vòng hiện tại\n        best_gini = -np.inf   # GINI cao nhất\n        best_model = None     # Mô hình tốt nhất tạm thời\n\n        for var in remaining_vars:\n            # Kiểm tra tương quan với các biến đã chọn\n            if selected_vars:\n                corrs = X_woe[selected_vars + [var]].corr(method=\"spearman\").iloc[:-1, -1]\n                if corrs.abs().max() > corr_threshold:\n                    continue  # Bỏ qua nếu tương quan vượt ngưỡng\n\n            # Fit mô hình mới với biến này thêm vào current_X\n            temp_X = add_constant(pd.concat([current_X, X_woe[[var]]], axis=1))\n            model = Logit(y, temp_X).fit(disp=0)\n            pred = model.predict(temp_X)\n            auc = roc_auc_score(y, pred)\n            gini = 2 * auc - 1\n\n            # Cập nhật nếu GINI tốt hơn\n            if gini > best_gini:\n                best_gini = gini\n                best_var = var\n                best_model = model\n\n        # Nếu không còn biến nào được chọn thì dừng\n        if best_var is None:\n            if verbose:\n                print(\"✅ Dừng: Không còn biến thỏa điều kiện.\")\n            break\n\n        # Lưu lại biến được chọn tốt nhất\n        selected_vars.append(best_var)\n        current_X = pd.concat([current_X, X_woe[[best_var]]], axis=1)\n        remaining_vars.remove(best_var)\n\n        # Ghi lại lịch sử lựa chọn\n        history.append({\n            'selected_var': best_var,\n            'gini': best_gini,\n            'n_vars': len(selected_vars)\n        })\n\n        if verbose:\n            print(f\"✅ Chọn biến: {best_var: <20} | GINI: {best_gini:.4f}\")\n\n    # Trả về danh sách biến được chọn và lịch sử theo dạng DataFrame\n    return selected_vars, pd.DataFrame(history)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# LOGIC CODE\n\nMục tiêu: Tự động chọn biến theo hướng greedy-forward, tối ưu hóa GINI  \n\nVòng lặp chọn biến:\n\n1. Lọc các biến đủ điều kiện (IV > ngưỡng, không tương quan cao - thấp hơn corr_threshold).  \nVới mỗi biến, fit mô hình Logistic Regression.\n\n2. Đánh giá mô hình bằng AUC → GINI.  \nGiữ lại biến cải thiện GINI tốt nhất.\n\n3. Nếu không còn biến thỏa, thì dừng vòng lặp.","metadata":{}},{"cell_type":"markdown","source":"# FUNCTION MAP BINNING VÀ WOE TỪ TRAIN SANG TEST","metadata":{}},{"cell_type":"code","source":"# Hàm áp dụng binning và WOE cho tập test dựa trên thông tin từ tập train\n\ndef apply_multiple_bin_woe(df_test, df_train, ctb_dict):\n    # Tạo bản sao để không thay đổi dữ liệu gốc\n    df_test = df_test.copy()\n\n    # Lấy danh sách các cột GRP_ từ train (đã binning sẵn)\n    grp_cols = [col for col in df_train.columns if col.startswith('GRP_')]\n    all_varnames = [col.replace('GRP_', '') for col in grp_cols]\n\n    # Chỉ giữ lại những biến gốc có trong test\n    varnames = [var for var in all_varnames if var in df_test.columns]\n\n    print(f\"[ℹ️] Đang map WOE cho {len(varnames)} biến có trong test: {varnames}\")\n\n    # Duyệt từng biến\n    for var in varnames:\n        try:\n            bin_col = f'GRP_{var}'        # Tên cột nhóm\n            woe_col = f'WOE_{var}'        # Tên cột WOE\n\n            # Đánh dấu missing\n            missing_mask = df_test[var].isna()\n\n            if var in ctb_dict:  # Biến số có bin\n                bin_edges = [-np.inf] + ctb_dict[var] + [np.inf]  # Tạo khoảng bin\n                df_test[bin_col] = pd.cut(df_test[var], bins=bin_edges, include_lowest=True)\n                df_test[bin_col] = df_test[bin_col].astype(object)\n\n                # Gán \"Missing\" nếu có thiếu\n                if missing_mask.any():\n                    df_test.loc[missing_mask, bin_col] = 'Missing'\n\n            else:  # Biến phân loại (categorical)\n                df_test[bin_col] = df_test[var].astype(object)\n\n                if missing_mask.any():\n                    df_test.loc[missing_mask, bin_col] = 'Missing'\n\n            # Trích mapping WOE từ train\n            mapping = df_train[[bin_col, woe_col]].drop_duplicates()\n            bin_to_woe = {str(k): v for k, v in zip(mapping[bin_col], mapping[woe_col])}\n\n            # Map giá trị WOE tương ứng từ nhóm\n            df_test[woe_col] = df_test[bin_col].astype(str).map(bin_to_woe)\n\n        except Exception as e:\n            print(f\"[⚠️] Lỗi với biến '{var}': {e}\")\n\n    # Trả lại dữ liệu test đã gán WOE\n    return df_test","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# LOGIC CODE\n\nMục tiêu: Gán lại nhóm (GRP_) và giá trị WOE (WOE_) cho các biến trong tập test, dựa theo thông tin đã học từ tập train.  \n\n1. Xác định danh sách biến cần xử lý:  \nLấy các biến đã có GRP_ trong train  \nKiểm tra xem biến gốc đó có tồn tại trong test không\n\n2. Với mỗi biến:  \nNếu là biến số: bin lại bằng pd.cut theo ctb_dict  \nNếu là biến phân loại: giữ nguyên hoặc gán 'Missing' nếu null\n\n3. Lấy mapping từ train (GRP_ → WOE_)  \nSau đó gán WOE tương ứng vào test (df_test)\n\n","metadata":{}},{"cell_type":"markdown","source":"# FUNCTION TÍNH CHỈ SỐ PSI","metadata":{}},{"cell_type":"code","source":"# Hàm Psi chia đều số obs bằng nhau\ndef calculate_psi_percentile(expected, actual, bins=10, verbose=True):\n    \"\"\"\n    Tính PSI giữa hai phân phối với cách chia bin theo percentile (mỗi bin có số lượng quan sát gần bằng nhau).\n\n    Args:\n        expected: phân phối gốc (vd: train scores)\n        actual: phân phối mới (vd: test scores)\n        bins: số bin (mặc định 10)\n        verbose: nếu True thì in kết quả từng nhóm\n\n    Returns:\n        psi_total: tổng PSI\n        psi_df: DataFrame chứa PSI từng nhóm\n    \"\"\"\n    # 1. Xác định breakpoints theo percentiles từ expected\n    quantiles = np.linspace(0, 1, bins + 1)\n    breakpoints = np.quantile(expected, quantiles)\n    breakpoints[0] = -np.inf  # để bao trùm toàn bộ giá trị\n    breakpoints[-1] = np.inf\n\n    # 2. Phân loại bin theo breakpoints\n    expected_bins = pd.cut(expected, bins=breakpoints, include_lowest=True)\n    actual_bins = pd.cut(actual, bins=breakpoints, include_lowest=True)\n\n    # 3. Tính % từng bin\n    expected_percents = expected_bins.value_counts(sort=False, normalize=True).values\n    actual_percents = actual_bins.value_counts(sort=False, normalize=True).values\n\n    # 4. Thay epsilon để tránh chia 0\n    epsilon = 1e-6\n    expected_percents = np.where(expected_percents == 0, epsilon, expected_percents)\n    actual_percents = np.where(actual_percents == 0, epsilon, actual_percents)\n\n    # 5. Tính PSI từng bin\n    psi_values = (expected_percents - actual_percents) * np.log(expected_percents / actual_percents)\n    psi_total = np.sum(psi_values)\n\n    # 6. Tạo bảng PSI\n    bin_labels = [f\"[{breakpoints[i]:.2f}, {breakpoints[i+1]:.2f})\" for i in range(bins)]\n    psi_df = pd.DataFrame({\n        'bin_range': bin_labels,\n        'train_scores_pct': expected_percents,\n        'oot_scores_pct': actual_percents,\n        'psi_value': psi_values\n    })\n\n    if verbose:\n        print(psi_df)\n        print(f\"\\n➡ Tổng PSI: {psi_total:.6f}\")\n\n    return psi_total, psi_df","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# LOGIC CODE\n\nMục tiêu: Đo sự khác biệt về phân phối giữa tập train (expected) và test (actual).  \n\n1. Xác định khoảng min–max chung giữa 2 tập.\n\n2. Chia đều thành bins nhóm theo khoảng giá trị (10 nhóm).\n\n3. Tính tỉ lệ phần trăm của mỗi bin trong expected  và actual.  \nPi = (số lượng obs trong bin)/ (tổng số obs train)  \nQi = (số lượng obs trong bin test)/ (tổng số obs test)\n\n4. Tính chỉ số PSI cho từng bin (Pi - Qi)*log(Pi/Qi)  \n\n5. Tổng hợp các giá trị lại thành PSI tổng. (sum psi từng bin nhỏ)","metadata":{}},{"cell_type":"code","source":"# VISUALIZE PSI VÀ XUẤT FILE EXCEL\ndef plot_psi(psi_detail, psi_total, title=\"PSI Distribution\", excel_file=None):\n    \"\"\"\n    Vẽ biểu đồ PSI và (nếu có) xuất ra file Excel.\n    psi_detail: DataFrame có các cột ['bin_range', 'train_scores_pct', 'oot_scores_pct', 'psi_value']\n    psi_total: tổng PSI\n    title: tiêu đề biểu đồ\n    excel_file: tên file excel để lưu (vd: 'psi_output.xlsx'), nếu None thì chỉ hiển thị\n    \"\"\"\n    # Chuyển tỷ lệ về %\n    df = psi_detail.copy()\n    df['train_scores_pct'] = df['train_scores_pct'] * 100\n    df['oot_scores_pct']   = df['oot_scores_pct'] * 100\n\n    # Vẽ biểu đồ\n    fig, ax1 = plt.subplots(figsize=(12,6))\n    width = 0.35\n    x = range(len(df))\n\n    ax1.bar([i - width/2 for i in x], df['train_scores_pct'], \n            width=width, label='Train %', alpha=0.7)\n    ax1.bar([i + width/2 for i in x], df['oot_scores_pct'], \n            width=width, label='OOT %', alpha=0.7)\n\n    ax1.set_xlabel(\"Bin Range\")\n    ax1.set_ylabel(\"Percentage (%)\")\n    ax1.set_xticks(list(x))\n    ax1.set_xticklabels(df['bin_range'], rotation=45, ha='right')\n    ax1.legend(loc='upper left')\n\n    ax2 = ax1.twinx()\n    ax2.plot(x, df['psi_value'], color='red', marker='o', label='PSI Value')\n    ax2.set_ylabel(\"PSI Value\")\n    ax2.legend(loc='upper right')\n\n    plt.title(f\"{title}\\nTotal PSI = {psi_total:.4f}\")\n    plt.tight_layout()\n\n    # Nếu có excel_file thì lưu biểu đồ vào Excel\n    if excel_file:\n        img_data = BytesIO()\n        fig.savefig(img_data, format='png')\n        img_data.seek(0)\n\n        wb = Workbook()\n        ws = wb.active\n        ws.title = \"PSI_Chart\"\n\n        img = Image(img_data)\n        img.anchor = \"A1\"\n        ws.add_image(img)\n\n        wb.save(excel_file)\n        print(f\"✅ Biểu đồ đã được lưu vào {excel_file}\")\n\n    plt.show()\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# FUNCTION TÍNH CSI VÀ XUẤT FILE EXCEL","metadata":{}},{"cell_type":"code","source":"def calculate_csi_discrete_verbose(expected_series, actual_series, verbose=True):\n    \"\"\"\n    Tính CSI cho biến rời rạc + hiển thị chi tiết từng nhóm.\n\n    Returns:\n        csi_total: CSI tổng\n        csi_df: DataFrame chi tiết từng nhóm\n    \"\"\"\n    # Tính phân phối\n    expected_dist = expected_series.value_counts(normalize=True)\n    actual_dist = actual_series.value_counts(normalize=True)\n\n    expected_dist.index = expected_dist.index.astype(str)\n    actual_dist.index = actual_dist.index.astype(str)\n\n    all_bins = sorted(set(expected_dist.index).union(actual_dist.index))\n    epsilon = 1e-6\n\n    rows = []\n    csi_total = 0\n\n    for bin_val in all_bins:\n        p = expected_dist.get(bin_val, 0)\n        q = actual_dist.get(bin_val, 0)\n\n        p_adj = max(p, epsilon)\n        q_adj = max(q, epsilon)\n\n        csi_bin = (p_adj - q_adj) * np.log(p_adj / q_adj)\n        csi_total += csi_bin\n\n        rows.append({\n            'bin': bin_val,\n            'expected_pct': p,\n            'actual_pct': q,\n            'csi_value': csi_bin\n        })\n\n    csi_df = pd.DataFrame(rows)\n\n    if verbose:\n        print(csi_df)\n        print(f\"\\n➡ CSI tổng: {round(csi_total, 4)}\")\n\n    return round(csi_total, 4), csi_df\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def calculate_csi_for_features_verbose(df_train, df_test, features, verbose=True):\n    \"\"\"\n    Tính CSI cho nhiều biến + hiển thị chi tiết từng nhóm.\n\n    Returns:\n        csi_summary_df: Bảng tổng CSI\n        csi_detail_dict: Dict chứa bảng chi tiết từng biến (đã thêm tên biến)\n    \"\"\"\n    csi_results = []\n    csi_detail_dict = {}\n\n    for col in features:\n        expected_series = df_train[col].astype(str)\n        actual_series = df_test[col].astype(str)\n\n        csi_val, csi_df = calculate_csi_discrete_verbose(expected_series, actual_series, verbose=verbose)\n\n        # Thêm cột Feature để dễ nhận biết khi export\n        csi_df = csi_df.copy()\n        csi_df.insert(0, \"Feature\", col)\n\n        csi_results.append({'feature': col, 'csi': csi_val})\n        csi_detail_dict[col] = csi_df\n\n    csi_summary_df = pd.DataFrame(csi_results).sort_values(by='csi', ascending=False).reset_index(drop=True)\n    return csi_summary_df, csi_detail_dict\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# VISUALIZE CSI VÀ XUẤT FILE EXCEL\nimport matplotlib.pyplot as plt\nfrom io import BytesIO\nfrom openpyxl import Workbook\nfrom openpyxl.drawing.image import Image\n\ndef export_csi_report(csi_summary, csi_details, excel_file=\"csi_report.xlsx\"):\n    \"\"\"\n    Xuất CSI detail + chart cho từng biến vào 1 file Excel.\n    - Bảng CSI detail từ cột A\n    - Biểu đồ CSI từ cột F\n    - Mỗi biến cách nhau 10 dòng\n    \"\"\"\n    wb = Workbook()\n    ws = wb.active\n    ws.title = \"CSI_Report\"\n\n    row = 1  # dòng bắt đầu ghi\n    \n    for feature, df in csi_details.items():\n        # --- Ghi tên biến ---\n        ws.cell(row=row, column=1, value=f\"Feature: {feature}\")\n        row += 1\n\n        # --- Ghi bảng CSI detail ---\n        # header\n        for j, col in enumerate(df.columns, start=1):\n            ws.cell(row=row, column=j, value=col)\n        row += 1\n        # data\n        for i in range(len(df)):\n            for j, col in enumerate(df.columns, start=1):\n                ws.cell(row=row+i, column=j, value=df.iloc[i, j-1])\n        row += len(df)\n\n        # --- Xác định tên cột cho train/oot ---\n        colnames = [c.lower() for c in df.columns]\n        if \"train_pct\" in colnames:\n            train_col, oot_col = \"train_pct\", \"oot_pct\"\n        elif \"train_scores_pct\" in colnames:\n            train_col, oot_col = \"train_scores_pct\", \"oot_scores_pct\"\n        elif \"expected_pct\" in colnames:\n            train_col, oot_col = \"expected_pct\", \"actual_pct\"\n        else:\n            raise KeyError(f\"Không tìm thấy cột tỷ lệ trong bảng CSI của {feature}: {df.columns.tolist()}\")\n\n        # --- Vẽ biểu đồ CSI ---\n        fig, ax1 = plt.subplots(figsize=(6,4))\n        x = range(len(df))\n        width = 0.35\n\n        ax1.bar([i - width/2 for i in x], df[train_col]*100, width=width, alpha=0.7, label=\"Train %\")\n        ax1.bar([i + width/2 for i in x], df[oot_col]*100,   width=width, alpha=0.7, label=\"OOT %\")\n\n        ax1.set_ylabel(\"Percentage (%)\")\n        ax1.set_xticks(list(x))\n        ax1.set_xticklabels(df['bin'], rotation=45, ha='right')\n        ax1.legend(loc=\"upper left\")\n\n        if \"csi_value\" in df.columns:\n            ax2 = ax1.twinx()\n            ax2.plot(x, df['csi_value'], color=\"red\", marker=\"o\", label=\"CSI Value\")\n            ax2.set_ylabel(\"CSI Value\")\n            ax2.legend(loc=\"upper right\")\n\n        plt.title(f\"CSI - {feature}\")\n        plt.tight_layout()\n\n        # --- Lưu chart vào bộ nhớ ---\n        img_data = BytesIO()\n        fig.savefig(img_data, format=\"png\")\n        plt.close(fig)\n        img_data.seek(0)\n\n        # --- Chèn chart vào Excel ---\n        img = Image(img_data)\n        img.anchor = f\"F{row-len(df)-1}\"  # đặt biểu đồ song song với bảng\n        ws.add_image(img)\n\n        # --- Tạo khoảng cách 10 dòng ---\n        row += 10\n\n    wb.save(excel_file)\n    print(f\"✅ CSI report đã được lưu tại {excel_file}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}