En los últimos años, los casos de fraude de telecomunicaciones nacionales se han vuelto cada vez más intensos. Este artículo utiliza una versión simplificada del modelo antifraude de una empresa provincial de telecomunicaciones como caso, utilizando herramientas de aprendizaje automático de Python, utilizando algoritmos forestales aleatorios, a partir del procesamiento de datos. , desde la ingeniería de características hasta los modelos antifraude. Un simple registro e introducción del proceso completo de construcción y evaluación del modelo.
diagrama de flujo
Configuración del entorno, carga del módulo
# coding: utf-8 import os import numpy as np import pandas as pd from sklearn.ensemble import IsolationForest de sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestClassifier de sklearn.metrics import confusion_matrix de sklearn.externals import joblibtrics de desde scipy import stats import time from datetime import datetime import warnings warnings.filterwarnings ("ignore") os.chdir ('home // zj // python // python3.6.9 // bin // python3') 123456789101112131415161718
Carga de datos
Personalice el directorio de trabajo y cargue datos de muestra
def read_file (filepath): os.chdir (os.path.dirname (filepath)) return pd.read_csv (os.path.basename (filepath), encoding = 'utf-8') file_pos = "E: \\ archivo de trabajo \\ *** \\ Identificación antifraude \\ data_train.csv " data_pos = read_file (file_pos) 123456
Cambio de nombre de función
data_pos.columns = ['BIL_ACCS_NBR', 'ASSET_ROW_ID', 'CCUST_ROW_ID', 'LATN_ID', 'TOTAL_CNT', 'TOTAL_DURATION', 'ZJ_CNT', 'ZJ_TOTAL_DURATION', 'Z_CJ_CTAL_ROAM_ ZJ_ROAM_DURATION', 'ZJ_LOCAL_DURATION', 'ZJ_LONG_CNT', 'BJ_LOCAL_CNT', 'WORK_TIME_TH_TT_CNT', 'FREE_TIME_TH_TT_CNT', 'NIGHT_TIME_TH_TT_CNT', 'DURATION_TP_1', 'DURATION_TP_2', 'DURATION_TP_3', 'DURATION_TP_4', 'DURATION_TP_5', 'DURATION_TP_6' , 'DURATION_TP_7', 'DURATION_TP_8', 'DURATION_TP_9', 'TOTAL_DIS_BJ_NUM', 'DIS_BJ_NUM', 'DIS_OPP_HOME_NUM', 'OPP_HOME_NUM', 'MSC_NUM', 'DIS_MSC_NUM', 'ZJ_AVG_DURATION', 'TOTAL_ROAM_CNT_RATE', 'ZJ_DURATION_RATE', 'ZJ_CNT_RATE', 'ZJ_ROAM_DURATION_RATE', 'ZJ_ROAM_CNT_RATE', 'DURATION_RATIO_0_15', 'DURATION_RATIO_15_30', 'DURATION_RATIO_30_45', 'DURATION_RATIO_45_60', 'DURATION_RATIO_60_300', 'DUR_30_CNT_RATE', 'DUR_60_CNT_RATE', 'DUR_90_CNT_RATE', 'DUR_120_CNT_RATE', 'DUR_180_CNT_RATE', 'DUR_BIGGER_180_CNT_RATE', 'DIS_BJ_NUM_RATE', 'TOTAL_DIS_BJ_NUM_RATE', 'CALLING_REGION_DISTRI_LEVEL', 'ACT_DAY', 'ACT_DAY_RATE', 'WEEK_DIS_BJ_NUM', 'YY_WORK_DAY_OIDD_23_NUM', 'IS_GJMY ', 'ZJ_DURATION_0_15_CNT', 'ZJ_DURATION_15_30_CNT', 'ZJ_DURATION_30_60_CNT', 'ZJ_DURATION_RATIO_0_15', 'ZJ_DURATION_RATIO_15_30', 'ZJ_DURATION_RATIO_30_60', 'H_MAX_CNT', 'H_MAX_CIRCLE', 'INNER_MONTH', 'MIX_CDSC_FLG', 'CPRD_NAME', 'AMT', 'CUST_ASSET_CNT', 'CUST_TELE_CNT', 'CUST_C_CNT', 'ALL_LL_USE', 'MY_LL_USE', 'MY_LL_ZB', 'ALL_LL_DUR', 'MY_LL_DUR ',' MY_DUR_ZB ',' EDAD ',' GENDER ',' CUST_TYPE_GRADE_NAME ',' ISP ',' TERM_PRICE ',' SALES_CHANNEL_LVL2_NAME ',' CORP_USER_NAME ',' TOTOL_7_CNT ', ' TJOL_DUR_7, 'TZ_7_CNT', 'TJOL_DUR_7,' TZ_7 , 'TOTOL_7_ZJ_D_CNT', 'TOTOL_7_BJ_D_DUR', 'TOTOL_7_JZGS_CNT', 'WEEK_CNT', 'WEEK_DUR', 'ZB_WS', 'COUPLE_NUMBER', 'TIME_COUPLE_NUMBER', 'ZJ_0912', 'HB_0912', 'ZJ_1417', 'HBG_1417', 'ZJ_1417', 'HBG_1417', 'CH ',' IS_HARASS '] 12345678
Vista de datos
Fila / columna de la tabla de datos
data_pos.shape 1
Se puede ver que los datos de la muestra positiva son solo 3436, y hay muchas muestras negativas, que son datos de muestra extremadamente desequilibrados.
Preprocesamiento de datos
Eliminación de campos sin sentido
data_pos_1 = data_pos.drop ([ 'BIL_ACCS_NBR', 'ASSET_ROW_ID', 'CCUST_ROW_ID', 'LATN_ID', 'CPRD_NAME', 'ISP', 'edad', 'CUST_TYPE_GRADE_NAME', 'ETL_DT', 'WEEK_DIS_BJ_NUM', 'TOTOL_7_ZJ_D_CNT' , 'TOTOL_7_JZGS_CNT', 'INNER_MONTH' ], eje = 1) 123456789101112131415
Tamaño de muestra positivo y negativo
data_pos.IS_HARASS.value_counts () 1
TERM_PRICE para el procesamiento de agrupaciones
data_pos_1 ['TERM_PRICE'] = data_pos_1 ['TERM_PRICE']. apply (lambda x: np.where (x> 5000, '> 5000', np.where (x> 3000, '(3000,5000]', np. donde (x> 2000, '(2000,3000]', np . donde (x> 1000, '(1000,2000])', np . where (x> 0, '(0,1000]', '未 识别' )))))) 123456
Relleno y conversión de campos
Reemplazar valores nulos de variables categóricas y categorías de muy pequeña escala
data_pos_1.TERM_PRICE.value_counts () data_pos_1.MIX_CDSC_FLG.value_counts () data_pos_1.CORP_USER_NAME.value_counts () data_pos_1.SALES_CHANNEL_LVL2_NAME.value_counts () 1234
#Proceso TERM_PRICE, MIX_CDSC_FLG, CORP_USER_NAME, SALES_CHANNEL_LVL2_NAME def CHANGE_SALES_CHANNEL_LVL2_NAME (datos): si hay datos en ['canales sociales', 'canales físicos', 'canales electrónicos', 'canales de venta directa']: devolver datos else: return'unidentified ' data_pos_1 ['SALES_CHANNEL_LVL2_NAME'] = data_pos_1.SALES_CHANNEL_LVL2_NAME.apply (CHANGE_SALES_CHANNEL_LVL2_NAME) 12345678
Procesamiento de valor faltante
## 缺失 值 统计 def na_count (datos): data_count = data.count () na_count = len (datos) - data_count na_rate = na_count / len (datos) na_result = pd.concat ([data_count, na_count, na_rate], eje = 1) devuelve na_result na_count = na_count (data_pos_1) na_count 12345678910
Campo dividido Los
campos se dividen según la categoría continua y
def category_continuous_resolution (datos, variable_category): para clave en la lista (data.columns): si clave no en variable_category: variable_continuous.append (key) else: continue return variable_continuous # 字段 按照 类型 拆分 variable_category = ['MIX_CDSC_FLG', 'GENDER ',' TERM_PRICE ',' SALES_CHANNEL_LVL2_NAME ',' CORP_USER_NAME '] variable_continuous = [] variable_continuous = category_continuous_resolution (data_pos_1, variable_category) 1234567891011121314
Conversión de tipo de campo
def feture_type_change (datos, variable_category): '' ' 字段 类型 转化 ' '' para col_key en la lista (data.columns): if col_key en variable_category: datos [col_key] = datos [col_key] .astype (eval ('objeto') , copy = False) else: data [col_key] = data [col_key] .astype (eval ('float'), copy = False) return data data_pos_2 = feture_type_change (data_pos_1, variable_category) 123456789101112
Relleno de valor faltante
def na_fill (data, col_name_1, col_name_2): '' ' 缺失 值 填充 ' '' para col_key en la lista (data.columns): if col_key en col_name_1: data [col_key] = data [col_key] .fillna (value = '未识别 ') elif col_key en col_name_2: data [col_key] = data [col_key] .fillna (data [col_key] .mean ()) else: data [col_key] = data [col_key] .fillna (valor = 0) return data #缺失 值 填充 col_name_1 = variable_category col_name_2 = [] data_pos_3 = na_fill (data_pos_2, col_name_1, col_name_2) 1234567891011121314151617
Procesamiento de variables categóricas one_hot
## one_hot def data_deliver (data, variable_category): '' ' ont_hot 衍生 ' '' para col_key en la lista (data.columns): si col_key en variable_category: temp_one_hot_code = pd.get_dummies (data [col_key], prefix = col_key) data = pd.concat ([data, temp_one_hot_code], axis = 1) del data [col_key] else: continuar devuelve data_pos_4 = data_deliver (data_pos_3, variable_category) 123456789101112131415
Ingeniería de características
Análisis de correlación
def max_corr_feture_droped (train_data, variable_continuous, k): '' ' 相关 性 分析 ' '' table_col = train_data.columns table_col_list = table_col.values.tolist () all_lines = len (train_data) train_data_number = train_data] # [ #_continuo变量 的 处理 过程 : 数据 的 标准化 de numpy import array de sklearn import preprocesamiento def normalization (data, method, feature_range = (0,1)): if method == 'MaxMin': train_data_scale = data.apply (lambda x: ( x - np.min (x)) / (np.max (x) - np.min (x))) return train_data_scale if method == 'z_score': train_data_scale = data.apply (lambda x: (x-np.mean (x)) / (np.std (x))) return train_data_scale train_data_scale = normalization (train_data_number, method = scale_method) # Genere la correlación entre cada variable Report def data_corr_analysis (raw_data, sigmod = k): corr_data = raw_data.corr () for i in range (len (corr_data)): for j in range (len (corr_data)): if j == i: corr_data.iloc [i, j] = 0 x, y, corr_xishu = [], [], [] for i in list (corr_data.index): for j in list (corr_data.columns): if abs (corr_data.loc [i, j]) > sigmod: # Conserva los atributos cuyo valor absoluto del coeficiente de correlación es mayor que el umbral x.append (i) y.append ( j) corr_xishu.append (corr_data.loc [i, j]) z = [[x [i], y [i], corr_xishu [i]] para i en el rango (len (x))] high_corr = pd.DataFrame (z , columnas = [ 'VAR1', 'VAR2', 'CORR_XISHU']) volver high_corr high_corr_data = data_corr_analysis (train_data_number, SIGMOD = k) def data_corr_choice (datos, train_data_scale, high_corr_data): high_corr_data_1 = [] target_var = pd.DataFrame (datos .loc [:, target_col]) para i en rango (high_corr_data.shape [0]): para j en rango (high_corr_data.shape [1] -1): d1 = pd.DataFrame (train_data_scale. loc [:, high_corr_data.iloc [i, j]]) data1 = pd.concat ([d1, data.loc [: , target_col]], axis = 1, join = 'inner') corr_data = data1.corr () high_corr_data_1.append (corr_data.iloc [0, -1]) # 输出 的 为 各个 变量 与目标 变量 之间 的 相关 关系 high_corr_data_2 = np.array (high_corr_data_1) .reshape (high_corr_data.shape [0], high_corr_data.shape [1] -1) high_corr_data_2 = pd.DataFrame = high_corr_data_data: columnas 1]) del_var_cor = [] for i in range (high_corr_data_2.shape [0]): if abs (high_corr_data_2.iloc [i, 0])> = abs (high_corr_data_2.iloc [i, 1]): del_var_cor.append ( high_corr_data.iloc [i, 1]) else: del_var_cor.append (high_corr_data.iloc [i, 0]) train_data_number_2.drop (del_var_cor, axis = 1, inplace = True) # estará fuertemente correlacionado Las variables se eliminan directamente return set (high_corr_data_1), set (del_var_cor), train_data_number_2 train_data_number_2 = pd.concat ([train_data [variable_continuous], train_data [col_fuente]], eje = 1) high_corr_data_1, del_var_cor, train_data_scale = data_corr_choice (train_data_number_2, train_data_scale, high_corr_data) train_data2 = train_data [:] train_data2.drop (conjunto (del_var_cor) , eje = 1, inplace = True) train_data2 retorno, del_var_cor #相关性分析,去除高相关变量 scale_method = 'MaxMin' col_fuente = 'IS_HARASS' data_pos_5, del_var_cor = max_corr_feture_droped (data_pos_4, variable_continuous, k = 0,8) del_var_cor #Delete variable view 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
Análisis de importancia de características
def data_sample (data, target_col, smp): '' ' 数据 平衡 ' '' data_1 = data [data [target_col] == 1] .sample (frac = 1) data_0 = data [data [target_col] == 0]. sample (n = len (data_1) * smp) # data_1 = data_1.sample (len (data_2) * smp) data = pd.concat ([data_1, data_0]). reset_index () devuelve datos 123456789
def train_test_spl (data): '' ' Datos de entrenamiento, segmentación de datos de prueba ' '' X_train, X_test, y_train, y_test = train_test_split ( data [ipt_col], data [target_col], test_size = 0.2, random_state = 42) return X_train, X_test , y_train, y_test 1234567
Defina la función de análisis de importancia de la característica y recorrala para obtener la mejor proporción de muestreo
def feture_extracted (train_data, alpha): '' ' 维度 重要性 判断 ' '' global ipt_col ipt_col = list (train_data.columns) ipt_col.remove (target_col) sample_present = [1,5] # 定义 抽样 比例 f1_score_list = [] model_dict = {} para i en sample_present: intente: train_data = data_sample (train_data, target_col, smp = i) excepto ValueError: break X_train, X_test, y_train, y_test = train_test_spl (train_data) # 开始 RF 选取 特征 modelo de = RandomForestClassifier () modelo = model.fit (X_train, y_train) model_pred = model.predict (X_test ) f1_score = metrics.f1_score (y_test, model_pred) f1_score_list.append (f1_score) model_dict [i] = model max_f1_index = f1_score_list.index (max (f1_score_list)) print ('最优 的 抽样 比例 是 : 1:', sample_present [max_f1_index]) d = dict (zip (zip) [float ('%. 3f'% i) para i en model_dict [sample_present [max_f1_index]]. feature_importances_])) f = zip (d.values (), d.keys ()) important_df = pd.DataFrame (sorted ( f, reverso = Verdadero), columnas = ['importancia', 'nombre_feture']) lista_imp = np.cumsum (importancia_df ['importancia']). tolist () para i, j en enumerate (list_imp): si j> = alpha: break print ('大于 alpha 的 特征 及 重要性 如下 : \ n', important_df.Iloc [0: i + 1,:]) print ('其 特征 如下 :') feture_selected = important_df.iloc [0: i + 1, 1] .tolist () print (feture_selected) return feture_selected #importance test, seleccione variables importantes data_pos_5_feture = feture_extracted (data_pos_5, alpha = 0.9) 12345678910111213141516171819202122232425262728293031323334353637383940
Entrenamiento de modelos
Balance de datos
data_pos_6 = data_sample (data_pos_5, target_col, smp = 3) 1
División de muestra positiva y negativa
def model_select (data, rf_feture, target_col, test_size): '' ' 正负 样本 拆分 ' '' X_train, X_test, y_train, y_test = train_test_split ( data [rf_feture], data [target_col], test_size = test_size, random_state = 42 ) return X_train, X_test, y_train, y_test # 拆分 比例 7: 3 X_train, X_test, y_train, y_test = model_select (data_pos_6, data_pos_5_feture, target_col, test_size = 0.3) 12345678910
Definir la función del modelo
Dos parámetros principales de RF:
- min_samples_split: al dividir un nodo interno, se requiere el número mínimo de muestras en el nodo, el valor predeterminado es 2;
- min_samples_leaf: establece el número mínimo de muestras en el nodo hoja, el valor predeterminado es 1. Cuando se intenta dividir un nodo, solo cuando el número de muestras en las ramas izquierda y derecha después de la división no es menor que el valor especificado por este parámetro, se considera que el nodo está dividido. En otras palabras, cuando el número de muestras en el nodo hoja es menor que el valor Cuando el parámetro especifica el valor, el nodo hoja y sus nodos hermanos se podarán. Cuando la cantidad de datos de muestra es grande, puede considerar aumentar este valor para detener el crecimiento del árbol antes de tiempo.
def model_train (x_train, y_train, model): '' ' 算法 模型 , 默认 为 RF ' '' if model == 'RF': res_model = RandomForestClassifier (min_samples_split = 50, min_samples_leaf = 50) res_model = res_model.fit (x_train, y_train) feature_importances = res_model.feature_importances_ [1] if model == 'LR': res_model = LogisticRegression () res_model = res_model.fit (x_train, y_train) list_feature_importances = [x para x en res_model.coef_index = list (x_train.columns) feature_importances = pd.DataFrame (list_feature_importances, list_index) else: pass return res_model, feature_importances #training model rf_model, feature_importances = model_train (X_train, y_train, model = 'RF') #También puede optar por utilizar LR 123456789101112131415161718192021
Modelo de validación
def model_predict (res_model, input_data, alpha): # model prediction # input_data: ingresa nuevos datos sin variables de destino data_proba = pd.DataFrame (res_model.predict_proba (input_data) .round (4)) data_proba.columns = ['neg', ' pos '] data_proba [' res '] = data_proba [' pos ']. apply (lambda x: np.where (x> = alpha, 1, 0)) #Ajustar la salida de> 0.5 como positivo a 1 return data_proba def model_evaluate (y_true, y_pred): y_true = np.array (y_true) y_true.shape = (len (y_true),) y_pred = np.array (y_pred) y_pred.shape = (len (y_pred),) print (métricas. clasificación de clasificación) (y_true, y_pred)) 1234567891011121314
data_pos_6 = data_sample (data_pos_5, target_col, smp = 50) X_train, X_test, y_train, y_test = model_select (data_pos_6, data_pos_5_feture, target_col, test_size = 0.5) Precisión = [] Recall = [] para alfa en np.arange (0, 1 , 0.1): y_pred_rf = model_predict (rf_model, X_test, alpha = alpha) cnf_matrix = confusion_matrix (y_test, y_pred_rf ['res']) Precision.append ((cnf_matrix [1,1] / (cnf_matrix [0,1] + cnf_matrix [0,1] + cnf_matrix [1,1])). Round (4)) Recall.append ((cnf_matrix [1,1] / (cnf_matrix [1,0] + cnf_matrix [1,1])). Round (4)) puntuación = pd .DataFrame (np.arange (0, 1, 0.1), columnas = ['score']) Precisión = pd.DataFrame (Precisión, columnas = ['Precision']) Recall = pd.DataFrame (Recall, columnas = [' Recordar']) Precision_Recall_F1 = pd.concat ([score, Precision, Recall], eje = 1) Precision_Recall_F1 [ 'F1'] = (2 * Precision_Recall_F1 [ 'Precision'] * Precision_Recall_F1 [ 'Recall'] / (Precision_Recall_F1 [ 'Precision'] + Precision_Recall_F1 [ 'Recall'])). Ronda (2) Precision_Recall_F1 12345678910111213141516171819
Conservación del paquete modelo
start = datetime.now () joblib.dump (rf_model, 'model.dmp', compress = 3) print ("El tiempo necesario para guardar el modelo:% s segundos"% (datetime.now () - inicio) .seconds ) 123
El caso anterior es relativamente simple, no implica mucha limpieza y preprocesamiento de datos, incluido el algoritmo de RF, solo define dos parámetros, y no existe un proceso de optimización de parámetros, los interesados pueden profundizar sobre esta base.
Recientemente, muchos amigos consultaron sobre problemas de aprendizaje de Python a través de mensajes privados. Para facilitar la comunicación, haga clic en el azul para unirse a la base de recursos de discusión y respuesta usted mismo