Setting the file. One moment. Example · Datanalysis Credit Risk · github/awesome-copilot · Skills Docsscripts/example.py
Python·391 lines·17 KB
13
14# =============================================================================
15# System Configuration
16# =============================================================================
17CPU_COUNT = multiprocessing.cpu_count()
18N_JOBS = max(1, CPU_COUNT - 1) # Multi-process parallel count, keep 1 core for system
19
20def _ensure_references_on_path():
21 script_dir = os.path.dirname(__file__)
22 cur = script_dir
23 for _ in range(8):
24 candidate = os.path.join(cur, 'references')
25 if os.path.isdir(candidate):
26 # add parent folder (which contains `references`) to sys.path
27 sys.path.insert(0, cur)
28 return
29 parent = os.path.dirname(cur)
30 if parent == cur:
31 break
32 cur = parent
33 # fallback: add a reasonable repo-root guess
34 sys.path.insert(0, os.path.abspath(os.path.join(script_dir, '..', '..', '..')))
35
36
37_ensure_references_on_path()
38
39from references.func import get_dataset, missing_check, org_analysis
40from references.analysis import (drop_abnormal_ym, drop_highmiss_features,
41 drop_lowiv_features, drop_highcorr_features,
42 drop_highpsi_features,
43 drop_highnoise_features,
44 export_cleaning_report,
45 iv_distribution_by_org,
46 psi_distribution_by_org,
47 value_ratio_distribution_by_org)
48
49# ==================== Path Configuration (Interactive Input) ====================
50# Use 50-column test data as default, support interactive modification in command line
51default_data_path = ''
52default_output_dir = ''
53
54def _get_path_input(prompt, default):
55 try:
56 user_val = input(f"{prompt} (default: {default}): ").strip()
57 except Exception:
58 user_val = ''
59 return user_val if user_val else default
60
61DATA_PATH = _get_path_input('Please enter data file path DATA_PATH', default_data_path)
62OUTPUT_DIR = _get_path_input('Please enter output directory OUTPUT_DIR', default_output_dir)
63REPORT_PATH = os.path.join(OUTPUT_DIR, '数据清洗报告.xlsx')
64
65# Data column name configuration (adjust according to actual data)
66DATE_COL = _get_path_input('Please enter date column name in data', 'apply_date')
67Y_COL = _get_path_input('Please enter label column name in data', 'target')
68ORG_COL = _get_path_input('Please enter organization column name in data', 'org_info')
69
70# Support multiple primary key column names input (comma or space separated)
71def _get_list_input(prompt, default):
72 try:
73 user_val = input(f"{prompt} (default: {default}): ").strip()
74 except Exception:
75 user_val = ''
76 if not user_val:
77 user_val = default
78 # Support comma or space separation
79 parts = [p.strip() for p in user_val.replace(',', ' ').split() if p.strip()]
80 return parts
81
82KEY_COLS = _get_list_input('Please enter primary key column names in data (multiple columns separated by comma or space)', 'record_id')
83
84# ==================== Multi-process Configuration Information ====================
85print("=" * 60)
86print("Multi-process Configuration")
87print("=" * 60)
88print(f" Local CPU cores: {CPU_COUNT}")
89print(f" Current process count: {N_JOBS}")
90print("=" * 60)
91
92# ==================== OOS Organization Configuration (Interactive Input) ====================
93# Default out-of-sample organization list, users can input custom list in comma-separated format during interaction
94default_oos = [
95 'orgA', 'orgB', 'orgC', 'orgD', 'orgE',
96]
97
98try:
99 oos_input = input('Please enter out-of-sample organization list, comma separated (press Enter to use default list):').strip()
100except Exception:
101 oos_input = ''
102if oos_input:
103 OOS_ORGS = [s.strip() for s in oos_input.split(',') if s.strip()]
104else:
105 OOS_ORGS = default_oos
106
107os.makedirs(OUTPUT_DIR, exist_ok=True)
108
109# ==================== Interactive Hyperparameter Input ====================
110def get_user_input(prompt, default, dtype=float):
111 """Get user input, support default value and type conversion"""
112 while True:
113 try:
114 user_input = input(f"{prompt} (default: {default}): ").strip()
115 if not user_input:
116 return default
117 return dtype(user_input)
118 except ValueError:
119 print(f" Invalid input, please enter {dtype.__name__} type")
120
121# Record cleaning steps
122steps = []
123
124# Store parameters for each step
125params = {}
126
127# Timer decorator
128def timer(step_name):
129 """Timer decorator"""
130 def decorator(func):
131 def wrapper(*args, **kwargs):
132 print(f"\nStarting {step_name}...")
133 start_time = time.time()
134 result = func(*args, **kwargs)
135 elapsed = time.time() - start_time
136 print(f" {step_name} elapsed: {elapsed:.2f} seconds")
137 return result
138 return wrapper
139 return decorator
140
141# ==================== Step 1: Get Data ====================
142print("\n" + "=" * 60)
143print("Step 1: Get Data")
144print("=" * 60)
145step_start = time.time()
146# Use configuration from global_parameters
147data = get_dataset(
148 data_pth=DATA_PATH,
149 date_colName=DATE_COL,
150 y_colName=Y_COL,
151 org_colName=ORG_COL,
152 data_encode='utf-8',
153 key_colNames=KEY_COLS,
154 drop_colNames=[],
155 miss_vals=[-1, -999, -1111]
156)
157print(f" Original data: {data.shape}")
158print(f" Abnormal values replaced with NaN: [-1, -999, -1111]")
159print(f" Step 1 elapsed: {time.time() - step_start:.2f} seconds")
160
161# ==================== Step 2: Organization Sample Analysis ====================
162print("\n" + "=" * 60)
163print("Step 2: Organization Sample Analysis")
164print("=" * 60)
165step_start = time.time()
166org_stat = org_analysis(data, oos_orgs=OOS_ORGS)
167steps.append(('机构样本统计', org_stat))
168print(f" Organization count: {data['new_org'].nunique()}, Month count: {data['new_date_ym'].nunique()}")
169print(f" Out-of-sample organizations: {len(OOS_ORGS)}")
170print(f" Step 2 elapsed: {time.time() - step_start:.2f} seconds")
171
172# ==================== Step 3: Separate OOS Data ====================
173print("\n" + "=" * 60)
174print("Step 3: Separate OOS Data")
175print("=" * 60)
176step_start = time.time()
177oos_data = data[data['new_org'].isin(OOS_ORGS)]
178data = data[~data['new_org'].isin(OOS_ORGS)]
179print(f" OOS samples: {oos_data.shape[0]} rows")
180print(f" Modeling samples: {data.shape[0]} rows")
181print(f" OOS organizations: {OOS_ORGS}")
182print(f" Step 3 elapsed: {time.time() - step_start:.2f} seconds")
183# Create separation information DataFrame
184oos_info = pd.DataFrame({'变量': ['OOS样本', '建模样本'], '数量': [oos_data.shape[0], data.shape[0]]})
185steps.append(('分离OOS数据', oos_info))
186
187# ==================== Step 4: Filter Abnormal Months (Modeling Data Only) ====================
188print("\n" + "=" * 60)
189print("Step 4: Filter Abnormal Months (Modeling Data Only)")
190print("=" * 60)
191print(" Press Enter to use default values")
192print("=" * 60)
193params['min_ym_bad_sample'] = int(get_user_input("Bad sample count threshold", 10, int))
194params['min_ym_sample'] = int(get_user_input("Total sample count threshold", 500, int))
195step_start = time.time()
196data_filtered, abnormal_ym = drop_abnormal_ym(data.copy(), min_ym_bad_sample=params['min_ym_bad_sample'], min_ym_sample=params['min_ym_sample'])
197steps.append(('Step4-异常月份处理', abnormal_ym))
198print(f" After filtering: {data_filtered.shape}")
199print(f" Parameters: min_ym_bad_sample={params['min_ym_bad_sample']}, min_ym_sample={params['min_ym_sample']}")
200if len(abnormal_ym) > 0:
201 print(f" Dropped months: {abnormal_ym['年月'].tolist()}")
202 print(f" Removal conditions: {abnormal_ym['去除条件'].tolist()}")
203print(f" Step 4 elapsed: {time.time() - step_start:.2f} seconds")
204
205# ==================== Step 5: Calculate Missing Rate ====================
206print("\n" + "=" * 60)
207print("Step 5: Calculate Missing Rate")
208print("=" * 60)
209step_start = time.time()
210orgs = data['new_org'].unique().tolist()
211channel = {'整体': orgs}
212miss_detail, miss_channel = missing_check(data, channel=channel)
213# miss_detail: Missing rate details (format: feature, overall, org1, org2, ..., orgn)
214# miss_channel: Overall missing rate
215steps.append(('缺失率明细', miss_detail))
216print(f" Feature count: {len(miss_detail['变量'].unique())}")
217print(f" Organization count: {len(miss_detail.columns) - 2}") # Subtract '变量' and '整体' columns
218print(f" Step 5 elapsed: {time.time() - step_start:.2f} seconds")
219
220# ==================== Step 6: Drop High Missing Rate Features ====================
221print("\n" + "=" * 60)
222print("Step 6: Drop High Missing Rate Features")
223print("=" * 60)
224print(" Press Enter to use default values")
225print("=" * 60)
226params['missing_ratio'] = get_user_input("Missing rate threshold", 0.6)
227step_start = time.time()
228data_miss, dropped_miss = drop_highmiss_features(data.copy(), miss_channel, threshold=params['missing_ratio'])
229steps.append(('Step6-高缺失率处理', dropped_miss))
230print(f" Dropped: {len(dropped_miss)}")
231print(f" Threshold: {params['missing_ratio']}")
232if len(dropped_miss) > 0:
233 print(f" Dropped features: {dropped_miss['变量'].tolist()[:5]}...")
234 print(f" Removal conditions: {dropped_miss['去除条件'].tolist()[:5]}...")
235print(f" Step 6 elapsed: {time.time() - step_start:.2f} seconds")
236
237# ==================== Step 7: Drop Low IV Features ====================
238print("\n" + "=" * 60)
239print("Step 7: Drop Low IV Features")
240print("=" * 60)
241print(" Press Enter to use default values")
242print("=" * 60)
243params['overall_iv_threshold'] = get_user_input("Overall IV threshold", 0.1)
244params['org_iv_threshold'] = get_user_input("Single organization IV threshold", 0.1)
245params['max_org_threshold'] = int(get_user_input("Maximum tolerated low IV organization count", 2, int))
246step_start = time.time()
247# Get feature list (use all features)
248features = [c for c in data.columns if c.startswith('i_')]
249data_iv, iv_detail, iv_process = drop_lowiv_features(
250 data.copy(), features,
251 overall_iv_threshold=params['overall_iv_threshold'],
252 org_iv_threshold=params['org_iv_threshold'],
253 max_org_threshold=params['max_org_threshold'],
254 n_jobs=N_JOBS
255)
256# iv_detail: IV details (IV value of each feature in each organization and overall)
257# iv_process: IV processing table (features that do not meet the conditions)
258steps.append(('Step7-IV处理', iv_process))
259print(f" Dropped: {len(iv_process)}")
260print(f" Parameters: overall_iv_threshold={params['overall_iv_threshold']}, org_iv_threshold={params['org_iv_threshold']}, max_org_threshold={params['max_org_threshold']}")
261if len(iv_process) > 0:
262 print(f" Dropped features: {iv_process['变量'].tolist()[:5]}...")
263 print(f" Processing reasons: {iv_process['处理原因'].tolist()[:5]}...")
264print(f" Step 7 elapsed: {time.time() - step_start:.2f} seconds")
265
266# ==================== Step 8: Drop High PSI Features ====================
267print("\n" + "=" * 60)
268print("Step 8: Drop High PSI Features (By Organization + Month-by-Month)")
269print("=" * 60)
270print(" Press Enter to use default values")
271print("=" * 60)
272params['psi_threshold'] = get_user_input("PSI threshold", 0.1)
273params['max_months_ratio'] = get_user_input("Maximum unstable month ratio", 1/3)
274params['max_orgs'] = int(get_user_input("Maximum unstable organization count", 6, int))
275step_start = time.time()
276# Get features before PSI calculation (use all features)
277features_for_psi = [c for c in data.columns if c.startswith('i_')]
278data_psi, psi_detail, psi_process = drop_highpsi_features(
279 data.copy(), features_for_psi,
280 psi_threshold=params['psi_threshold'],
281 max_months_ratio=params['max_months_ratio'],
282 max_orgs=params['max_orgs'],
283 min_sample_per_month=100,
284 n_jobs=N_JOBS
285)
286# psi_detail: PSI details (PSI value of each feature in each organization each month)
287# psi_process: PSI processing table (features that do not meet the conditions)
288steps.append(('Step8-PSI处理', psi_process))
289print(f" Dropped: {len(psi_process)}")
290print(f" Parameters: psi_threshold={params['psi_threshold']}, max_months_ratio={params['max_months_ratio']:.2f}, max_orgs={params['max_orgs']}")
291if len(psi_process) > 0:
292 print(f" Dropped features: {psi_process['变量'].tolist()[:5]}...")
293 print(f" Processing reasons: {psi_process['处理原因'].tolist()[:5]}...")
294print(f" PSI details: {len(psi_detail)} records")
295print(f" Step 8 elapsed: {time.time() - step_start:.2f} seconds")
296
297# ==================== Step 9: Null Importance Denoising ====================
298print("\n" + "=" * 60)
299print("Step 9: Null Importance Remove High Noise Features")
300print("=" * 60)
301print(" Press Enter to use default values")
302print("=" * 60)
303params['n_estimators'] = int(get_user_input("Number of trees", 100, int))
304params['max_depth'] = int(get_user_input("Maximum tree depth", 5, int))
305params['gain_threshold'] = get_user_input("Gain difference threshold", 50)
306step_start = time.time()
307# Get feature list (use all features)
308features = [c for c in data.columns if c.startswith('i_')]
309data_noise, dropped_noise = drop_highnoise_features(data.copy(), features, n_estimators=params['n_estimators'], max_depth=params['max_depth'], gain_threshold=params['gain_threshold'])
310steps.append(('Step9-null importance处理', dropped_noise))
311print(f" Dropped: {len(dropped_noise)}")
312print(f" Parameters: n_estimators={params['n_estimators']}, max_depth={params['max_depth']}, gain_threshold={params['gain_threshold']}")
313if len(dropped_noise) > 0:
314 print(f" Dropped features: {dropped_noise['变量'].tolist()}")
315print(f" Step 9 elapsed: {time.time() - step_start:.2f} seconds")
316
317# ==================== Step 10: Drop High Correlation Features (Based on Null Importance Original Gain) ====================
318print("\n" + "=" * 60)
319print("Step 10: Drop High Correlation Features (Based on Null Importance Original Gain)")
320print("=" * 60)
321print(" Press Enter to use default values")
322print("=" * 60)
323params['max_corr'] = get_user_input("Correlation threshold", 0.9)
324params['top_n_keep'] = int(get_user_input("Keep top N features by original gain ranking", 20, int))
325step_start = time.time()
326# Get feature list (use all features)
327features = [c for c in data.columns if c.startswith('i_')]
328# Get original gain from null importance results
329if len(dropped_noise) > 0 and '原始gain' in dropped_noise.columns:
330 gain_dict = dict(zip(dropped_noise['变量'], dropped_noise['原始gain']))
331else:
332 gain_dict = {}
333data_corr, dropped_corr = drop_highcorr_features(data.copy(), features, threshold=params['max_corr'], gain_dict=gain_dict, top_n_keep=params['top_n_keep'])
334steps.append(('Step10-高相关性剔除', dropped_corr))
335print(f" Dropped: {len(dropped_corr)}")
336print(f" Threshold: {params['max_corr']}")
337if len(dropped_corr) > 0:
338 print(f" Dropped features: {dropped_corr['变量'].tolist()}")
339 print(f" Removal conditions: {dropped_corr['去除条件'].tolist()[:5]}...")
340print(f" Step 10 elapsed: {time.time() - step_start:.2f} seconds")
341
342# ==================== Step 11: Export Report ====================
343print("\n" + "=" * 60)
344print("Step 11: Export Report")
345print("=" * 60)
346step_start = time.time()
347
348# Calculate IV distribution statistics
349print(" Calculating IV distribution statistics...")
350iv_distribution = iv_distribution_by_org(iv_detail, oos_orgs=OOS_ORGS)
351print(f" IV distribution statistics: {len(iv_distribution)} records")
352
353# Calculate PSI distribution statistics
354print(" Calculating PSI distribution statistics...")
355psi_distribution = psi_distribution_by_org(psi_detail, oos_orgs=OOS_ORGS)
356print(f" PSI distribution statistics: {len(psi_distribution)} records")
357
358# Calculate value ratio distribution statistics (use all features)
359print(" Calculating value ratio distribution statistics...")
360features_for_value_ratio = [c for c in data.columns if c.startswith('i_')]
361value_ratio_distribution = value_ratio_distribution_by_org(data, features_for_value_ratio, oos_orgs=OOS_ORGS)
362print(f" Value ratio distribution statistics: {len(value_ratio_distribution)} records")
363
364# Add details and distribution statistics to steps list
365steps.append(('Step7-IV明细', iv_detail))
366steps.append(('Step7-IV分布统计', iv_distribution))
367steps.append(('Step8-PSI明细', psi_detail))
368steps.append(('Step8-PSI分布统计', psi_distribution))
369steps.append(('Step5-有值率分布统计', value_ratio_distribution))
370
371export_cleaning_report(REPORT_PATH, steps,
372 iv_detail=iv_detail,
373 iv_process=iv_process,
374 psi_detail=psi_detail,
375 psi_process=psi_process,
376 params=params,
377 iv_distribution=iv_distribution,
378 psi_distribution=psi_distribution,
379 value_ratio_distribution=value_ratio_distribution)
380print(f" Report: {REPORT_PATH}")
381print(f" Step 11 elapsed: {time.time() - step_start:.2f} seconds")
382
383# ==================== Summary ====================
384print("\n" + "=" * 60)
385print("Data Cleaning Completed!")
386print("=" * 60)
387print(f" Original data: {data.shape[0]} rows")
388print(f" Original features: {len([c for c in data.columns if c.startswith('i_')])}")
389print(f" Cleaning steps (each step executed independently, data not deleted):")
390for name, df in steps:
391 print(f" - {name}: Dropped {df.shape[0] if hasattr(df, 'shape') else len(df)}")