We knew customers were coming in, we just didn't know where from
A customer saw an ad and left their details, then got a few emails, then saw an ad again, and a month later a colleague talked to them and closed the subscription
A simple question: where did this customer come from
The answer wasn't anywhere, because every step of their journey lived in a different system, and nothing connected those systems to each other
One customer's journey
Here is an example close to what happened every week
Six steps, and each one saved in a different place
The ad platform knows the first and fourth steps, the email system knows the third, and the CRM knows the fifth and sixth
And there is no single place that keeps the whole sequence
So when someone asks where this customer came from, every answer is partly right and none of them is complete
Where the customer's source kept getting lost
It wasn't one cause, it was three things happening at once
First: the campaign tag was getting lost on the way
Every ad carries a small tag that says where the visit came from. Its technical name is UTM, and it is supposed to travel with the visitor and settle in their record inside the CRM
Because of a database issue and a broken link between the website and the CRM, this tag never arrived, so the customer entered the system with no source
And once the source is lost at that moment it never comes back, because a month later nobody remembers where each name came from
Second: a lot gets lost when things are logged by hand
When the source doesn't arrive automatically, everything depends on manual entry, and manual entry on a busy day leaves gaps
A call happens and doesn't get logged, a follow up happens and the system knows nothing about it
And that isn't anyone's fault
Any system that asks people to log every step by hand on a busy day ends up the same way
Third: the person who tries the product isn't the person who pays
In B2B sales, the person who signs up for the trial is usually an employee or an operations manager, and when the company moves to a paid plan, the payment comes from the finance team's email
omar@company.comTwo different people
accounts@company.comThe system sees two different emails and treats them as two unrelated people, even though it's the same company
The only shared part is the company domain
What we had and what we didn't
Fixing the issue at the root needed time from the product team and a spot on a long priority list, and waiting meant more months of spending decisions based on guesses
And there was no budget for a ready made tool to connect the systems
So the question became: what can we do with what we have right now, in days, not months
The idea, simply put
I started from the end instead of the beginning
Instead of tracking every visitor and waiting until they buy, I took the list of people who had actually paid, then went backwards looking for a trace of each one in every available source: their mobile number, their email, their company domain, their company name
And instead of a yes or no answer, I gave it a score
This is conclusive evidence, this is strong evidence, this is possible evidence, and every match comes out with its confidence score
The technical name for this work is Attribution, which simply means knowing where a customer came from and what evidence proves it
The rest of this page is how it worked, and what we found
Where we looked for customers
Five places, and each one knows the customer in a different way
The ad platforms keep what the person typed into the signup form, the email base keeps company records with a business name and work email, and the subscription records know which company paid
Size of each source
Counts only, no names and no personal data
The three platforms together hold only 6,627 records, small next to the email base, but they are the only proof that a customer saw an ad
How we know it's the same customer
There is no single ID shared across all these systems, so I ranked the evidence from strongest to weakest
We look for the strongest proof first, if it isn't there we move to the next one, and with every match we write down how confident we are
The evidence ladder and confidence scores
We start at the top, and once we find a match we don't go lower
And if we find no evidence at all, the customer stays with no source, and we don't credit any channel just because it was running at the time
Why cleaning was half the work
The same number can be written five different ways, so nothing is compared as is
Numbers are stripped of spaces, symbols and country codes and cut down to the last nine digits so different formats of the same number match, emails are made lowercase with spaces removed, and company names are split into words with common generic words removed, like company, establishment and trading, because keeping them makes any two names look alike
And why confidence scores are different
A mobile number and an email are personal identifiers, and a match on either one means the same person, so I gave them 100%
A company email domain proves the company, not the person, and since a domain belongs to one company I gave it 95%, while excluding public domains such as gmail and yahoo because a domain used by millions of people proves nothing
Company name is the weakest evidence because similar doesn't mean identical, so I required three or more words in common and gave it 85%, and that number is my way of saying clearly that this kind of evidence can be wrong more often
Which level produced the matches
How 2,182 successful matches were spread across the ladder levels
More than 64% of the matches came from conclusive evidence, an email or a mobile number, and the rest is inference we know the strength of
There are more matches than attributed records because one record can match more than one source, 2,182 matches spread across 1,801 records
We found where 47.7% of customers came from, with proof
Out of all paying customers, we traced 47.7% with evidence, and for 52.3% we didn't find enough
Attribution status across records
One run on 3,779 records
Before this work the number was zero, and every answer about where a customer came from was a guess
Now we know 47.7% with evidence, and we also know clearly what we don't know
And for the customers we did trace, most rest on conclusive evidence, not on inference
Confidence in attributed records
How 1,801 records are spread by the highest confidence score they reached
71.7% of attribution rests on a mobile or email match, so about seven out of ten you can defend in any meeting with confidence, and the rest is probable, but we know how big it is and we say so
And why we didn't make up an answer for the rest
52.3% of records came out with no source, and it's easy to read that number as failure, but it means much less than that
All it means is that we didn't find enough evidence to link this customer to a source
It doesn't mean they didn't come from marketing
The clearest possibility is that the customer signed up on one of the channels with a personal email and personal number, then signed up for the product with a work email and office number, so both records belong to the same person and nothing in the data links them
That is possible and logical, but it stays a possibility, and turning it into attribution means making up a result
So we decided to leave it as it is, with no source
We don't split it across channels and we don't credit it to whichever channel was spending that month, because that would make the numbers look better than they are
And this gap shrinks as data collection at the point of entry gets better, not as the guessing gets better
How many places a customer passed through before buying
The first thing that showed up after connecting the data is that one customer can exist in more than one place, and that never showed up in any earlier report, because every report picked one source per customer
Presence of each source in attributed records
Share of records linked to each source, out of 1,801 records
The shares don't add up to 100 because one record can show up in more than one source, and the email base is the biggest common factor because of its size and age
How many sources a customer showed up in
Depth of touchpoints across 1,801 attributed records
15% of attributed records showed up in more than one source, and 2.1% showed up in all four sources
This is the lowest estimate
A customer who showed up in two sources may have passed through five, but we only have evidence for two
Three things people mix up
This distinction is the most important thing I took from this work, and it's very simple once it's written down
First, that the customer showed up in a source, which only describes what's in the data: their name, number or email exists in that channel's records
Second, that we have evidence linking them to that source, and this is where match type and confidence come in
A mobile number match is one kind of evidence, a similar company name is weaker evidence, and the difference is written next to every customer instead of getting lost in one word
Third, that the source caused the subscription, and we don't prove or claim that
A customer showing up in an ad campaign and in email doesn't tell us which channel drove the decision or which came first
What we did answers the first and second, and the third needs the dates of every event in order, which wasn't available
What changed in the daily work
The amount of data didn't change and neither did the number of channels
What changed is that everyone now opens the same table and sees the same answer
- The budget now goes to channels where paying customers actually show up, not by guesswork
- Every channel is now measured by customers who paid, not by its own dashboard, and that made a big difference for email because its impact doesn't show up as a click
- Email can finally be judged, because we can see whether the people we email actually subscribe
- The customer journey got clearer, as 15% of customers went through more than one channel and used to be counted under just one
- The team now knows what it has evidence for and what it doesn't, instead of both getting mixed into one number
The limits of this work
So the numbers aren't read as bigger than they are, here are the honest limits of what we did
What these numbers don't say
- 52.3% of records weren't linked to any source, and that is an absence of evidence, not a failure rate
- Matching by company name is inferred by nature and covers 15.5% of attributed records
- Correlation isn't causation, and nothing here proves that a specific channel produced a subscription
- The numbers come from one run over a fixed period, and can't tell you a trend over time
- The campaign tag still doesn't reach the CRM, and that is what limits accuracy today, not the method itself
- There is no customer or company name on this page, only counts and shares
If this were my company, this is what I would ask to improve
The engine solved the urgent problem in days with no budget, and that was the goal
But anything built fast leaves things worth improving, and this is what I see when I go back to the code
Building an index that links each word to the records that contain it cuts this down to minutes
The cleaner way is dated file names and an explicit structure check that stops with a clear error
And the most important fix stays completely outside the engine: getting the campaign tag into the customer record from the first moment, and logging the date of every touchpoint
Then talking about first touch and the order of the journey becomes possible, and fewer customers end up with no source in the first place
Tools and code
All the work happened inside Jupyter Notebook, with one run that reads the sources, does the matching and writes the final table
Processing with pandas for reading, cleaning and matching, NumPy for numeric work, plus the standard Python libraries glob, os and time for file handling and timing the run
The email base is read as Parquet because it's too big for a regular spreadsheet format, customer records are read from an Excel export, and the platform tables are pulled straight from their source
I used ChatGPT and Claude while writing the code, to speed up the writing and review edge cases
Defining the problem, designing the evidence ladder and confidence scores, and the exclusion rules were decisions I built from understanding the data itself
Strict Hierarchical Attribution Engine
Matching logic, confidence scores, rules for cleaning numbers and names, and excluding public domains
The repository only holds the engine logic, with no customer data, source files or access keys
Tags for this case study
The solution didn't come from a new tool or a budget, it came from defining the problem precisely and then building the smallest thing that answers it
Related case study: How we moved the marketing budget to the decision window before Ramadan and sales grew without a big jump in spend
The problem wasn't demand, it was timingIf you know how many customers you get but not where each one came from, the problem is usually in how your systems connect, not in your channels
In one session we review your sources, your touchpoints and what actually reaches the customer record, and we find where the chain breaks and what can be proven today before any new spend
Book a diagnosis session 60 minutes, fully credited to any later engagement Or read the measurement and attribution service details