| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324 |
- """整机多缸振动事件标签。
- 最终标签不再按单缸输出,而是按稳定运行段内的整机多缸聚集事件输出:
- - 排除开机后 6 小时、停机前 6 小时;
- - 基线至少成熟 7 天;
- - 24 小时窗口内至少 4 个不同缸有候选标签;
- - 其中至少 2 条达到“异常”或“严重异常”;
- - 相邻候选窗口合并为同一个整机事件;
- - 事件只描述“整机可能异常”,不指定故障缸。
- """
- from __future__ import annotations
- from datetime import timedelta
- from pathlib import Path
- import numpy as np
- import pandas as pd
- from . import config, unified_labels
- SEVERITY = {"轻微": 1, "异常": 2, "严重异常": 3}
- LEVELS = ("轻微", "异常", "严重异常")
- BAD_LEVELS = {"异常", "严重异常"}
- MACHINE_FIELDS = unified_labels.CORE_FIELDS + [
- "参与缸数", "参与缸", "异常缸数", "严重异常缸数",
- "异常标签数", "严重异常标签数", "候选标签数",
- "最大异常分数", "最大RMS", "最大峭度",
- "转速范围", "运行段开始", "运行段结束", "整机聚集窗口小时",
- ]
- def build_runs(frame: pd.DataFrame, gap_hours: float = 2.0) -> pd.DataFrame:
- frame = frame.copy()
- frame["sample_time"] = pd.to_datetime(frame["sample_time"])
- running = frame.loc[frame["rpm"].astype(float) > 0, ["sample_time"]].drop_duplicates()
- running = running.sort_values("sample_time").reset_index(drop=True)
- if running.empty:
- return pd.DataFrame(columns=["run_id", "start", "end", "hours"])
- running["gap_hours"] = running["sample_time"].diff().dt.total_seconds() / 3600.0
- running["run_id"] = (running["gap_hours"] > gap_hours).cumsum().astype(int)
- rows = []
- for run_id, group in running.groupby("run_id", sort=True):
- start = pd.Timestamp(group["sample_time"].min())
- end = pd.Timestamp(group["sample_time"].max())
- rows.append({
- "run_id": int(run_id),
- "start": start,
- "end": end,
- "hours": round((end - start).total_seconds() / 3600.0, 2),
- })
- return pd.DataFrame(rows)
- def _run_index(timestamp: pd.Timestamp, runs: pd.DataFrame, tolerance_hours: float = 1.0) -> int | None:
- tolerance = pd.Timedelta(hours=tolerance_hours)
- for row in runs.itertuples(index=False):
- if row.start - tolerance <= timestamp <= row.end + tolerance:
- return int(row.run_id)
- return None
- def prepare_candidates(
- labels: pd.DataFrame,
- frame: pd.DataFrame,
- runs: pd.DataFrame,
- guard_start_hours: int = 6,
- guard_end_hours: int = 6,
- ) -> pd.DataFrame:
- labels = labels.copy()
- labels["开始小时"] = pd.to_datetime(labels["开始小时"])
- labels["结束小时"] = pd.to_datetime(labels["结束小时"])
- labels["cylinder"] = labels["阶段部位"].astype(str).str.replace("5号机", "", regex=False)
- run_by_id = runs.set_index("run_id")
- rows = []
- for row in labels.itertuples(index=False):
- run_id = _run_index(pd.Timestamp(row.开始小时), runs)
- if run_id is None:
- continue
- run = run_by_id.loc[run_id]
- start = pd.Timestamp(row.开始小时)
- end = pd.Timestamp(row.结束小时)
- stable = bool(start >= run["start"] + pd.Timedelta(hours=guard_start_hours)
- and end <= run["end"] - pd.Timedelta(hours=guard_end_hours))
- rows.append({
- **row._asdict(),
- "run_id": run_id,
- "run_start": run["start"],
- "run_end": run["end"],
- "stable": stable,
- })
- result = pd.DataFrame(rows)
- if result.empty:
- return result
- return result
- def _merge_candidate_windows(candidates: list[dict], merge_gap_hours: int = 6) -> list[dict]:
- if not candidates:
- return []
- candidates = sorted(candidates, key=lambda item: item["window_start"])
- merged = [candidates[0].copy()]
- for candidate in candidates[1:]:
- current = merged[-1]
- if candidate["window_start"] <= current["window_end"] + pd.Timedelta(hours=merge_gap_hours):
- current["window_end"] = max(current["window_end"], candidate["window_end"])
- current["indices"] = sorted(set(current["indices"]) | set(candidate["indices"]))
- else:
- merged.append(candidate.copy())
- return merged
- def _ordered_cylinders(values: list[str]) -> list[str]:
- present = set(values)
- return [name for name in config.CYLINDERS if name in present]
- def build_machine_events(
- labels: pd.DataFrame,
- frame: pd.DataFrame,
- hourly: pd.DataFrame,
- runs: pd.DataFrame,
- guard_start_hours: int = 6,
- guard_end_hours: int = 6,
- window_hours: int = 24,
- min_cylinders: int = 4,
- min_bad_labels: int = 2,
- merge_gap_hours: int = 6,
- ) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]:
- candidates = prepare_candidates(
- labels, frame, runs,
- guard_start_hours=guard_start_hours,
- guard_end_hours=guard_end_hours,
- )
- if candidates.empty:
- return pd.DataFrame(columns=MACHINE_FIELDS), candidates, pd.DataFrame()
- candidates = candidates[candidates["stable"]].copy()
- candidates = candidates.sort_values(["开始小时", "cylinder"]).reset_index(drop=True)
- candidates["severity_num"] = candidates["阶段"].map(SEVERITY).fillna(0).astype(int)
- score_map = {
- (row.cylinder, pd.Timestamp(row.start_hour)): float(row.peak_score)
- for row in hourly.itertuples(index=False)
- }
- windows = []
- for index, row in candidates.iterrows():
- start = pd.Timestamp(row["开始小时"])
- end = start + pd.Timedelta(hours=window_hours)
- part = candidates[(candidates["结束小时"] >= start) & (candidates["开始小时"] <= end)]
- if part["cylinder"].nunique() < min_cylinders:
- continue
- if int(part["阶段"].isin(BAD_LEVELS).sum()) < min_bad_labels:
- continue
- windows.append({
- "window_start": start,
- "window_end": end,
- "indices": part.index.tolist(),
- })
- windows = _merge_candidate_windows(windows, merge_gap_hours=merge_gap_hours)
- frame = frame.copy()
- frame["sample_time"] = pd.to_datetime(frame["sample_time"])
- rows = []
- for index, window in enumerate(windows, start=1):
- part = candidates.loc[window["indices"]].copy()
- start = pd.Timestamp(part["开始小时"].min())
- end = pd.Timestamp(part["结束小时"].max())
- cylinders = _ordered_cylinders(part["cylinder"].tolist())
- bad = part[part["阶段"].isin(BAD_LEVELS)]
- severe = part[part["阶段"] == "严重异常"]
- bad_cylinders = _ordered_cylinders(bad["cylinder"].tolist())
- severe_cylinders = _ordered_cylinders(severe["cylinder"].tolist())
- max_level = max(part["阶段"], key=lambda value: SEVERITY.get(value, 0))
- peak_score = max(
- (score_map.get((row.cylinder, pd.Timestamp(row.开始小时)), np.nan) for row in part.itertuples(index=False)),
- default=np.nan,
- )
- rpm_part = frame[
- (frame["sample_time"] >= start)
- & (frame["sample_time"] <= end)
- & (frame["cylinder"].isin(cylinders))
- ]
- rpm_min = float(rpm_part["rpm"].min()) if not rpm_part.empty else np.nan
- rpm_max = float(rpm_part["rpm"].max()) if not rpm_part.empty else np.nan
- rms_col = pd.to_numeric(part.get("阶段最大RMS"), errors="coerce")
- kurt_col = pd.to_numeric(part.get("阶段最大峭度"), errors="coerce")
- rms_max = float(rms_col.max()) if rms_col.notna().any() else np.nan
- kurt_max = float(kurt_col.max()) if kurt_col.notna().any() else np.nan
- run_start = pd.Timestamp(part["run_start"].min())
- run_end = pd.Timestamp(part["run_end"].max())
- hours = round((end - start).total_seconds() / 3600.0, 2)
- bad_hours = int(bad["持续自然小时数"].sum()) if not bad.empty else 0
- severe_hours = int(severe["持续自然小时数"].sum()) if not severe.empty else 0
- trigger = (
- f"5号机整机稳定运行段:{start:%Y-%m-%d %H:%M}~{end:%Y-%m-%d %H:%M},"
- f"24小时窗口内{len(cylinders)}个缸出现候选标签({'、'.join(cylinders)}),"
- f"其中{len(bad)}条达到异常/严重异常({'、'.join(bad_cylinders)});"
- f"最大等级{max_level},最大异常分数{peak_score:.2f};"
- f"参与缸数={len(cylinders)},异常缸数={len(bad_cylinders)}"
- )
- rows.append({
- "标签ID": f"5号机-MACH-{index:04d}",
- "标签类型": "振动",
- "机组": "5号机",
- "批次": "全量",
- "周期编号": f"5号机-MACH-{index:04d}",
- "阶段": max_level,
- "阶段部位": "5号机整机",
- "阶段点位": "5号机一缸~六缸缸体振动",
- "开始小时": start,
- "结束小时": end,
- "持续自然小时数": hours,
- "有效运行小时数": int(part["持续自然小时数"].sum()),
- "基线值": np.nan,
- "正常波动带": "",
- "阶段开始值": np.nan,
- "阶段结束值": np.nan,
- "阶段最大值": rms_max,
- "相对基线变化": np.nan,
- "阶段最大连续异常有效小时数": bad_hours,
- "阶段高报警小时数": bad_hours,
- "阶段高高报警小时数": severe_hours,
- "方向": "整机多缸",
- "特征类型": "整机振动",
- "置信度": "高" if severe_cylinders else "中",
- "是否形成确认等级": "是",
- "阶段触发依据": trigger,
- "事件关联": "",
- "启动过渡比例": 0.0,
- "局部基线可用比例": 1.0,
- "数据来源": "wave_file 首周期波形",
- "算法版本": "cylinder_vibration_machine_v2",
- "参与缸数": len(cylinders),
- "参与缸": " / ".join(cylinders),
- "异常缸数": len(bad_cylinders),
- "严重异常缸数": len(severe_cylinders),
- "异常标签数": int(len(bad)),
- "严重异常标签数": int(len(severe)),
- "候选标签数": int(len(part)),
- "最大异常分数": round(peak_score, 3) if np.isfinite(peak_score) else np.nan,
- "最大RMS": round(rms_max, 4) if np.isfinite(rms_max) else np.nan,
- "最大峭度": round(kurt_max, 3) if np.isfinite(kurt_max) else np.nan,
- "转速范围": f"{rpm_min:.1f}~{rpm_max:.1f}" if np.isfinite(rpm_min) and np.isfinite(rpm_max) else "",
- "运行段开始": run_start,
- "运行段结束": run_end,
- "整机聚集窗口小时": window_hours,
- })
- result = pd.DataFrame(rows, columns=MACHINE_FIELDS)
- return result, candidates, pd.DataFrame(windows)
- def validate_events(events: pd.DataFrame, events_config: list[dict] | None = None) -> pd.DataFrame:
- rows = []
- for event in events_config or config.EVENTS:
- event_id = event["event_id"]
- event_date = pd.Timestamp(event["date"])
- window_start = event_date - pd.Timedelta(days=7)
- window_end = event_date + pd.Timedelta(days=1)
- matched = events[
- (events["开始小时"] <= window_end)
- & (events["结束小时"] >= window_start)
- ].sort_values("开始小时") if not events.empty else events
- if matched.empty:
- rows.append({
- "event_id": event_id,
- "event_date": event["date"],
- "fault_cylinder": event["cylinder"],
- "hit": False,
- "machine_label_id": "",
- "start_time": pd.NaT,
- "end_time": pd.NaT,
- "duration_hours": np.nan,
- "lead_days": np.nan,
- "max_level": "",
- "participating_cylinders": "",
- "abnormal_cylinders": "",
- })
- continue
- matched_event = matched.iloc[0]
- start = pd.Timestamp(matched_event["开始小时"])
- rows.append({
- "event_id": event_id,
- "event_date": event["date"],
- "fault_cylinder": event["cylinder"],
- "hit": True,
- "machine_label_id": matched_event["标签ID"],
- "start_time": start,
- "end_time": pd.Timestamp(matched_event["结束小时"]),
- "duration_hours": float(matched_event["持续自然小时数"]),
- "lead_days": round((event_date - start).total_seconds() / 86400.0, 2),
- "max_level": matched_event["阶段"],
- "participating_cylinders": matched_event["参与缸"],
- "abnormal_cylinders": matched_event["异常缸数"],
- })
- return pd.DataFrame(rows)
- def build_summary(events: pd.DataFrame, candidates: pd.DataFrame, validation: pd.DataFrame, runs: pd.DataFrame) -> dict:
- event_run_ids = sorted(set(events["运行段开始"].dropna().tolist()))
- all_run_ids = set(runs["run_id"].tolist())
- return {
- "rule": {
- "stable_start_hours": 6,
- "stable_end_hours": 6,
- "window_hours": 24,
- "min_cylinders": 4,
- "min_bad_labels": 2,
- "merge_gap_hours": 6,
- },
- "candidate_labels": int(len(candidates)),
- "events": int(len(events)),
- "runs": int(len(runs)),
- "event_run_ids": event_run_ids,
- "event_runs": int(len(event_run_ids)),
- "normal_runs": int(len(all_run_ids) - len(event_run_ids)),
- "validation": {
- "total": int(len(validation)),
- "hit": int(validation["hit"].sum()) if not validation.empty else 0,
- },
- }
|