A map of dots gets looked at. A number for the spot somebody pointed at gets argued with

Customer Intelligence: leadership had only ever seen its customer base as rows in an export, and seeing where customers were or how old they were took hours of spreadsheet work. As the bank’s Credit Analyst & AI Implementation Lead, I led the project and built four browser tools (three maps and a data-preparation tool). With them, both questions could be answered in the meeting that raised them, on a laptop with nothing to install and no backend.

Sponsor
A community bank in the Upper Midwest
Status
In use
Governing constraint
No backend, nothing to install
Reading time
9 minutes

In use The build is finished, so this page describes it in the past tense. The tools are still open on executives’ laptops, which is the best outcome this Engagement had and the one I had least control over.

What this Engagement shipped

  • 4 tools shipped

    Three maps and a data-preparation tool, each a single file that opens with a double-click.
  • 0 servers

    No backend, no login and no database, so every figure is computed on the computer that opened the file.
  • 3 silent defects

    A filter, a loader and an age check, each giving wrong numbers that looked right.

The Situation

Two questions came up often enough that they needed a real answer. Where are our customers, and what does our age profile look like? The core system exported rows showing an address, a balance and sometimes a date of birth, which are static numbers that say little on their own. Turning them into a useful picture meant hours of manual work in a spreadsheet, for an answer that might not be worth it.

When a question takes hours to answer, it gets asked only when somebody is already willing to spend those hours. Often people stop asking without realizing they should, or without knowing how to ask, and nobody keeps a record of the questions that never got asked. The first thing these tools changed was the cost of asking.

What I Did

The four tools split the work. One map shows where customers are and who they are. The second, the profitability map, shows the net annual income of each location, meaning one customer address and the accounts held there. The third compares two periods. The data-preparation tool turns a raw export into the one format all three maps read.

Each is a single HTML file that opens in a browser, with no server, no login, no database and nothing to install. Because nothing runs on a server, nothing needed approval before someone could try it. The only thing any of them downloads is map imagery, and no customer data leaves the computer. I built them in a deliberate order, starting with a version that only had to prove the idea could work.

From a demo map to a tool you can question

The first version was a map with two layers and dots at hardcoded coordinates, about five hundred lines of code with no real data in it. Its only job was to prove this could be done in a browser.

Because everything about this project was new to me, I met with someone who has years of experience in GIS (mapping and geographic data) to ask what good practice looks like. He pushed back on my format, because points on a map that do nothing say nothing. That led to a map that shades each area by its net annual income when zoomed out and shows individual locations as you zoom in.

The idea that turned the picture into a tool was simple. Select an area and see real numbers for it. Click to drop a circle, drag a slider to size it, and the panel reports what is inside, including the dollars, the record counts and the age distribution. Everything added afterwards applies that idea to something else. It also changed the demos, because people stopped just looking at dots and started arguing over the numbers for a place they had picked.

Hovering a location shows its arithmetic

The profitability map colors each location by its net annual income, which is what its loans earn at their own rates minus what its deposits cost. Its scale runs from red for a loss to green for a gain, and a grid view groups an area into cells the reader can size. A color alone gives the reader nothing to check. So hovering a location shows the subtraction instead, loan income minus deposit expense, and the net that results.

Loan income and deposit expense are the two figures a lender would challenge first, so any disagreement happens over a rate or a balance, where it can be settled.

Exhibit 1 What sits inside the circle, and its net annual income

Click or drag the map, or choose an area, to place the circle, then size it with the slider.

