low_interest_rate_recognizer.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  1. from pydantic import BaseModel, Field
  2. from typing import Dict, Any, Optional, Type, List
  3. import pandas as pd
  4. import re
  5. from .enhanced_base_recognizer import EnhancedBaseRecognizer
  6. class LowInterestRateInput(BaseModel):
  7. """低利率结息记录识别工具输入"""
  8. csv_path: Optional[str] = Field(
  9. None,
  10. description="CSV文件路径(可选)。如果初始化时已提供csv_path,可以不用再次传入。"
  11. )
  12. class Config:
  13. arbitrary_types_allowed = True
  14. class LowInterestRateRecognizer(EnhancedBaseRecognizer):
  15. """
  16. 低利率结息记录识别器
  17. 异常规则定义:
  18. 银行流水结息核查中,若实际结息金额对应的有效利率显著低于同档期银行公布的
  19. 活期存款基准利率,或低于账户所属银行同期执行的活期存款利率标准,
  20. 且无合理利率下浮依据,可判定为结息记录存在异常,需进一步核查。
  21. 核心逻辑:
  22. 1. 识别结息记录(txSummary包含关键词,txDirection为收入)
  23. 2. 估算年化利率:结息金额 ÷ 结息时点余额 × 年化系数
  24. 3. 对比配置的基准利率和阈值
  25. 4. 标记利率异常低的结息记录
  26. """
  27. args_schema: Type[BaseModel] = LowInterestRateInput
  28. # 配置参数
  29. base_interest_rate: float = Field(
  30. 0.0035,
  31. description="基准活期存款利率(如0.35%应输入为0.0035)"
  32. )
  33. threshold_ratio: float = Field(
  34. 0.5,
  35. description="异常阈值比例,实际利率低于基准利率的比例时视为异常"
  36. )
  37. # 结息识别关键词
  38. interest_keywords: List[str] = Field(
  39. ['结息', '利息', '存款利息'],
  40. description="识别结息记录的关键词列表"
  41. )
  42. # 计息参数
  43. assumed_interest_days: int = Field(
  44. 90,
  45. description="假设计息天数(通常为90天,按季结息)"
  46. )
  47. annual_days: int = Field(
  48. 360,
  49. description="年计息天数(银行常用360天)"
  50. )
  51. # 计算所需的最小余额
  52. min_balance_for_calc: float = Field(
  53. 100.0,
  54. description="计算利率所需的最小余额(元),余额低于此值可能无法准确计算"
  55. )
  56. # 严重程度配置
  57. severity_level: str = Field(
  58. 'medium',
  59. description="异常严重程度(high/medium/low)"
  60. )
  61. # 模糊匹配设置
  62. enable_fuzzy_match: bool = Field(
  63. True,
  64. description="是否启用模糊匹配(处理不规范的txSummary字段)"
  65. )
  66. def __init__(self, csv_path: str = None, config: Dict[str, Any] = None, **kwargs):
  67. """
  68. 初始化低利率结息记录识别器
  69. Args:
  70. csv_path: CSV文件路径
  71. config: 配置参数
  72. **kwargs: 其他参数
  73. """
  74. # 调用父类的 __init__
  75. super().__init__(
  76. name="low_interest_rate_recognizer",
  77. description="识别银行流水中利率异常低的结息记录,检测结息金额对应的有效利率是否显著低于基准利率。",
  78. display_name="低利率结息记录识别器",
  79. csv_path=csv_path,
  80. config=config,
  81. **kwargs
  82. )
  83. # 从config获取配置,更新Field属性
  84. interest_config = self.get_config_value('interest_rate_check', {})
  85. if interest_config:
  86. config_mapping = {
  87. 'base_interest_rate': 'base_interest_rate',
  88. 'threshold_ratio': 'threshold_ratio',
  89. 'interest_keywords': 'interest_keywords',
  90. 'assumed_interest_days': 'assumed_interest_days',
  91. 'annual_days': 'annual_days',
  92. 'min_balance_for_calc': 'min_balance_for_calc',
  93. 'severity_level': 'severity_level',
  94. 'enable_fuzzy_match': 'enable_fuzzy_match'
  95. }
  96. for config_key, attr_name in config_mapping.items():
  97. if config_key in interest_config:
  98. setattr(self, attr_name, interest_config[config_key])
  99. print(f"✅ {self.display_name} 初始化完成")
  100. print(f" 基准利率: {self.base_interest_rate:.4%}")
  101. print(f" 异常阈值: 低于基准的{self.threshold_ratio:.0%}")
  102. print(f" 识别关键词: {', '.join(self.interest_keywords)}")
  103. print(f" 假设计息天数: {self.assumed_interest_days}天")
  104. print(f" 年计息天数: {self.annual_days}天")
  105. print(f" 最小计算余额: ¥{self.min_balance_for_calc:,.2f}")
  106. print(f" 模糊匹配: {'启用' if self.enable_fuzzy_match else '禁用'}")
  107. def _is_interest_record(self, summary: str, direction: str) -> bool:
  108. """
  109. 判断是否为结息记录
  110. Args:
  111. summary: 交易摘要
  112. direction: 交易方向
  113. Returns:
  114. bool: 是否为结息记录
  115. """
  116. # 交易方向必须是收入
  117. if direction != '收入':
  118. return False
  119. # 检查摘要是否包含结息关键词
  120. summary_lower = str(summary).lower()
  121. if self.enable_fuzzy_match:
  122. # 模糊匹配:检查是否包含任何关键词
  123. for keyword in self.interest_keywords:
  124. if keyword in summary_lower:
  125. return True
  126. return False
  127. else:
  128. # 精确匹配:使用正则表达式
  129. pattern = '|'.join(self.interest_keywords)
  130. return bool(re.search(pattern, summary_lower))
  131. def _estimate_annual_interest_rate(self, interest_amount: float,
  132. balance_at_interest: float) -> Optional[float]:
  133. """
  134. 估算年化利率
  135. 公式:
  136. 年化利率 = (结息金额 ÷ 结息时点余额) × (年计息天数 ÷ 假设计息天数)
  137. Args:
  138. interest_amount: 结息金额(元)
  139. balance_at_interest: 结息时点余额(元)
  140. Returns:
  141. Optional[float]: 估算的年化利率,如果无法计算则返回None
  142. """
  143. # 检查输入有效性
  144. if pd.isna(interest_amount) or pd.isna(balance_at_interest):
  145. return None
  146. if balance_at_interest <= 0:
  147. return None
  148. if abs(interest_amount) < 0.01: # 结息金额过小
  149. return None
  150. if balance_at_interest < self.min_balance_for_calc:
  151. return None
  152. try:
  153. # 计算日利率
  154. daily_rate = interest_amount / balance_at_interest
  155. # 年化利率
  156. annual_rate = daily_rate * (self.annual_days / self.assumed_interest_days)
  157. return annual_rate
  158. except ZeroDivisionError:
  159. return None
  160. except Exception:
  161. return None
  162. def _is_abnormal_interest(self, annual_rate: float) -> bool:
  163. """
  164. 判断结息利率是否异常
  165. Args:
  166. annual_rate: 估算的年化利率
  167. Returns:
  168. bool: 是否异常
  169. """
  170. if annual_rate is None:
  171. return False
  172. # 判断是否低于阈值
  173. threshold_rate = self.base_interest_rate * self.threshold_ratio
  174. return annual_rate < threshold_rate
  175. def _generate_interest_reason(self, row: pd.Series, annual_rate: float) -> str:
  176. """
  177. 生成异常原因描述
  178. Args:
  179. row: 交易记录
  180. annual_rate: 估算的年化利率
  181. Returns:
  182. str: 异常原因描述
  183. """
  184. interest_amount = row['txAmount']
  185. balance = row.get('txBalance', 0)
  186. threshold_rate = self.base_interest_rate * self.threshold_ratio
  187. reason_parts = []
  188. # 利率对比
  189. if annual_rate is not None:
  190. rate_diff_percent = (self.base_interest_rate - annual_rate) / self.base_interest_rate * 100
  191. reason_parts.append(
  192. f"估算年化利率{annual_rate:.4%},显著低于基准利率{self.base_interest_rate:.4%}"
  193. f"(低{rate_diff_percent:.1f}%,低于阈值{threshold_rate:.4%})"
  194. )
  195. # 金额信息
  196. reason_parts.append(f"结息金额¥{interest_amount:,.2f},结息时点余额¥{balance:,.2f}")
  197. # 补充信息
  198. if annual_rate is not None and annual_rate < 0.0001: # 利率极低
  199. reason_parts.append("利率极低,可能存在异常")
  200. return "结息利率异常: " + ",".join(reason_parts)
  201. def recognize(self, csv_path: str = None, **kwargs) -> Dict[str, Any]:
  202. """
  203. 识别低利率结息记录异常
  204. Args:
  205. csv_path: CSV文件路径
  206. **kwargs: 其他参数
  207. Returns:
  208. Dict[str, Any]: 识别结果
  209. """
  210. try:
  211. # 使用父类的load_data方法加载标准化数据
  212. df = self.load_data(csv_path)
  213. print(f"🔍 {self.display_name}开始检查,共 {len(df)} 条记录")
  214. print(f" 检查规则: 结息记录的实际利率 < 基准利率({self.base_interest_rate:.4%}) × 阈值({self.threshold_ratio:.0%})")
  215. # 检查必需字段
  216. required_fields = ['txId', 'txSummary', 'txDirection', 'txAmount']
  217. missing_fields = [field for field in required_fields if field not in df.columns]
  218. if missing_fields:
  219. return {
  220. 'recognition_type': self.display_name,
  221. 'identified_count': 0,
  222. 'identified_anomalies': [],
  223. 'recognition_status': '失败',
  224. 'error': f'缺少必需字段: {missing_fields}'
  225. }
  226. # 检查余额字段(可选但重要)
  227. has_balance_field = 'txBalance' in df.columns
  228. if not has_balance_field:
  229. print(f"⚠️ 警告: 缺少txBalance字段,将无法准确计算利率")
  230. print(f" 建议: 确保数据包含余额信息以进行准确的利率分析")
  231. # ============ 识别结息记录 ============
  232. print(f"🔍 正在识别结息记录...")
  233. # 筛选可能的结息记录
  234. interest_mask = df.apply(
  235. lambda row: self._is_interest_record(row['txSummary'], row['txDirection']),
  236. axis=1
  237. )
  238. interest_transactions = df[interest_mask].copy()
  239. if len(interest_transactions) == 0:
  240. print(f"📊 未发现结息记录")
  241. print(f" 检查的关键词: {', '.join(self.interest_keywords)}")
  242. print(f" 检查的交易方向: 收入")
  243. return {
  244. 'recognition_type': self.display_name,
  245. 'identified_count': 0,
  246. 'identified_anomalies': [],
  247. 'recognition_status': '完成',
  248. 'recognition_parameters': {
  249. 'base_interest_rate': self.base_interest_rate,
  250. 'threshold_ratio': self.threshold_ratio,
  251. 'interest_keywords': self.interest_keywords,
  252. 'assumed_interest_days': self.assumed_interest_days,
  253. 'total_checked': len(df)
  254. },
  255. 'statistics': {
  256. 'total_transactions': len(df),
  257. 'interest_transactions': 0,
  258. 'has_balance_field': has_balance_field
  259. }
  260. }
  261. print(f"📊 发现 {len(interest_transactions)} 笔结息记录")
  262. # ============ 分析结息记录 ============
  263. print(f"🔍 正在分析结息记录利率...")
  264. identified_anomalies = []
  265. interest_analyses = []
  266. for idx, row in interest_transactions.iterrows():
  267. tx_id = str(row['txId'])
  268. interest_amount = float(row['txAmount'])
  269. balance = float(row.get('txBalance', 0)) if has_balance_field else 0
  270. print(f" 🔍 分析结息记录 {tx_id}: ¥{interest_amount:,.2f}")
  271. # 1. 估算年化利率
  272. annual_rate = None
  273. if has_balance_field and balance >= self.min_balance_for_calc:
  274. annual_rate = self._estimate_annual_interest_rate(interest_amount, balance)
  275. else:
  276. print(f" ⚠️ 无法计算利率: 余额不足或缺少余额字段")
  277. # 2. 判断是否异常
  278. is_abnormal = False
  279. if annual_rate is not None:
  280. is_abnormal = self._is_abnormal_interest(annual_rate)
  281. # 记录分析结果
  282. analysis = {
  283. 'tx_id': tx_id,
  284. 'interest_amount': interest_amount,
  285. 'balance_at_interest': balance if has_balance_field else None,
  286. 'estimated_annual_rate': annual_rate,
  287. 'is_abnormal': is_abnormal,
  288. 'can_calculate_rate': annual_rate is not None
  289. }
  290. interest_analyses.append(analysis)
  291. # 3. 如果异常,生成异常记录
  292. if is_abnormal:
  293. # 生成异常原因
  294. reason = self._generate_interest_reason(row, annual_rate)
  295. print(f" ❌ 发现利率异常: {reason[:80]}...")
  296. # 额外信息
  297. additional_info = {
  298. 'interest_analysis': {
  299. 'estimated_annual_rate': annual_rate,
  300. 'base_interest_rate': self.base_interest_rate,
  301. 'threshold_rate': self.base_interest_rate * self.threshold_ratio,
  302. 'interest_amount': interest_amount,
  303. 'balance_at_interest': balance,
  304. 'rate_calculation': {
  305. 'assumed_interest_days': self.assumed_interest_days,
  306. 'annual_days': self.annual_days,
  307. 'min_balance_for_calc': self.min_balance_for_calc
  308. }
  309. }
  310. }
  311. anomaly = self.format_anomaly_record(
  312. row=row,
  313. reason=reason,
  314. severity=self.severity_level,
  315. check_type='low_interest_rate',
  316. **additional_info
  317. )
  318. identified_anomalies.append(anomaly)
  319. elif annual_rate is not None:
  320. print(f" ✅ 利率正常: {annual_rate:.4%} ≥ 阈值{self.base_interest_rate * self.threshold_ratio:.4%}")
  321. else:
  322. print(f" ⚠️ 无法判断: 缺少余额数据或余额不足")
  323. # ============ 结果统计 ============
  324. print(f"✅ {self.display_name}检查完成")
  325. print(f" 检查结果:")
  326. print(f" 结息记录总数: {len(interest_transactions)}")
  327. print(f" 可计算利率记录: {sum(1 for a in interest_analyses if a['can_calculate_rate'])}")
  328. print(f" 利率异常记录: {len(identified_anomalies)}")
  329. # 显示结息统计
  330. if len(interest_transactions) > 0:
  331. print("📋 结息记录统计:")
  332. total_interest = interest_transactions['txAmount'].sum()
  333. avg_interest = interest_transactions['txAmount'].mean()
  334. print(f" 总结息金额: ¥{total_interest:,.2f}")
  335. print(f" 平均结息金额: ¥{avg_interest:,.2f}")
  336. # 显示利率分布
  337. valid_rates = [a['estimated_annual_rate'] for a in interest_analyses
  338. if a['estimated_annual_rate'] is not None]
  339. if valid_rates:
  340. avg_rate = sum(valid_rates) / len(valid_rates)
  341. min_rate = min(valid_rates)
  342. max_rate = max(valid_rates)
  343. print(f" 平均估算利率: {avg_rate:.4%}")
  344. print(f" 最低估算利率: {min_rate:.4%}")
  345. print(f" 最高估算利率: {max_rate:.4%}")
  346. return {
  347. 'recognition_type': self.display_name,
  348. 'identified_count': len(identified_anomalies),
  349. 'identified_anomalies': identified_anomalies,
  350. 'recognition_status': '完成',
  351. 'recognition_parameters': {
  352. 'base_interest_rate': self.base_interest_rate,
  353. 'threshold_ratio': self.threshold_ratio,
  354. 'interest_keywords': self.interest_keywords,
  355. 'assumed_interest_days': self.assumed_interest_days,
  356. 'annual_days': self.annual_days,
  357. 'min_balance_for_calc': self.min_balance_for_calc,
  358. 'has_balance_field': has_balance_field,
  359. 'total_interest_records': len(interest_transactions)
  360. },
  361. 'statistics': {
  362. 'total_transactions': len(df),
  363. 'interest_transactions': len(interest_transactions),
  364. 'abnormal_interest_transactions': len(identified_anomalies),
  365. 'interest_amount_statistics': {
  366. 'total_interest': float(interest_transactions['txAmount'].sum()),
  367. 'avg_interest': float(interest_transactions['txAmount'].mean()),
  368. 'max_interest': float(interest_transactions['txAmount'].max()),
  369. 'min_interest': float(interest_transactions['txAmount'].min())
  370. } if len(interest_transactions) > 0 else {},
  371. 'rate_analysis': {
  372. 'valid_rate_count': sum(1 for a in interest_analyses if a['can_calculate_rate']),
  373. 'abnormal_rate_count': len(identified_anomalies),
  374. 'rate_summary': {
  375. 'avg_rate': float(sum(a['estimated_annual_rate'] for a in interest_analyses
  376. if a['estimated_annual_rate'] is not None) /
  377. max(1, sum(1 for a in interest_analyses
  378. if a['estimated_annual_rate'] is not None)))
  379. } if any(a['estimated_annual_rate'] is not None for a in interest_analyses) else {}
  380. }
  381. }
  382. }
  383. except FileNotFoundError as e:
  384. return {
  385. 'recognition_type': self.display_name,
  386. 'identified_count': 0,
  387. 'identified_anomalies': [],
  388. 'recognition_status': '失败',
  389. 'error': f'文件不存在: {str(e)}'
  390. }
  391. except Exception as e:
  392. import traceback
  393. traceback.print_exc()
  394. return {
  395. 'recognition_type': self.display_name,
  396. 'identified_count': 0,
  397. 'identified_anomalies': [],
  398. 'recognition_status': '失败',
  399. 'error': f'数据加载或处理失败: {str(e)}'
  400. }
  401. def get_summary(self) -> Dict[str, Any]:
  402. """获取识别器摘要"""
  403. summary = super().get_summary()
  404. summary.update({
  405. 'base_interest_rate': self.base_interest_rate,
  406. 'threshold_ratio': self.threshold_ratio,
  407. 'interest_keywords_count': len(self.interest_keywords),
  408. 'assumed_interest_days': self.assumed_interest_days,
  409. 'annual_days': self.annual_days,
  410. 'min_balance_for_calc': self.min_balance_for_calc,
  411. 'severity_level': self.severity_level,
  412. 'enable_fuzzy_match': self.enable_fuzzy_match,
  413. 'data_loaded': self._data is not None
  414. })
  415. return summary
  416. def get_config_summary(self) -> Dict[str, Any]:
  417. """获取配置摘要"""
  418. return {
  419. "基准利率": f"{self.base_interest_rate:.4%}",
  420. "异常阈值": f"低于基准的{self.threshold_ratio:.0%}",
  421. "识别关键词": f"{len(self.interest_keywords)}个: {', '.join(self.interest_keywords[:3])}..."
  422. if len(
  423. self.interest_keywords) > 3 else f"{len(self.interest_keywords)}个: {', '.join(self.interest_keywords)}",
  424. "计息参数": f"{self.assumed_interest_days}天/季,{self.annual_days}天/年",
  425. "最小计算余额": f"¥{self.min_balance_for_calc:,.2f}",
  426. "检测逻辑": f"结息记录的实际利率 < {self.base_interest_rate:.4%} × {self.threshold_ratio:.0%} = 异常",
  427. "业务规则描述": "结息金额对应的有效利率显著低于基准利率,需核查结息真实性"
  428. }