utils.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. # -*- coding:utf-8 -*-
  2. """
  3. @author: yq
  4. @time: 2023/12/28
  5. @desc: 特征工具类
  6. """
  7. import json
  8. import os
  9. from typing import Union
  10. import numpy as np
  11. import pandas as pd
  12. from statsmodels.stats.outliers_influence import variance_inflation_factor as vif
  13. from commom import GeneralException, f_is_number
  14. from enums import ResultCodesEnum, FileEnum
  15. FORMAT_DICT = {
  16. # 比例类 -1 - 1
  17. "bin_rate1": np.arange(-1, 1 + 0.1, 0.1).tolist(),
  18. # 次数类1 0 -10
  19. "bin_cnt1": np.arange(0.0, 11.0, 1.0).tolist(),
  20. # 次数类2 0 - 20
  21. "bin_cnt2": [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 12.0, 15.0, 17.0, 20.0],
  22. # 次数类3 0 - 50
  23. "bin_cnt3": [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 15.0, 20.0, 25.0, 30.0, 35.0, 40.0, 45.0,
  24. 50.0],
  25. # 次数类4 0 - 100
  26. "bin_cnt4": [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 15.0, 20.0, 30.0, 40.0, 50.0, 80.0, 100.0],
  27. # 金额类1 0 - 1w
  28. "bin_amt1": np.arange(0, 1.1e4, 1e3).tolist(),
  29. # 金额类2 0 - 5w
  30. "bin_amt2": np.arange(0, 5.5e4, 5e3).tolist(),
  31. # 金额类3 0 - 10w
  32. "bin_amt3": np.arange(0, 11e4, 1e4).tolist(),
  33. # 金额类4 0 - 20w
  34. "bin_amt4": [0.0, 1e4, 2e4, 3e4, 4e4, 5e4, 8e4, 10e4, 15e4, 20e4],
  35. # 金额类5 0 - 100w
  36. "bin_amt5": [0.0, 5e4, 10e4, 15e4, 20e4, 25e4, 30e4, 40e4, 50e4, 100e4],
  37. # 年龄类
  38. "bin_age": [20.0, 25.0, 30.0, 35.0, 40.0, 45.0, 50.0, 55.0, 60.0, 65.0],
  39. }
  40. # 粗分箱
  41. def f_format_bin(data_describe: pd.Series, raw_v):
  42. percent10 = data_describe["10%"]
  43. percent90 = data_describe["90%"]
  44. format_v = raw_v
  45. # 筛选最合适的标准化分箱节点
  46. bin = None
  47. for k, v_list in FORMAT_DICT.items():
  48. bin_min = min(v_list)
  49. bin_max = max(v_list)
  50. if percent10 >= bin_min and percent90 <= bin_max:
  51. if bin is None:
  52. bin = (k, bin_max)
  53. elif bin[1] > bin_max:
  54. bin = (k, bin_max)
  55. if bin is None:
  56. return format_v
  57. # 选择分箱内适合的切分点
  58. v_list = FORMAT_DICT[bin[0]]
  59. for idx in range(1, len(v_list)):
  60. v_left = v_list[idx - 1]
  61. v_right = v_list[idx]
  62. # 就近原则
  63. if v_left <= raw_v <= v_right:
  64. format_v = v_right if (raw_v - v_left) - (v_right - raw_v) > 0 else v_left
  65. if format_v not in v_list:
  66. if format_v > v_list[-1]:
  67. format_v = v_list[-1]
  68. if format_v < v_list[0]:
  69. format_v = v_list[0]
  70. return format_v
  71. # 单调性变化次数
  72. def f_monto_shift(badprobs: list) -> int:
  73. if len(badprobs) <= 2:
  74. return 0
  75. before = badprobs[1] - badprobs[0]
  76. change_cnt = 0
  77. for i in range(2, len(badprobs)):
  78. next = badprobs[i] - badprobs[i - 1]
  79. # 后一位bad_rate减前一位bad_rate,保证bad_rate的单调性
  80. if (next >= 0 and before >= 0) or (next <= 0 and before <= 0):
  81. # 满足趋势保持,查看下一位
  82. continue
  83. else:
  84. # 记录一次符号变化
  85. before = next
  86. change_cnt += 1
  87. return change_cnt
  88. # 变量趋势一致变化次数
  89. def f_trend_shift(train_badprobs: list, test_badprobs: list) -> int:
  90. if len(train_badprobs) != len(test_badprobs) or len(train_badprobs) < 2 or len(test_badprobs) < 2:
  91. return 0
  92. train_monto = np.array(train_badprobs[1:]) - np.array(train_badprobs[0:-1])
  93. train_monto = np.where(train_monto >= 0, 1, -1)
  94. test_monto = np.array(test_badprobs[1:]) - np.array(test_badprobs[0:-1])
  95. test_monto = np.where(test_monto >= 0, 1, -1)
  96. contrast = train_monto - test_monto
  97. return len(contrast[contrast != 0])
  98. def f_get_psi(train_bins, test_bins):
  99. train_bins['count'] = train_bins['good'] + train_bins['bad']
  100. train_bins['proportion'] = train_bins['count'] / train_bins['count'].sum()
  101. test_bins['count'] = test_bins['good'] + test_bins['bad']
  102. test_bins['proportion'] = test_bins['count'] / test_bins['count'].sum()
  103. psi = (train_bins['proportion'] - test_bins['proportion']) * np.log(
  104. train_bins['proportion'] / test_bins['proportion'])
  105. psi = psi.reset_index()
  106. psi = psi.rename(columns={"proportion": "psi"})
  107. return psi["psi"].sum().round(3)
  108. def f_get_corr(data: pd.DataFrame, meth: str = 'spearman') -> pd.DataFrame:
  109. return data.corr(method=meth)
  110. def f_get_vif(data: pd.DataFrame) -> Union[pd.DataFrame, None]:
  111. if len(data.columns.to_list()) <= 1:
  112. return None
  113. vif_v = [round(vif(data.values, data.columns.get_loc(i)), 3) for i in data.columns]
  114. df_vif = pd.DataFrame()
  115. df_vif["变量"] = [column.replace("_woe", "") for column in data.columns]
  116. df_vif['vif'] = vif_v
  117. return df_vif
  118. def f_woebin_load(path: str):
  119. if os.path.isdir(path):
  120. path = os.path.join(path, FileEnum.FEATURE.value)
  121. if not os.path.isfile(path) or FileEnum.FEATURE.value not in path:
  122. raise GeneralException(ResultCodesEnum.NOT_FOUND, message=f"特征信息【{FileEnum.FEATURE.value}】不存在")
  123. df_woebin = pd.read_csv(path)
  124. variables = df_woebin["variable"].unique().tolist()
  125. sc_woebin = {}
  126. for variable in variables:
  127. sc_woebin[variable] = df_woebin[df_woebin["variable"] == variable]
  128. print(f"feature load from【{path}】success.")
  129. return sc_woebin
  130. def f_get_var_mapping(df_bins, df_card, model_name="", model_desc="", columns_anns={}) -> pd.DataFrame:
  131. def _get_bin_opt(bin: str):
  132. is_num = 0
  133. bin = str(bin)
  134. rst = {
  135. "LEFT_OP": "",
  136. "LEFT_VALUE": "",
  137. "RIGHT_OP": "",
  138. "RIGHT_VALUE": "",
  139. }
  140. # 数值型
  141. if "," in bin and ("[" in bin or "]" in bin or "(" in bin or ")" in bin):
  142. is_num = 1
  143. left = bin.split(",")[0]
  144. if "-inf" not in left:
  145. rst["LEFT_VALUE"] = left[1:]
  146. rst["LEFT_OP"] = ">"
  147. if "[" in left:
  148. rst["LEFT_OP"] = ">="
  149. right = bin.split(",")[1]
  150. if "inf" not in right:
  151. rst["RIGHT_VALUE"] = right[:-1]
  152. rst["RIGHT_OP"] = "<"
  153. if "]" in right:
  154. rst["LEFT_OP"] = "<="
  155. else:
  156. # 字符型
  157. e = bin.split("%,%")
  158. if len(e) == 1:
  159. rst["LEFT_VALUE"] = e[0]
  160. if f_is_number(e[0]):
  161. is_num = 1
  162. else:
  163. rst["LEFT_VALUE"] = json.dumps(e, ensure_ascii=False)
  164. return rst, is_num
  165. rows = []
  166. binning_id_dict = {}
  167. for _, row_bin in df_bins.iterrows():
  168. variable = row_bin["variable"]
  169. binning_id = binning_id_dict.get(variable, 1)
  170. bin_opt, is_num = _get_bin_opt(row_bin["bin"])
  171. var_info = {
  172. "MODEL_NAME": model_name,
  173. "MODEL_DESC": model_desc,
  174. "VERSION": 1,
  175. "VAR_NAME": variable,
  176. "VAR_DESC": columns_anns.get(variable, ""),
  177. "BINNING_ID": binning_id,
  178. "IS_NUM": is_num,
  179. "VAR_WOE": df_card[(df_card["variable"] == variable) & (df_card["bin"] == row_bin["bin"])][
  180. 'points'].values[0],
  181. "VAR_WEIGHT": 1,
  182. "VAR_IV": round(row_bin["total_iv"], 3),
  183. "BINNING_PARTION": round(row_bin["count_distr"], 3),
  184. }
  185. var_info.update(bin_opt)
  186. rows.append(var_info)
  187. binning_id_dict[variable] = binning_id + 1
  188. rows.append({
  189. "MODEL_NAME": model_name,
  190. "MODEL_DESC": model_desc,
  191. "VERSION": 1,
  192. "VAR_NAME": "INTERCEPT",
  193. "VAR_DESC": "截距",
  194. "BINNING_ID": 0,
  195. "IS_NUM": 1,
  196. "LEFT_OP": "",
  197. "LEFT_VALUE": "",
  198. "RIGHT_OP": "",
  199. "RIGHT_VALUE": "",
  200. "VAR_WOE": "",
  201. "VAR_WEIGHT": 0,
  202. "VAR_IV": "",
  203. "BINNING_PARTION": "",
  204. })
  205. df_var_mapping = pd.DataFrame(
  206. columns=["MODEL_NAME", "MODEL_DESC", "VERSION", "VAR_NAME", "VAR_DESC", "BINNING_ID", "IS_NUM",
  207. "LEFT_OP", "LEFT_VALUE", "RIGHT_OP", "RIGHT_VALUE", "VAR_WOE", "VAR_WEIGHT", "VAR_IV",
  208. "BINNING_PARTION"],
  209. data=rows
  210. )
  211. return df_var_mapping