View the data
Synthetic locations, one per row: the area it belongs to, where it sits in miles east and north of the map’s south-west corner, what its loans earn and its deposits cost in a year, in dollars, the net of the two, and its holder’s age and age band. The location the readout opens on is L27.
LocationAreaMiles eastMiles northLoan incomeDeposit expenseNetAgeAge band
L01Area A5.59.458,0002,90055,1004435–49
L02Area A5.312.229,00028,7003006565–79
L03Area A6.410.341,00038,2002,8002618–34
L04Area A2.69.538,00027,30010,7002518–34
L05Area A4.111.9040,000−40,0003318–34
L06Area A5.311.6024,600−24,6001818–34
L07Area A4.59.9021,500−21,5006665–79
L08Area A7.610.97,00041,000−34,0003118–34
L09Area A3.910.911,00023,900−12,9006665–79
L10Area A6.311.550,0008,30041,7005950–64
L11Area A4.711.202,400−2,4008980+
L12Area A6.310.4019,700−19,7005150–64
L13Area A7.811.263,00017,80045,2001918–34
L14Area A4.210.5025,300−25,3008780+
L15Area A4.311.269,00020,30048,7005350–64
L16Area A6.28.079,00025,90053,1003118–34
L17Area B14.57.525,00025,900−9005150–64
L18Area B13.24.651,0008,90042,1006765–79
L19Area B15.56.052,00020,40031,6007765–79
L20Area B12.23.912,00034,800−22,8008880+
L21Area B11.85.0024,500−24,5001818–34
L22Area B15.25.070,00015,40054,6005950–64
L23Area B12.46.164,00018,50045,5008080+
L24Area B12.69.704,700−4,7004535–49
L25Area B15.46.721,00035,200−14,2003418–34
L26Area B14.13.8022,500−22,5006050–64
L27Area B13.56.984,20031,60052,6005850–64
L28Area B10.75.131,00010,40020,6003218–34
L29Area B12.93.6030,200−30,2004335–49
L30Area B14.27.6015,400−15,4005450–64
L31Area C5.74.9010,300−10,3003535–49
L32Area C6.82.687,00040,90046,1008380+
L33Area C3.14.282,00022,70059,3006450–64
L34Area C6.34.656,00027,10028,9005150–64
L35Area C4.83.8024,800−24,8001818–34
L36Area C4.74.5016,400−16,4007265–79
L37Area C6.35.775,0004,00071,0004635–49
L38Area C6.63.729,0003,40025,6006765–79
L39Area C5.95.151,00017,80033,2006665–79
L40Area C5.33.3017,000−17,0005650–64
L41Area D15.011.183,00039,50043,5005950–64
L42Area D16.211.108,600−8,6002418–34
L43Area D15.611.482,00024,30057,7006450–64
L44Area D18.29.911,0001,6009,4004635–49
L45Area D15.713.583,0009,60073,4002218–34
L46Area D16.111.765,00036,00029,0007465–79
L47Area D15.99.8029,300−29,3005550–64
L48Area D19.415.3014,900−14,9006050–64
Source: synthetic geography and synthetic accounts, invented for this site, because the real tool’s figures are the Sponsor’s business. Each location carries its dollar figures directly rather than the balances and rates the real tool multiplies. Dot color runs from rose (loss) to blue (gain), where the real tool uses red and green.

This is the original layout, the version I first presented to the bank’s executives and board. It shows where customers are and the net annual income of each area, which gave leadership a clearer view of its customer base than any export could.

How the data-preparation tool handles a broken export

Real exports arrive broken in small ways that affect the outputs, such as a title row above the headers, an address with no coordinates or an age column holding dates. The data-preparation tool handles all of them by one standing rule.

It corrects a problem only when the correction is unambiguous. A date of birth becomes an age, and a missing coordinate is filled in from a matching address in a file that was already cleaned. Otherwise the cell is left empty. Every correction is flagged where the person using the output cannot miss it. The cell is highlighted, the reason shows on hover, and a count of corrections sits above the preview. A tool that quietly improves its data looks exactly like one that quietly damages it.

Exhibit 2 Four corrections, each flagged, and two cells left empty

Choose Prepared, then hover or focus a flagged cell to read why.

View the data
One synthetic export, line by line: each cell as it arrived, what the preparation tool made of the cells it checks, and why. Line 2 is the export’s header row.
LineAccountAddressCoordinatesAgeCoordinates, preparedAge, preparedWhy
1Customer extract, run 2026-09-01BlankBlankBlankLine removedLine removedA title row above the headers, which is not a record.
3A-1001Address A45.12, −93.4047UnchangedUnchangedNone
4A-1002Address BBlank6245.10, −93.44UnchangedNo coordinates, so filled from the same address in a previously cleaned file.
5A-1003Address C45.15, −93.371958-03-14Unchanged68A date of birth in the age column, converted to the age on the day the export was run.
6A-1004Address DBlank39Left emptyUnchangedNo coordinates and no matching address in a previously cleaned file, so left empty rather than placed at a guess.
7A-1005Address E45.11, −93.423/4/24UnchangedLeft emptyA date with a two-digit year: born in 1924 or in 2024, and nothing in the row says which.
8A-1006Address F45.13, −93.391947-11-02Unchanged78A date of birth in the age column, converted to the age on the day the export was run.
Source: synthetic, an invented export of six accounts whose addresses are placeholders and whose coordinates describe no one. No real export is shown, because every row of one is a customer of the Sponsor.

I also built a prototype that puts an assistant on top of the same data. It runs a trained language model, Llama, inside the browser. A user can ask a question in plain words and go back and forth until it is the right question, or use the map as before. Either way, the customer data never leaves the computer.

What Changed

