← All projects

Positioning Lab

In development

Helps an independent practitioner work out who their customers actually are, and refuses to name one it can't back with evidence.

FreelanceWeb AppPythonClient Work

At a glance

  • Outcome: A practitioner opening a practice forms a short list of credible customer hypotheses, tests whether those customers are actually reachable in their real local market, picks one with a human in the loop, then measures the first 90 days against their own books.
  • Status: In development, with the evidence workflow running end to end against recorded source data. Built across 23 working days since mid-July 2026, with 76 decision records and roughly 1,900 tests behind it.
  • Role: Solo, under contract through my LLC.
  • Stack & libraries: Python web app on Postgres with a background job queue. Organization scope is bound to the database session, so a query that forgets it fails instead of returning another practitioner’s rows.
  • Source: Private client work. This page is the case study.
  • Limitations: No practitioner has run the full 90 days yet. It can’t name a winner, only a candidate worth testing. And the reach data it depends on publishes ranges wide enough that in four of five recorded markets, the candidates tie.

The problem it’s for

Someone opening an independent practice has to guess who their customers are.

The first version of this was a drive-time and demographics tool, and it worked. What it found is the reason I replaced it wholesale. Golf, desk work, cycling and postpartum hypotheses all collapsed into the same age, income and population calculation, even though reaching those four customers takes four entirely different channels. The number was fine. It just wasn’t an answer to the question anyone actually had.

So the rebuild changed the question from how big is this market to can this practitioner test this customer here. The product’s central constraint is a negative one. It isn’t an oracle. It can identify the best candidate to test, and it can only name a winner once there are real outcomes to compare.

Provenance says where a number came from. It doesn’t say what it’s about.

That gap is the thing this system had to invent a noun for.

A subject register holds it separately. For each customer concept, one row records what the ad platform, each search network and the facility data each call it. A person writes those correspondences; nothing infers them. “Nobody asked this source” stays a different answer from “this source was asked and has no word for it.” A concept with no row has no source names at all, which is what stops free text becoming something you can point an ad platform at.

The bill for getting this wrong arrived on schedule. The first live recorder resolved “runners” by taking the first row the ad platform’s catalog returned. That row was Pop music of the 1980s. Running came fifth. It filed five audience measurements against an 80s-pop audience before anyone noticed.

The application already had a rule against first-result matching. The tool feeding it didn’t, because nobody had written the rule down on the acquisition side. The five recordings are kept, moved to a folder that says exactly what they are and that nothing may attribute them to runners or to any market.

Matching on the name wouldn’t have saved it either. “Runners” is no more equal to “Running” than “golfers” was ever equal to “Golf.” That’s why a person writes the correspondence.

The most recent decision handles the cost of that rule without giving it up. Writing every correspondence by hand is worst exactly where the product is meant to help, because search phrasings vary by city and by month and nobody can hand-write the set that will surface in a small metro. So a model may propose a correspondence and may never author one. A proposal has to pick from the names the source actually returned, so it can’t invent an audience. Picking nothing is a legitimate answer, and it’s still refused as a register entry, because concluding that a catalog has no word for something on the basis of one search is a finding about the search. Accepting a proposal takes a person’s name and a date.

The other refusals

Every figure carries what it counts, not just where it came from. Search events, ad accounts, residents, households, facilities, claim rates and the practitioner’s own revenue are sixteen different kinds of number, and combining two of them raises an error instead of producing a score. Two search networks count as two of the sixteen, because a phrase one network has volume for isn’t evidence the other does.

Gating is separate from ordering. Each candidate lands in exactly one bucket: worth testing, not reachable, not evidenced enough, or waiting on vocabulary review. Only the first bucket gets ordered, and it gets ordered once per lane because the lanes count different things. There’s no combined ranking and no score anywhere. A candidate that failed a gate isn’t a low-ranked candidate.

The ordering often can’t separate anything, and it says so. The reach source publishes every estimate as a range fifteen percent wide, which turns out to be presentation rather than real uncertainty. So two audiences within fifteen percent of each other aren’t distinguishable by that source at all. Across five recorded markets, four can’t tell golf from running from cycling. That result is written into a test, so the day a re-recording changes it a test says so instead of a screen quietly reordering. A tie is the ordinary outcome here, not a broken one.

What the discipline costs

Twice the demo screen rendered no ordering at all.

Once because the only candidate carrying a real measurement was the practitioner’s own wording, which sits in the waiting-on-review bucket by design. Once because two activities had a reach measurement but no recorded search volume, so they never reached the bucket that gets ordered.

Both times the shortcut was available. Fabricate a reading and the screen looks finished. Both times the fix was a decision instead: admit the wording, or go record the missing evidence.

Building the vocabulary and the refusals before any feature was a slower start, and it has already paid for itself twice. A recording tool filed five measurements against the wrong interest and the application refused them, because the rule was already written down. Adding a fourth source’s vocabulary surfaced a figure leaking across sources that no screen showed.

The original drive-time build is kept at a tag, and nothing comes back from it out of convenience.