Case studySoftware and cloud services

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

IndustrySoftware and cloud services
Delivery timeThree working days
BudgetZero, using the tools we already had
My roleSpotted the problem, designed the engine and connected the sources
0contacts we compared against
0paying customers we checked
0of them we traced to a source, with proof
0working days from idea to result

One customer's journey

Here is an example close to what happened every week

1Saw an adOn an ad platform
2Left their detailsSignup form
3Got emailsEmail campaign
4Saw an ad againRetargeting
5Got a callAbout a month later
6Subscribed and paidPaid subscription

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

Email used for the trialomar@company.com
One company
Two different people
Email used for paymentaccounts@company.com

The 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

510,785contacts in the email base, the reference everything is compared against
4,184records from Meta
2,009records from TikTok
434records from Snapchat
3,779paying customers, the people we wanted to trace back

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

1 Mobile numberAfter cleaning the number and standardizing its format 100%
2 EmailAfter making it lowercase and removing spaces 100%
3 Company email domainThis is what solves the problem of different emails inside the same company 95%
4 Company nameThree or more words in common after removing generic words 85%

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

Faster name matchingName matching compares every record with every name in every source, which is why one run takes about forty minutes
Building an index that links each word to the records that contain it cuts this down to minutes
Batch processing instead of row by rowThe first three levels depend on whether a value exists in a set, and that is a batch operation in pandas that doesn't need to go through records one by one
Measuring the accuracy of level fourThe 85% is a judgment call based on understanding the data, and turning it into a measured number needs a manually reviewed sample that shows how many name matches were actually right
Weighting rare wordsA three word overlap treats a rare word and a common word the same, and giving rare words more weight raises accuracy without making the rule stricter
Reliable input filesThe engine picks the newest file by modified date and looks for the header row in the first rows of the file, and both work until the export format changes
The cleaner way is dated file names and an explicit structure check that stops with a clear error
Scheduled runs and a shared outputThe run happens on demand, and turning it into a regular run that writes to one table read by marketing and sales makes it something that runs every day instead of a report pulled on request

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

Jupyter NotebookPythonpandasNumPyParquetExcelChatGPTClaude
</>

Strict Hierarchical Attribution Engine

Matching logic, confidence scores, rules for cleaning numbers and names, and excluding public domains

Open the repository

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 timing

If 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
عرض