Selbstgesteuertes Projekt
Predictive Maintenance: Industrial ML for Fault Detection
Condition-Monitoring Engineer · KI / ML · Juli 2026
Built an end-to-end rolling-bearing fault detector from vibration data: engineered time-domain and envelope-spectrum features, trained a depth-capped decision tree on training bearings only, and evaluated it on a held-out set with explicit catch-rate and false-alarm metrics. The final memo defended a 0.40 alarm threshold using the student’s own cost numbers and correctly localized the fault to the outer race via BPFO.
Bewertet nach
- Feature engineering (signal to honest numbers)25%
- Held-out evaluation (honest measurement)25%
- Cost-based threshold decision + memo25%
- Compact model + embedded-budget justification15%
- Plain-language explanation10%
Bestanden · Bestehensgrenze 60/100
Was herausstach6
- Used kurtosis as the root split and stated a shallow tree depth of 1 with 3 nodes, which fits the embedded-budget constraint.
- Reported held-out catch-rate and false-alarm rate explicitly and tied the threshold choice to miss-vs-inspection costs.
- Correctly mapped the dominant defect frequency to BPFO and the outer race in the maintenance memo.
- Leakage-Free Feature Engineering
- Honest Held-Out Evaluation
- Physics-Based Fault Localization
Meine eingereichte Arbeit11 Aufgaben
Initial Prediction
{"guess":"No, the metal case blocks it","index":1}Load Plot Signal
{"prompt":"After plotting the faulty bearing, identify what its waveform shows that the healthy one lacked.","selectedRead":"periodic","correctRead":"periodic","readCorrect":true,"passed":true,"attempt":1}Load Plot Signal Code
{"checkpointId":"schaeffler_load_plot","code":"# STEP 1 of your detector: plot the FAULTY bearing so you can SEE the fault.\nimport numpy as np, matplotlib.pyplot as plt\n\nfs = 20_000 # sampling rate: 20,000 readings/second\nt = np.arange(0, 0.05, 1/fs) # a 50-ms window\nrng = np.random.default_rng(7)\n\n# ▼▼▼ CHANGE THIS ONE LINE: load the faulty snapshot, not the healthy one ▼▼▼\nSNAPSHOT = \"healthy\" # <-- set this to \"faulty\"\n# ▲▲▲ then press \"Run cell\" and watch the plot change ▲▲▲\n\n# (Both snapshots come from the same housing-resonance + noise. The FAULTY one\n# also carries a sharp KNOCK every time a ball rolls over the spall — about\n# once every 4 ms here. This is realistic physics-based SYNTHETIC vibration,\n# modeled on documented bearing-fault signatures — not real measured data.)\nbase = 0.6*np.sin(2*np.pi*3000*t)*np.exp(-40*(t % 0.004)) + rng.normal(0, 0.5, t.size)\nif SNAPSHOT == \"faulty\":\n knock = np.zeros_like(t)\n for strike in np.arange(0.0, 0.05, 0.004): # an impulse every 4 ms\n k = np.abs(t - strike) < 1/fs\n knock += 6.0 * k # the sharp impact\n signal = base + knock\nelse:\n signal = base\n\nplt.figure(figsize=(7, 2.6))\nplt.plot(t*1000, signal, lw=0.6,\n color=\"#B45309\" if SNAPSHOT == \"faulty\" else \"#0E9F6E\")\nplt.title(f\"{SNAPSHOT.capitalize()} bearing — vibration over 50 ms\")\nplt.xlabel(\"time (ms)\"); plt.ylabel(\"acceleration (g)\")\nplt.tight_layout(); plt.show()\nprint(\"Plotted:\", SNAPSHOT, \"bearing —\", signal.size, \"samples\")","stdout":"Plotted: healthy bearing — 1000 samples\n","stderr":"","error":null,"imageCount":1,"durationMs":167,"ranAt":"2026-07-24T21:41:45.555Z"}Example Rms Worked
{"code":"import numpy as np\n\n# A healthy bearing snapshot: low, even shaking, no sharp knocks.\n# (In the lab you'll load real curated snapshots from CSV instead.)\nrng = np.random.default_rng(7)\nhealthy = rng.normal(0.0, 0.18, size=20000) # 1 second @ 20 kHz\n\n# RMS, step by step:\nsquared = healthy ** 2 # 1) square every reading\nmean_sq = squared.mean() # 2) average them\nrms = np.sqrt(mean_sq) # 3) square-root -> typical energy\n\nprint(f\"samples : {healthy.size}\")\nprint(f\"RMS (healthy): {rms:.3f}\")","stdout":"samples : 20000\nRMS (healthy): 0.179\n","stderr":"","error":null,"imageCount":0,"durationMs":2,"ranAt":"2026-07-24T21:44:42.945Z"}Time Features
{"prompt":"Which time-domain feature jumps the most on the faulty bearing?","picked":"kurtosis","correct":"kurtosis","passed":true,"attempt":1}Freq Features
{"code":"import numpy as np\n\n# envelope_spectrum(signal, fs) is PROVIDED (plain numpy). It returns\n# (freqs, amp): the frequency axis and the envelope-spectrum amplitude.\nfreqs, amp = envelope_spectrum(signal, fs=20000)\nprint(\"freq range:\", freqs.min(), freqs.max())\nprint(\"amp max:\", np.max(amp))\nprint(\"BPFO bins:\", np.sum(np.abs(freqs - 236.4) <= 3))\nprint(\"First 10 freqs:\", freqs[:10])\n\n# The four SUPPLIED defect frequencies (Rexnord ZA-2115 @ 2000 RPM):\ndefect_hz = {\"FTF\": 14.7, \"BSF\": 70.9, \"BPFO\": 236.4, \"BPFI\": 296.9}\n\ndef band_energy(freqs, amp, centre, half_width=15.0):\n # Sum the amplitude in a +/- half_width Hz band around centre.\n mask = np.abs(freqs - centre) <= half_width\n return float(amp[mask].sum())\n\nenergy = {name: band_energy(freqs, amp, hz) for name, hz in defect_hz.items()}\ndominant = max(energy, key=energy.get)\n\nfor name, e in energy.items():\n print(f\"{name:5s} {e:8.2f}\")\nprint(\"dominant:\", dominant)","stdout":"freq range: 0.0 10000.0\namp max: 0.07923414023948491\nBPFO bins: 0\nFirst 10 freqs: [ 0. 20. 40. 60. 80. 100. 120. 140. 160. 180.]\nFTF 0.01\nBSF 0.05\nBPFO 0.05\nBPFI 0.03\ndominant: BSF\n","stderr":"","error":null,"imageCount":0,"durationMs":2,"ranAt":"2026-07-24T22:16:02.047Z"}Fault Location
{"prompt":"Which defect frequency dominates the envelope spectrum (which names the cracked ring)?","choice":"BPFO","choiceRing":"Outer race","correctAnswer":"BPFO","correctRing":"Outer race","priorGuessRing":null,"passed":true,"attempt":1}Train Classifier
{"prompt":"Fit a depth-capped DecisionTree on TRAINING bearings only, then name the root-split feature and state the tree depth + node count against the embedded budget (depth ≤ 3, a few KB).","rootFeature":"kurtosis","rootCorrect":true,"statedDepth":1,"statedNodes":3,"withinBudget":true,"budgetDepthCap":3,"trainedOnTrainingOnly":true,"passed":true,"attempt":1}Evaluation Heldout
{"prompt":"Run the detector on the held-out set (healthy + faulty, split by physical bearing), then read back catch-rate and false-alarm rate from the printout.","catchRate":0.83,"falseAlarmRate":0.25,"keyCatchRate":0.83,"keyFalseAlarmRate":0.25,"tolerance":0.1,"passed":true,"attempt":1}Threshold Decision
{"prompt":"Set the alarm threshold on the held-out scores. Lower catches more faults but raises false alarms; pick the threshold you can defend on a cost basis.","threshold":0.4,"caught":12,"missed":0,"falseAlarms":3,"catchRatePct":100,"falseAlarmRatePct":25,"totalCost":1500,"costMissEur":20000,"costFalseAlarmEur":500,"locked":true}Decision Memo
{"prompt":"Write the one-page maintenance decision memo: chosen threshold, catch-rate, false-alarm rate, cost reasoning, which ring is cracked and how you know, plus one plain-language sentence for a non-expert manager. Use your own numbers.","memo":"I recommend using an alarm threshold of 0.40. At this threshold, the model achieves a catch-rate of 100% and a false-alarm rate of 25% . I chose this threshold because missing a bearing fault could cause an expensive production line stop (about €20,000), while a false alarm only requires a quick inspection (about €500). The model identified the Fault located: outer-race bearing fault (BPFO) using the vibration features and defect-frequency information. In simple terms, the system monitors bearing vibration and raises an alarm when the pattern is similar to a faulty bearing, helping maintenance staff detect problems early before they lead to equipment failure.","checklistCovered":["threshold","catch","falseAlarm","cost","fault","plain"],"passed":true,"attempt":1}