The map plots every loan and deposit account the bank holds and filters them by geography, customer age and loan type. A question about where customers are or how old they are used to take hours of spreadsheet work, and the map now answers it directly. The bank’s CEO, CFO, senior lending staff and board use it to decide where to focus outreach and to manage risk.

The map also turned up something nobody had seen before. The bank’s deposits are concentrated in its oldest customers, enough that it belongs in a conversation about funding rather than demographics. If those deposits leave, the bank has to replace that funding, usually at a higher cost. The finding is described here without figures because it is the Sponsor’s business, and only my own work product gets numbers on this site.

The outcome I am most confident in came as a byproduct of the work. This Engagement is why the bank offered me an ongoing role after it ended. The quality of these tools is why I have kept building more there, including the Credit Process Rebuild (Engagement 02).

What I Learned

That a check which returns a plausible answer on bad input is worse than one that refuses.

An age column sometimes arrives holding date serials, which are dates of birth stored as a count of days and never formatted as dates. The map recognized these and converted them, but only above a floor of twenty thousand days, which lands in October 1954.

Any customer born before then, roughly anyone seventy-one or older, failed the serial test. Their value then failed every other test and came back as zero. Zero is a real age band in the tool, not an error value, so those customers were neither dropped nor flagged. They were filed with the infants, and the band they were taken from was the very band the demographic finding was about.

It was the third defect of this kind, where a tool gave wrong numbers that looked right. An age filter counted records it had been told to exclude. A loader read every dollar value as zero. Together they taught me to check a tool’s inputs as carefully as its outputs, and to keep a record of every change made to the data.

Exhibit 3 Born before October 1954, filed as age zero

Slide the date of birth across the floor and watch the computed age switch between zero and the true age.

View the data
Dates of birth, oldest first: each one’s day serial, whether it passes the map’s serial test by being above the floor, the age the map computed from it, its true age on 2026-09-01, the date the synthetic export in Exhibit 2 carries, and the ten-year band that age belongs in.
Date of birthDay serialSerial test, floor 20,000Age the map computedAge on 2026-09-01Band it belongs in
1930-05-1511,093Fails09690–99
1935-05-1512,919Fails09190–99
1940-05-1514,746Fails08680–89
1945-05-1516,572Fails08180–89
1950-05-1518,398Fails07670–79
1954-10-0219,999Fails07170–79
1954-10-0420,001Passes717170–79
1955-05-1520,224Passes717170–79
1960-05-1522,051Passes666660–69
1965-05-1523,877Passes616160–69
1970-05-1525,703Passes565650–59
1975-05-1527,529Passes515150–59
1980-05-1529,356Passes464640–49
1985-05-1531,182Passes414140–49
1990-05-1533,008Passes363630–39
1995-05-1534,834Passes313130–39
2000-05-1536,661Passes262620–29
2005-05-1538,487Passes212120–29
2010-05-1540,313Passes161610–19
2015-05-1542,139Passes111110–19
2020-05-1543,966Passes660–9
Source: illustrative. The dates of birth are points on a calendar, one every five years and one a day either side of the floor, and belong to no one. The floor of twenty thousand days and the month it lands in are the ones stated above, and serials count days the way a spreadsheet does. The ten-year bands are drawn for this Exhibit, not taken from the tool, and no customer’s age or count is shown, because each is the Sponsor’s.
How it surfaced, and how it was fixed

I did not find it by reading the code. I was testing something else with a test file built from the shape of a real export. Two of its ten ages came back as zero when I knew what they should have been. The same check had been written twice a few weeks apart, and the two copies had drifted. The data-preparation tool had the range right the whole time.

The fix checks for a plausible age first and a date serial second, and any value that fits neither is now flagged instead of being filed at zero. Because the fix moved ages on screen in a tool executives were already reading, I told them before it went in. Numbers changing without warning is its own problem, even when they change from wrong to right.

Test data I invent agrees with my code by design, because I build it on the same assumptions the code was written under. A test file catches what the code does not expect only when it copies a real file, as this one copied a real export’s shape. Every edge case the data-preparation tool handles came from a real file that broke it first.

Where Else This Applies

Lowering the cost of a question. Any organization where an ordinary question costs hours of somebody’s time has a set of questions it has stopped asking, and no record of which ones.

Making a visual show its arithmetic. Heat maps, scores, ratings, risk tiers and red-amber-green dashboards all hand the reader a conclusion with the working removed, and each is believed until the first person checks one.

Showing every correction. Any process that cleans data on its way into a decision has to choose between staying silent about what it changed and showing it. That covers claims intake, survey responses, meter reads and system migrations alike.

All six Engagements Ask me about this one