Setting the file. One moment. Func · Datanalysis Credit Risk · github/awesome-copilot · Skills DocsReference
references/func.py
Python·228 lines·9 KB
11
from
openpyxl.styles
import
Font, PatternFill, Alignment
12 HAS_OPENPYXL = True
13except:
14 HAS_OPENPYXL = False
15
16
17def get_dataset(data_pth: str, date_colName: str, y_colName: str,
18 org_colName: str, data_encode: str, key_colNames: List[str],
19 drop_colNames: List[str] = None,
20 miss_vals: List[int] = None) -> pd.DataFrame:
21 """Load and format data
22
23 Args:
24 data_pth: Data file path
25 date_colName: Date column name
26 y_colName: Label column name
27 org_colName: Organization column name
28 data_encode: Data encoding
29 key_colNames: Primary key columns (for deduplication)
30 drop_colNames: Columns to drop
31 miss_vals: List of abnormal values to replace with NaN, default [-1, -999, -1111]
32 """
33 if drop_colNames is None:
34 drop_colNames = []
35 if miss_vals is None:
36 miss_vals = [-1, -999, -1111]
37
38 # Multi-format reading
39 for fmt, reader in [('parquet', pd.read_parquet), ('csv', pd.read_csv),
40 ('xlsx', pd.read_excel), ('pkl', pd.read_pickle)]:
41 try:
42 data = reader(data_pth)
43 break
44 except:
45 continue
46
47 # Replace abnormal values with NaN
48 data.replace({v: np.nan for v in miss_vals}, inplace=True)
49
50 # Deduplication and filtering
51 data = data[data[y_colName].isin([0, 1])]
52 data = data.drop_duplicates(subset=key_colNames)
53
54 # Drop invalid columns
55 data.drop(columns=[c for c in drop_colNames if c in data.columns], errors='ignore')
56 data.drop(columns=[c for c in data.columns if data[c].nunique() <= 1], errors='ignore')
57
58 # Rename columns
59 data.rename(columns={date_colName: 'new_date', y_colName: 'new_target',
60 org_colName: 'new_org'}, inplace=True)
61 data['new_date'] = data['new_date'].astype(str).str.replace('-', '', regex=False).str[:8]
62 data['new_date_ym'] = data['new_date'].str[:6]
63
64 return data
65
66
67def org_analysis(data: pd.DataFrame, oos_orgs: List[str] = None) -> pd.DataFrame:
68 """Organization sample statistics analysis
69
70 Args:
71 data: Data
72 oos_orgs: Out-of-sample organization list, used to identify OOS samples
73 """
74 stat = data.groupby(['new_org', 'new_date_ym']).agg(
75 单月坏样本数=('new_target', 'sum'),
76 单月总样本数=('new_target', 'count'),
77 单月坏样率=('new_target', 'mean')
78 ).reset_index()
79
80 # Cumulative statistics
81 stat['总坏样本数'] = stat.groupby('new_org')['单月坏样本数'].transform('sum')
82 stat['总样本数'] = stat.groupby('new_org')['单月总样本数'].transform('sum')
83 stat['总坏样率'] = stat['总坏样本数'] / stat['总样本数']
84
85 # Mark whether it is an OOS organization
86 if oos_orgs and len(oos_orgs) > 0:
87 stat['样本类型'] = stat['new_org'].apply(lambda x: '贷外' if x in oos_orgs else '建模')
88 else:
89 stat['样本类型'] = '建模'
90
91 stat = stat.rename(columns={'new_org': '机构', 'new_date_ym': '年月'})
92
93 # Sort by sample type (modeling first, OOS last)
94 stat = stat.sort_values(['样本类型', '机构', '年月'], ascending=[True, True, True])
95 stat = stat.reset_index(drop=True)
96
97 return stat[['机构', '年月', '单月坏样本数', '单月总样本数', '单月坏样率', '总坏样本数', '总样本数', '总坏样率', '样本类型']]
98
99
100def missing_check(data: pd.DataFrame, channel: Dict[str, List[str]] = None) -> Tuple[pd.DataFrame, pd.DataFrame]:
101 """Calculate missing rate - including overall and organization-level missing rates
102
103 Returns:
104 miss_detail: Missing rate details (format: variable, overall, org1, org2, ..., orgn)
105 miss_ch: Overall missing rate (overall missing rate for each variable)
106 """
107 miss_vals = [-1, -999, -1111]
108 miss_ch = []
109
110 # Exclude non-variable columns: record_id, target, org_info, etc.
111 exclude_cols = ['new_date', 'new_date_ym', 'new_target', 'new_org', 'record_id', 'target', 'org_info']
112 cols = [c for c in data.columns if c not in exclude_cols]
113
114 # Calculate overall missing rate
115 for col in tqdm.tqdm(cols, desc="Missing rate"):
116 rate = ((data[col].isin(miss_vals)) | (data[col].isna())).mean()
117 miss_ch.append({'变量': col, '整体缺失率': round(rate, 4)})
118
119 miss_ch = pd.DataFrame(miss_ch)
120
121 # Calculate organization-level missing rates and convert to wide format
122 orgs = sorted(data['new_org'].unique())
123 miss_detail_dict = {'变量': []}
124 miss_detail_dict['整体'] = []
125
126 for org in orgs:
127 miss_detail_dict[org] = []
128
129 for col in cols:
130 miss_detail_dict['变量'].append(col)
131 # Overall missing rate
132 overall_rate = ((data[col].isin(miss_vals)) | (data[col].isna())).mean()
133 miss_detail_dict['整体'].append(round(overall_rate, 4))
134
135 # Missing rate for each organization
136 for org in orgs:
137 org_data = data[data['new_org'] == org]
138 rate = ((org_data[col].isin(miss_vals)) | (org_data[col].isna())).mean()
139 miss_detail_dict[org].append(round(rate, 4))
140
141 miss_detail = pd.DataFrame(miss_detail_dict)
142 # Sort by overall missing rate in descending order
143 miss_detail = miss_detail.sort_values('整体', ascending=False)
144 miss_detail = miss_detail.reset_index(drop=True)
145
146 return miss_detail, miss_ch
147
148
149def calculate_iv(data: pd.DataFrame, features: List[str], n_jobs: int = 4) -> pd.DataFrame:
150 """Calculate IV value - use toad.transform.Combiner for binning, set number of bins to 5, keep NaN values"""
151 import tqdm
152 from joblib import Parallel, delayed
153
154 def _calc_iv(f):
155 try:
156 # Use toad.transform.Combiner for binning, set number of bins to 5
157 c = toad.transform.Combiner()
158 data_temp = data[[f, 'new_target']].copy()
159 data_temp.columns = ['x', 'y']
160 data_temp['x_bin'] = c.fit_transform(X=data_temp['x'], y=data_temp['y'], method='dt', n_bins=5, min_samples=0.05/5, empty_separate=True)
161
162 # Calculate IV value using binned data
163 iv_df = toad.quality(data_temp[['x_bin', 'y']], 'y', iv_only=True)
164 if 'iv' in iv_df.columns and len(iv_df) > 0:
165 iv_value = iv_df['iv'].iloc[0]
166 if not np.isnan(iv_value):
167 return {'变量': f, 'IV': round(iv_value, 4)}
168 return None
169 except Exception as e:
170 print(f" IV calculation error: variable={f}, error={e}")
171 return None
172
173 # Use tqdm to show progress
174 results = Parallel(n_jobs=n_jobs, verbose=0)(
175 delayed(_calc_iv)(f) for f in features
176 )
177 iv_list = [r for r in results if r is not None]
178
179 if len(iv_list) == 0:
180 print(f" IV calculation result is empty, number of features={len(features)}")
181 return pd.DataFrame(columns=['变量', 'IV'])
182
183 return pd.DataFrame(iv_list).sort_values('IV', ascending=False)
184
185
186def calculate_corr(data: pd.DataFrame, features: List[str]) -> pd.DataFrame:
187 """Calculate correlation matrix"""
188 corr = data[features].corr().abs()
189 return corr
190
191
192def export_report_xlsx(filepath: str, data_name: str, data: pd.DataFrame,
193 sheet_name: str, description: str = ""):
194 """Export xlsx report - supports appending"""
195 try:
196 from openpyxl import load_workbook
197 wb = load_workbook(filepath)
198 ws = wb.create_sheet(sheet_name)
199 except:
200 wb = Workbook()
201 ws = wb.active
202 ws.title = sheet_name
203
204 # Write description
205 ws['A1'] = f"Data: {data_name}"
206 ws['A2'] = f"Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
207 if description:
208 ws['A3'] = f"Description: {description}"
209
210 # Write data
211 start_row = 5
212 for i, col in enumerate(data.columns):
213 ws.cell(start_row, i+1, col)
214
215 for i, row in enumerate(data.values):
216 for j, val in enumerate(row):
217 ws.cell(start_row+1+i, j+1, val)
218
219 # Styles
220 header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid")
221 header_font = Font(color="FFFFFF", bold=True)
222 for cell in ws[start_row]:
223 cell.fill = header_fill
224 cell.font = header_font
225 cell.alignment = Alignment(horizontal='center')
226
227 wb.save(filepath)
228 print(f"[{sheet_name}] Saved to {filepath}")