Inspiration
Ask a city maintenance manager what's been going wrong with a particular water main and they can't easily tell you. Not because nobody wrote it down — because it got written down forty times, in forty places, by forty people, and nothing ties those records to each other.
That's the shape of the dataset we were handed. No incident ID. No field recording what actually failed. Nothing marking a job as finished or a repair as successful. You can see six work orders hanging off one asset and still have no idea whether that asset is in trouble. Those six visits might be routine inspections. They might be duplicate database links. They might belong to a bulk program that touched two thousand assets at once. They might concern six different parts of the same structure.
So the interesting problem here isn't missing data. It's data that's sitting right there and is easy to read wrong. I built PM Signal Desk to work through that carefully and hand a manager a shortlist they can actually check.
What it does
The app answers one question, and only one:
Which assets got repeated visits with recurring observations, and which of those histories deserve a human's attention?
Pick a candidate and you see its maintenance timeline with the work-order IDs and dates attached, the observations that keep recurring in the crew's own words, and, the part I care about most, the records that cut against the story. Below that sits a hypothesis, labelled as a hypothesis, and a concrete next step. Every report exports with its work-order citations intact.
There's also an offline semantic search across the cleaned notes, which helps when two crews described the same thing in completely different language.
What it doesn't do is diagnose anything. It decides what a person should look at next.
How I built it
Counting came first, and counting nearly broke the whole thing.
The work-order table holds 37,778 rows, and each one is a genuine job. The asset-link table holds 867,448. That gap exists because a single work order can attach itself to thousands of assets at once. Join the two tables, count the rows, and you aren't counting maintenance at all. You're counting attachments, and you will produce failure patterns out of thin air.
Everything downstream therefore counts distinct WorkOrderId. Asset identity needed fixing too, since EntityUid values collide across entity types, so the key became:
EntityType|EntityUid → WMAIN|2456
From there the pipeline stays deliberately unclever, because I wanted to be able to defend every step of it out loud:
- Audit and normalise the exports, which yields 75,058 distinct asset keys
- Drop what misleads: 2,096 duplicated work-order/asset links; 3,128 bulk work orders, each touching more than 10 assets and between them covering 797,709 attachment rows; 109 catch-all keys such as department roll-ups and placeholders; 2 records dated 1970 and 2222
- Strip dispatcher boilerplate, so repeated form headings can't pass themselves off as crew observations
- Split each asset's history into episodes wherever the gap runs past 180 days
- Run a cheap baseline of three or more distinct work orders, which yields 2,108 episodes
- Apply the evidence gate: the same observation family, across at least 2 distinct work orders, on at least 2 distinct dates. That leaves 115
- Keep the counterevidence: no problem found, no trouble found, operating when checked, line clear
- Report in three separated layers: Observed, Hypothesis, Suggested review action
The AI here is latent-semantic retrieval. A reproducible 6,000-note sample gets projected into 64 dimensions with truncated SVD, and cosine nearest-neighbour search pulls back related histories, reranked by observation family. All of it runs on the machine.
I didn't call a hosted language model, and that was a decision rather than a limitation I ran into. These notes carry employee names, phone numbers and street addresses, and the dataset has no verified causes anywhere in it. A model would have written me a confident paragraph explaining why the main failed, and there would have been nothing underneath that paragraph. Build the auditable chain first. An LLM layer makes sense later, after better redaction, expert-labelled examples, constrained outputs, enforced citations and human review, in that order and not before.
Example output: WMAIN|2456
Here's a real candidate, laid out the way the app lays it out. The layers stay separate throughout; nothing blends.
Asset: WMAIN|2456, a water main. Documentary support: HIGH. Window: 26 March to 13 June 2008. 4 distinct work orders.
Observed. What the records themselves support:
- Four distinct work orders inside 78 days: WO 93323, WO 95876, WO 96720, WO 98303
- Three carry water-leak or main-break language: WO 93323, WO 95876, WO 98303
- Repair and action language shows up before a later work order in the same observation family
- WO 96720 records leak detection with no problems found
Hypothesis. Offered as one, not as a finding:
- The earlier repair may not have reached the real leak. Or a nearby section of the same main failed on its own.
Not established here:
- That the first repair failed
- That the later complaint is the same defect
- That all four orders point at the same spot on the main
- What component was at fault
Suggested review: compare the physical locations, the earlier repair details and parts, and the leak-detection findings, and work out whether the later complaint sits at the same point, before anyone touches the preventive-maintenance plan.
I picked this case for the demo because of WO 96720. A candidate with four records all agreeing would have looked better on screen and taught me nothing.
Second case: E_STREETLIGHT|15364. HIGH support, 30 June 2008 to 22 May 2009, 9 distinct work orders: WO 99242, 103769, 113815, 114360, 116251, 116946, 117579, 118635, 119707. Five contain streetlight-out or still-out language. The notes also log fuse changes, water sitting in a fuse holder, equipment pulled for bench inspection, and ballast and globe work later on. WO 114360 is the counterevidence: a no-trouble-found test. Observed: recurring outage language across separate dates, alongside repeated component replacement. Hypothesis: something in the circuit, feeder, control or luminaire may be outlasting the individual visits. Review: check incoming power, control state, fuse-holder condition and the parts already replaced before closing another order.
Third case: SSGRAVITYMAIN|1087, MEDIUM support, 4 work orders: WO 102950, 102957, 103019, 110946. Two records carry sewer-restriction or overflow language and two mention grease. I don't claim root intrusion on this one, because nothing in the source notes for that episode says so.
What I explored, and what went wrong on the way
The database structure kept handing me convincing fake patterns. Bulk inventory sweeps made thousands of unconnected assets look maintained. Numeric IDs collided across types. Placeholder entries like CITYFACILITIES|642 quietly swallowed hundreds of unrelated addresses. Each one had to be found, measured and written down rather than silently discarded.
Then there was the boilerplate. Dispatcher intake headings repeat on nearly every ticket, and my first evidence gate was matching on them — reading a form label as though a crew had observed something. That's exactly the failure this challenge is about, and I'd built it into my own system without noticing.
Privacy turned out harder than I expected too. My first pass at the redacted public snapshot still had name formats surviving in it, which meant rebuilding the artifacts and scanning again before anything could be deployed anywhere.
Accomplishments I'm proud of
Keeping WO 96720 visible. Dropping it would have produced a tidier demo, and it would have made the tool worse at the one job it has.
And narrowing honestly. Going from 2,108 to 115 is a real reduction, and the app still says on screen that those 115 aren't confirmed failures and that the episodes outside them aren't being called healthy.
What I learned
Reading the records changed the rules. That's the whole lesson.
My first evidence-gated queue held 82 episodes and I was fairly pleased with it, right up until I sat down and read what crews actually type. They don't write "sewer stoppage." They write "stop up" and "back up." Teaching the library that shorthand qualified 18 more episodes. Root-related wording sitting near "line," "manhole," "sawed" or "jetted" added another 16. One episode came in through both routes.
82 plus 33 gives 115. The threshold never moved. I didn't loosen anything. I just learned how the people doing the work describe it, and no amount of reasoning from generic examples would have got me there.
What's next
Not more automation. A person.
The honest next step is a maintenance lead working through 30 to 50 candidate episodes and marking, for each one: do these work orders describe the same incident, is this genuinely recurring, does the hypothesis hold up, is the suggested action worth anything, and what did the system fail to see?
Those labels would give the first precision and recall numbers that aren't grading my own homework, plus episode boundaries tuned per asset group, a wider observation library, calibrated confidence, and a retrieval evaluation worth the name. Only after all of that does a tightly constrained LLM layer, or Cityworks integration, make any sense.
What this project does not claim
Worth stating flatly, because it matters more than the demo does:
- The 115 candidates are not confirmed failures
- No repair has been shown to have failed
- No cause has been established
- The 0.83 precision@5 figure comes from a six-query, non-independent smoke test. It is not system accuracy
- The 180-day episode boundary and the 10-asset bulk threshold are documented, configurable heuristics, not validated constants
- No maintenance expert has reviewed the candidates yet
PM Signal Desk helps a maintenance manager see what deserves investigation, and keeps every conclusion tied to the evidence behind it.
Built With
- cosine-similarity
- joblib
- json
- jupyter
- latent-semantic-analysis
- numpy
- pandas
- parquet
- pyarrow
- pytest
- python
- regex
- scikit-learn
- streamlit
- truncated-svd
Log in or sign up for Devpost to join the conversation.