app_utils.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. """缺陷分析页面的可测试业务逻辑。"""
  2. import numpy as np
  3. import pandas as pd
  4. from defect_analysis.root_cause import EXTENDED_ROOT_CAUSE_DIMENSIONS, build_extended_root_causes
  5. from defect_analysis.schemas import (
  6. CORE_REQUIRED_COLUMNS,
  7. INDUSTRY_OPTIONAL_COLUMNS,
  8. TEMPLATE_COLUMNS,
  9. get_missing_required_columns,
  10. normalize_defect_schema,
  11. )
  12. DEFECT_SOP_RECOMMENDATIONS = {
  13. "划痕": ["检查搬运轨道、吸嘴和治具接触面", "复核清洗滚刷与擦拭工位是否有硬质颗粒"],
  14. "气泡": ["检查贴合压力、真空度、OCA 状态和贴合速度", "复核贴合前清洁与材料开封时长"],
  15. "漏光": ["检查边缘贴合、背光组装、框胶和压合均匀性", "复核四角/边缘区应力与夹持状态"],
  16. "色差": ["检查背光、偏光片批次、贴合应力和老化条件", "对比同批材料与相邻工艺参数"],
  17. "异物": ["检查洁净度、清洗段、静电控制和材料暴露时间", "追溯同批材料与工位环境记录"],
  18. "亮点": ["复核点灯/AOI 判定、TFT 像素缺陷和异物压伤", "抽查高发区域是否存在压接或污染"],
  19. "暗点": ["复核点灯/AOI 判定、TFT 像素缺陷和异物压伤", "检查绑定/驱动相关区域异常"],
  20. "裂纹": ["立即检查切割、搬运、夹持和跌落冲击风险", "对同批面板执行 Hold 与复检"],
  21. }
  22. def normalize_date_bounds(start_date, end_date):
  23. """把日期范围转换成左闭右开的时间边界,确保结束日期整天被包含。"""
  24. start_ts = pd.Timestamp(start_date).normalize()
  25. end_exclusive = pd.Timestamp(end_date).normalize() + pd.Timedelta(days=1)
  26. return start_ts, end_exclusive
  27. def apply_defect_filters(
  28. df,
  29. *,
  30. start_date,
  31. end_date,
  32. selected_types,
  33. selected_batches,
  34. selected_equipment,
  35. selected_seats,
  36. selected_shift="全部",
  37. selected_severity="全部",
  38. ):
  39. """应用页面筛选条件。"""
  40. start_ts, end_exclusive = normalize_date_bounds(start_date, end_date)
  41. mask = (
  42. (df["timestamp"] >= start_ts)
  43. & (df["timestamp"] < end_exclusive)
  44. & (df["defect_type"].isin(selected_types))
  45. & (df["batch_id"].isin(selected_batches))
  46. & (df["equipment_id"].isin(selected_equipment))
  47. )
  48. if selected_shift != "全部":
  49. mask &= df["shift"] == selected_shift
  50. if selected_severity != "全部":
  51. mask &= df["severity"] == selected_severity
  52. if selected_seats:
  53. mask &= df["seat_id"].isin(selected_seats)
  54. return df[mask].copy()
  55. def classify_panel_zone(df):
  56. """按 3C 面板行业常用语义把坐标映射到关键区域。"""
  57. width = df.get("panel_width_mm", pd.Series(155.0, index=df.index)).replace(0, np.nan)
  58. height = df.get("panel_height_mm", pd.Series(340.0, index=df.index)).replace(0, np.nan)
  59. x = df.get("x_mm", width * 0.5)
  60. y = df.get("y_mm", height * 0.5)
  61. x_norm = x / width
  62. y_norm = y / height
  63. zones = []
  64. for x, y in zip(x_norm.fillna(0.5), y_norm.fillna(0.5)):
  65. labels = []
  66. if x <= 0.1:
  67. labels.append("左边缘区")
  68. if x >= 0.9:
  69. labels.append("右边缘区")
  70. if y <= 0.1:
  71. labels.append("下边缘区")
  72. if y >= 0.9:
  73. labels.append("上边缘区")
  74. if (x <= 0.12 or x >= 0.88) and (y <= 0.12 or y >= 0.88):
  75. labels.append("角落区")
  76. if 0.68 <= y <= 0.88 and 0.25 <= x <= 0.75:
  77. labels.append("FPC/绑定区")
  78. if not labels:
  79. labels.append("显示中心区")
  80. zones.append(" / ".join(labels))
  81. return pd.Series(zones, index=df.index, name="panel_zone")
  82. def calculate_kpis(source_df, filtered_df):
  83. """基于当前筛选结果计算页面 KPI。"""
  84. total_panels_inspected = filtered_df["panel_id"].nunique()
  85. defective_panels = filtered_df["panel_id"].nunique()
  86. total_defects = len(filtered_df)
  87. critical_defects = int((filtered_df["severity"] == "严重").sum()) if total_defects else 0
  88. top_defect_type = filtered_df["defect_type"].mode().iloc[0] if total_defects else "-"
  89. yield_rate = (1 - defective_panels / max(total_panels_inspected, 1)) * 100
  90. return {
  91. "total_panels_inspected": int(total_panels_inspected),
  92. "defective_panels": int(defective_panels),
  93. "yield_rate": float(yield_rate),
  94. "total_defects": int(total_defects),
  95. "critical_defects": int(critical_defects),
  96. "top_defect_type": top_defect_type,
  97. }
  98. def calculate_spc_metrics(df):
  99. """计算 SPC 所需数据,防止模拟分母造成非法概率。"""
  100. daily = df.groupby("day").agg(
  101. total_defects=("defect_id", "count"),
  102. panels_with_defects=("panel_id", "nunique"),
  103. ).reset_index()
  104. daily["day"] = pd.to_datetime(daily["day"])
  105. daily = daily.sort_values("day").reset_index(drop=True)
  106. if len(daily) < 2:
  107. return {
  108. "daily": daily,
  109. "p_bar": 0.0,
  110. "ucl": 0.0,
  111. "lcl": 0.0,
  112. "uwl": 0.0,
  113. "lwl": 0.0,
  114. "sigma_p": 0.0,
  115. }
  116. total_days = (df["timestamp"].max() - df["timestamp"].min()).days + 1
  117. total_unique_panels = df["panel_id"].nunique()
  118. estimated = max(total_unique_panels // max(total_days // 7, 1), 1)
  119. daily["estimated_inspected"] = np.maximum(estimated, daily["panels_with_defects"])
  120. daily["defect_rate"] = (
  121. daily["panels_with_defects"] / daily["estimated_inspected"]
  122. ).clip(lower=0, upper=1)
  123. p_bar = float(np.clip(daily["defect_rate"].mean(), 0, 1))
  124. n_avg = float(daily["estimated_inspected"].mean())
  125. sigma_p = float(np.sqrt(max(p_bar * (1 - p_bar), 0) / n_avg)) if n_avg > 0 else 0.0
  126. return {
  127. "daily": daily,
  128. "p_bar": p_bar,
  129. "ucl": min(1.0, p_bar + 3 * sigma_p),
  130. "lcl": max(0.0, p_bar - 3 * sigma_p),
  131. "uwl": min(1.0, p_bar + 2 * sigma_p),
  132. "lwl": max(0.0, p_bar - 2 * sigma_p),
  133. "sigma_p": sigma_p,
  134. }
  135. def build_diagnostic_dashboard(df):
  136. """生成诊断驾驶舱需要的摘要、根因候选和趋势数据。"""
  137. total_defects = len(df)
  138. if total_defects == 0:
  139. return {
  140. "severity_level": "正常",
  141. "top_defect_type": "-",
  142. "top_defect_share": 0.0,
  143. "serious_share": 0.0,
  144. "root_causes": pd.DataFrame(),
  145. "extended_root_causes": pd.DataFrame(),
  146. "daily_trend": pd.DataFrame(),
  147. "pareto": pd.DataFrame(),
  148. "primary_recommendation": "当前筛选条件下没有缺陷记录。",
  149. }
  150. type_counts = df["defect_type"].value_counts()
  151. zones = classify_panel_zone(df)
  152. zone_counts = zones.value_counts()
  153. top_defect_type = type_counts.index[0]
  154. top_defect_share = float(type_counts.iloc[0] / total_defects)
  155. top_zone = zone_counts.index[0]
  156. top_zone_share = float(zone_counts.iloc[0] / total_defects)
  157. serious_share = float((df["severity"] == "严重").sum() / total_defects)
  158. root_causes = (
  159. df.groupby(["equipment_id", "seat_id"])
  160. .agg(
  161. 缺陷数=("defect_id", "count"),
  162. 涉及面板=("panel_id", "nunique"),
  163. 主要缺陷=("defect_type", lambda s: s.mode().iloc[0]),
  164. 严重数=("severity", lambda s: int((s == "严重").sum())),
  165. )
  166. .reset_index()
  167. )
  168. root_causes["根因候选"] = root_causes["equipment_id"] + " / " + root_causes["seat_id"]
  169. root_causes["占比"] = root_causes["缺陷数"] / total_defects
  170. root_causes["严重占比"] = root_causes["严重数"] / root_causes["缺陷数"].clip(lower=1)
  171. equipment_totals = df.groupby("equipment_id")["defect_id"].count()
  172. equipment_seat_counts = df.groupby("equipment_id")["seat_id"].nunique().clip(lower=1)
  173. root_causes["期望缺陷数"] = root_causes["equipment_id"].map(
  174. equipment_totals / equipment_seat_counts
  175. ).clip(lower=0.001)
  176. root_causes["异常倍数"] = (root_causes["缺陷数"] / root_causes["期望缺陷数"]).round(2)
  177. count_score = root_causes["缺陷数"] / root_causes["缺陷数"].max()
  178. panel_score = root_causes["涉及面板"] / df["panel_id"].nunique()
  179. lift_score = (root_causes["异常倍数"] / 3).clip(upper=1)
  180. root_causes["风险分"] = (
  181. count_score * 55 + lift_score * 25 + root_causes["严重占比"] * 15 + panel_score * 5
  182. ).round(1)
  183. root_causes = root_causes.sort_values(["风险分", "缺陷数"], ascending=False).head(8)
  184. root_causes = root_causes[
  185. ["根因候选", "缺陷数", "占比", "异常倍数", "涉及面板", "主要缺陷", "严重占比", "风险分"]
  186. ].reset_index(drop=True)
  187. pareto = type_counts.rename_axis("缺陷类型").reset_index(name="缺陷数")
  188. pareto["占比"] = pareto["缺陷数"] / total_defects
  189. pareto["累计占比"] = pareto["占比"].cumsum()
  190. daily_trend = df.groupby("day").size().rename("缺陷数").reset_index()
  191. daily_trend["day"] = pd.to_datetime(daily_trend["day"])
  192. daily_trend = daily_trend.sort_values("day")
  193. extended_root_causes = build_extended_root_causes(df)
  194. if serious_share >= 0.2 or (len(root_causes) > 0 and root_causes.iloc[0]["占比"] >= 0.15):
  195. severity_level = "严重"
  196. elif serious_share >= 0.1 or top_defect_share >= 0.35:
  197. severity_level = "关注"
  198. else:
  199. severity_level = "正常"
  200. if len(root_causes) > 0:
  201. top_root = root_causes.iloc[0]
  202. primary_recommendation = (
  203. f"优先排查 {top_root['根因候选']},该组合贡献 {top_root['占比']:.1%} "
  204. f"缺陷,异常倍数 {top_root['异常倍数']:.2f}x,主要类型为 {top_root['主要缺陷']}。"
  205. )
  206. else:
  207. primary_recommendation = f"优先排查 {top_defect_type} 相关工艺参数。"
  208. return {
  209. "severity_level": severity_level,
  210. "top_defect_type": top_defect_type,
  211. "top_defect_share": top_defect_share,
  212. "top_zone": top_zone,
  213. "top_zone_share": top_zone_share,
  214. "zone_distribution": zone_counts.rename_axis("区域").reset_index(name="缺陷数"),
  215. "serious_share": serious_share,
  216. "root_causes": root_causes,
  217. "extended_root_causes": extended_root_causes,
  218. "daily_trend": daily_trend,
  219. "pareto": pareto,
  220. "primary_recommendation": primary_recommendation,
  221. }
  222. def detect_industry_patterns(df):
  223. """识别面板行业常见缺陷模式。"""
  224. if df.empty:
  225. return []
  226. patterns = []
  227. zones = classify_panel_zone(df)
  228. zone_share = zones.value_counts(normalize=True)
  229. if any(idx != "显示中心区" and share >= 0.35 for idx, share in zone_share.items()):
  230. patterns.append(f"区域集中: {zone_share.index[0]} 占比 {zone_share.iloc[0]:.1%}")
  231. coord_df = df.copy()
  232. coord_df["x_bin"] = (coord_df["x_mm"] // 5).astype(int)
  233. coord_df["y_bin"] = (coord_df["y_mm"] // 5).astype(int)
  234. repeat = coord_df.groupby(["x_bin", "y_bin"])["panel_id"].nunique().max()
  235. if repeat >= min(3, max(2, df["panel_id"].nunique())):
  236. patterns.append("跨面板重复坐标: 疑似治具、吸嘴、压头或固定接触点异常")
  237. if df["x_mm"].nunique() >= 3 and df["y_mm"].nunique() >= 3 and len(df) >= 6:
  238. corr = abs(pd.Series(df["x_mm"]).corr(pd.Series(df["y_mm"])))
  239. if pd.notna(corr) and corr >= 0.85:
  240. patterns.append("线状分布: 疑似搬运划伤、滚轮轨迹或线性压伤")
  241. batch_share = df["batch_id"].value_counts(normalize=True).iloc[0]
  242. if batch_share >= 0.5 and df["batch_id"].nunique() > 1:
  243. patterns.append(f"批次集中: {df['batch_id'].value_counts().index[0]} 占比 {batch_share:.1%}")
  244. return patterns or ["随机点状分布: 更偏向材料、环境尘埃或偶发检出"]
  245. def generate_industry_diagnosis(df, dashboard):
  246. """生成 3C 面板行业化诊断结论和排查建议。"""
  247. if df.empty:
  248. return {
  249. "headline": "当前筛选条件下没有可诊断缺陷。",
  250. "patterns": [],
  251. "recommendations": ["放宽筛选条件或上传更多检测记录后再诊断。"],
  252. }
  253. top_type = dashboard["top_defect_type"]
  254. top_zone = dashboard.get("top_zone", classify_panel_zone(df).value_counts().index[0])
  255. top_root = dashboard["root_causes"].iloc[0]["根因候选"] if len(dashboard["root_causes"]) else "当前筛选范围"
  256. patterns = detect_industry_patterns(df)
  257. recommendations = []
  258. if top_type in DEFECT_SOP_RECOMMENDATIONS:
  259. recommendations.extend(DEFECT_SOP_RECOMMENDATIONS[top_type])
  260. if "边缘" in top_zone or "角落" in top_zone:
  261. recommendations.append("优先复核边缘贴合、切割/搬运夹持、吸附接触面和四角应力状态")
  262. if "FPC" in top_zone or "绑定" in top_zone:
  263. recommendations.append("重点检查绑定压力、FPC/COF 区域异物、压接参数和 AOI 复判样本")
  264. if any("跨面板重复" in p for p in patterns):
  265. recommendations.append("对高发座号对应治具、吸嘴、压头做点检,并抽查同坐标复现样本")
  266. if dashboard["serious_share"] >= 0.2:
  267. recommendations.append("严重缺陷占比较高,建议对相关批次执行 Hold、复检或加严抽样")
  268. deduped = []
  269. for item in recommendations:
  270. if item not in deduped:
  271. deduped.append(item)
  272. headline = (
  273. f"{top_zone} 的 {top_type} 最突出,首要候选为 {top_root}。"
  274. f"建议按工序链路优先排查材料、贴合/搬运接触面和对应治具状态。"
  275. )
  276. return {
  277. "headline": headline,
  278. "patterns": patterns,
  279. "recommendations": deduped[:5],
  280. }