Preventive maintenance compliance¶
Are we doing the preventive work we planned, when we planned it?
Current value (maintenance case data): 86.6% - ! Watch. Open the case.
| Field | Value |
|---|---|
| Definition | Preventive work orders completed no later than due date + tolerance, divided by preventive work orders due in the period. |
| Formula | count(preventive AND completed_at <= due_date + tolerance_days) / count(preventive due) |
| Unit | % |
| Granularity | Preventive work order; reported monthly by asset and crew. |
| Direction | Higher is better ↑ |
| Target and thresholds | ≥ 90.0% (amber ≥ 80.0%) |
| Parameters | tolerance_days = 3 |
| Owner | Maintenance Planner |
| Source | Work-order generator (synthetic) |
| Input | table work_orders, columns wo_type, due_date, completed_at |
| Recommended chart | bar - Per-asset comparison sorted ascending; the worst assets are the action list. |
| Related KPIs | Planned maintenance share, Mean time between failures (MTBF) |
How to read it¶
A leading indicator of reliability. Late or skipped PMs show up as failures weeks later, so a drop here predicts a drop in MTBF.
Common misreadings¶
- Counting a PM done three weeks late as "done" hides the risk window; the tolerance makes lateness explicit.
- Rescheduling the due date to the completion date makes compliance 100% by construction - audit date changes.
Worked example (from the test suite)¶
| wo_type | due_date | completed_at |
|---|---|---|
| preventive | 2024-03-01 | 2024-03-01 |
| preventive | 2024-03-10 | 2024-03-13 |
| preventive | 2024-03-20 | 2024-03-24 |
| preventive | 2024-03-25 | – |
| corrective | 2024-03-05 | 2024-03-30 |
Parameters: tolerance_days = 3
Expected result: 50.00%. Tolerance 3 days. PM1 on the day: on time. PM2 +3 days: on time (limit inclusive). PM3 +4 days: late. PM4 not done: late. Corrective ignored. 2 / 4 = 0.5.
This example is asserted in tests/test_formulas.py against the Python function and in tests/test_sql.py against the SQL below.
Implementations¶
Python function (the single implementation used by the site):
@kpi("pm_compliance")
def pm_compliance(df: pd.DataFrame, tolerance_days: float) -> float:
pm = df[df["wo_type"].eq("preventive")]
return safe_ratio(pm_on_time(pm, tolerance_days).sum(), len(pm))
Reference SQL (tested on SQLite):
SELECT CAST(SUM(CASE WHEN completed_at IS NOT NULL
AND julianday(completed_at) <= julianday(due_date) + 3 THEN 1 ELSE 0 END) AS REAL) / COUNT(*)
FROM work_orders WHERE wo_type = 'preventive'