|
|
@@ -28,7 +28,7 @@ def load_features(path):
|
|
|
rows = list(csv.DictReader(open(path, encoding="utf-8-sig")))
|
|
|
for row in rows:
|
|
|
row["day"] = row["sample_time"][:10]
|
|
|
- for name in TREND_FEATURES:
|
|
|
+ for name in TREND_FEATURES + ("indicated_work",):
|
|
|
try:
|
|
|
row[name] = float(row[name])
|
|
|
except (TypeError, ValueError):
|
|
|
@@ -94,6 +94,58 @@ def peer_sudden_changes(rows, feature, segments):
|
|
|
print(f" {cylinder}缸{side}: 偏差 {values[i-1]*100:+.1f}% → {values[i]*100:+.1f}% 于 {days[i]} ({delta/scale:+.1f}×) {' '.join(flags)}")
|
|
|
|
|
|
|
|
|
+def _fit_drift(days, values):
|
|
|
+ x = np.array([(datetime.strptime(d, "%Y-%m-%d") - datetime.strptime(days[0], "%Y-%m-%d")).days for d in days], dtype=float)
|
|
|
+ y = np.asarray(values, dtype=float)
|
|
|
+ if x[-1] <= 0 or len(y) < 8:
|
|
|
+ return None
|
|
|
+ slope, intercept = np.polyfit(x, y, 1)
|
|
|
+ pred = slope * x + intercept
|
|
|
+ ss_res = float(np.sum((y - pred) ** 2))
|
|
|
+ ss_tot = float(np.sum((y - y.mean()) ** 2))
|
|
|
+ r2 = 1 - ss_res / ss_tot if ss_tot > 0 else 0.0
|
|
|
+ center = float(np.median(y))
|
|
|
+ rel30 = (slope * 30.0 / center) if abs(center) > 1e-9 else 0.0
|
|
|
+ return rel30, r2, float(y[0]), float(y[-1])
|
|
|
+
|
|
|
+
|
|
|
+def slow_trends(rows, features=("p_discharge", "pressure_ratio", "efficiency", "n_comp", "indicated_work")):
|
|
|
+ """Slow, weak drifts: linear fit per (cylinder, side) and per peer-relative series."""
|
|
|
+ print("\n== 慢趋势:自基线(斜率 %/30天,要求 |%|>=3 且 R2>=0.3) ==")
|
|
|
+ by_series = defaultdict(list)
|
|
|
+ for row in rows:
|
|
|
+ by_series[(row["cylinder"], row["side"])].append(row)
|
|
|
+ for (cylinder, side), series in sorted(by_series.items()):
|
|
|
+ days = sorted({r["day"] for r in series})
|
|
|
+ for feature in features:
|
|
|
+ daily = {d: np.median([r[feature] for r in series if r["day"] == d and r[feature] is not None]) for d in days}
|
|
|
+ present = [d for d in days if daily.get(d) is not None]
|
|
|
+ fit = _fit_drift(present, [daily[d] for d in present])
|
|
|
+ if fit and abs(fit[0]) >= 0.03 and fit[1] >= 0.3:
|
|
|
+ print(f" {cylinder}缸{side} {feature:<16} {fit[0]*100:+.1f}%/30天 R2={fit[1]:.2f} ({fit[2]:.3f}->{fit[3]:.3f})")
|
|
|
+
|
|
|
+ print("\n== 慢趋势:同级 peer 相对(%/30天) ==")
|
|
|
+ by_ps = defaultdict(dict)
|
|
|
+ for row in rows:
|
|
|
+ if row["pressure_ratio"] is not None:
|
|
|
+ by_ps[(row["stage"], row["side"], row["day"])][row["cylinder"]] = row["pressure_ratio"]
|
|
|
+ rel = defaultdict(dict)
|
|
|
+ for (stage, side, day), members in by_ps.items():
|
|
|
+ clean = {c: v for c, v in members.items() if v is not None}
|
|
|
+ if len(clean) < 2:
|
|
|
+ continue
|
|
|
+ med = float(np.median(list(clean.values())))
|
|
|
+ if abs(med) < 1e-9:
|
|
|
+ continue
|
|
|
+ for cylinder, value in clean.items():
|
|
|
+ rel[(cylinder, side)][day] = (value - med) / med
|
|
|
+ for (cylinder, side), daily in sorted(rel.items()):
|
|
|
+ days = sorted(daily)
|
|
|
+ fit = _fit_drift(days, [daily[d] for d in days])
|
|
|
+ if fit and abs(fit[0]) >= 0.03 and fit[1] >= 0.3:
|
|
|
+ print(f" {cylinder}缸{side} pressure_ratio(peer) {fit[0]*100:+.1f}%/30天 R2={fit[1]:.2f} ({fit[2]*100:+.1f}%->{fit[3]*100:+.1f}%)")
|
|
|
+
|
|
|
+
|
|
|
def main():
|
|
|
parser = argparse.ArgumentParser(description="功图特征长期偏离检测(对齐 evolution 异常段)")
|
|
|
parser.add_argument("csv_path")
|
|
|
@@ -148,6 +200,7 @@ def main():
|
|
|
tag = " ".join(flags)
|
|
|
print(f" [突发] {feature}: Δ={delta:+.3f} ({delta / scale:+.1f}×基线尺度) 于 {day_jump} {tag}")
|
|
|
peer_sudden_changes(rows, "p_discharge", segments)
|
|
|
+ slow_trends(rows)
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|