I’ve launched products with zero signups, blogs no one reads, and open-source projects that never got a single star.
I keep building things nobody wants. It’s like cutting puzzle pieces by hand and then tossing them at the puzzle, hoping one will fit—like an idiot.
When a piece actually fits, that’s product–market fit (PMF). I know this. But the thought of studying PMF makes me want to go full Dahmer.
So instead we’re just going to simulate PMF using an agent-based model (ABM) in Python. Coding is fun and it forces us to learn the concept without the constant urge to step in front of a moving train.
Agent-based modelling
An agent-based model (ABM) is a computational model for simulating the actions and interactions of autonomous agents. This is a good way to understand a system where different agents interact with each other.
The simulation runs in steps, each new step is a continuation of the previous state and represents an undefined period of time.
Example:
Step 1
Agent_1 talks to Agent_2
Agent_1 gets mad at Agent_2
Step 2
Agent_1 is still mad at Agent_2
Agent_2 brings Agent_1 a cake with nuts as a peace offering
Step 3
Surprise: Agent_1 is allergic to nuts and DIES.
Step 4
Agent_1’s wife sues Agent_2 for negligence
Okay, horrible example—but you get the idea. Agents interact, and the state evolves from step to step.
Simulations are a simplification of reality. We are not trying to capture every detail—if I could do that, I’d just simulate my way through life, become filthy rich, and laugh at everyone else.
The real goal is much simpler: to understand product–market fit by breaking the concept into smaller, manageable parts.
Definition of PMF
“Product-market fit means being in a good market with a product that can satisfy that market.”
We focus only on the actual fit between product and market. We ignore factors like brand loyalty, market exposure, and pricing.
We also assume customers have perfect information, they know about every product and its features at any given time.
⚠️ Are you a Haxxor?⚠️
This article uses simplified code examples to illustrate the concept of product–market fit. You can also explore a working simulation yourself here.
Creating a virtual market
The OOP paradigm is well-suited for designing an agent-based model, since we are explicitly working with entities. We only need three types of objects: Customer, Product, and Market.
Customer
Each customer has a randomly generated list of integers that represents their preferences.
A customer can search for products that best match their preferences.
A customer may leave empty-handed if no product matches their preferences well enough.
Product
Each product has a randomly generated list of integer that represents their features.
Each feature may or may not align with one or more customers’ preferences.
Products fail if they don’t eventually reach product–market fit (PMF).
Market
The market holds a list of different customer and product objects.
The market matches customers and products by comparing preferences with features.
New customers and products may enter the market at the start of each step.
Understanding preferences
When you buy a car, you don’t just pick any car. You look for one that matches your preferences.
Let’s say you want a black electric sedan. We can represent your preferences with a list of numbers:
Preferences = [7,1,10]
Each item represents one preference. If instead you wanted a dark gray electric sedan, it might look like this:
Preferences = [5,1,10]
The first item changed from 7 to 5, meaning black shifted to dark gray. The distance between 7 and 5 is only 2, showing that black is very close to dark gray.
Now imagine you’re at the dealership, and the salesperson shows you a bright yellow gas-powered sedan. Its features might look like this:
Features = [14, 20, 10]
But you wanted [7, 1, 10]. The gap between your dream car and that yellow nightmare is way too big.
We measure this gap using the L1 (Manhattan) distance, which just sums the absolute differences:
7 - 14 = 7
1 - 20 = 19
10 - 10 = 0
Total distance = 25.
There must be cars out there with a much smaller distance, meaning they’re a much better fit for you.
Then you see this beauty: car [8,1,10]. The almost-black electric sedan you always dreamed about. Sure, it might be average—but so are you.
Lets do the math on this one
7 - 8 = 1 (absolute value)
1 - 1 = 0
10 - 10 = 0
The total distance between your preferences [7,1,10] and that car's features [8,1,10] is only 1, which makes it the almost perfect car for you.
def l1_distance(a, b):
return sum(abs(x - y) for x, y in zip(a, b))
Customers
Remember two sentences ago when I implied that you are average—ouch! I was only preparing you for the concept of the preference landscape, a theoretical space where customers fall into clusters based on their preferences.
In your case you would be located near people with similar preferences like this:
Each of these dots represents a customer which is basically just an list of preferences. We create these lists using the make_blobs function from sklearn:
from sklearn.datasets import make_blobs
prefs, ids = make_blobs(n_samples=100, n_features=3, random_state=42)
X returns a list of lists: [[2,4,-2], [4,-12, 7], … ]
Y returns a list with cluster ids: [1,1,0,2…]
We then generate a set of customer with those preferences:
for i, p in enumerate(prefs):
customer = Customer(_id=i, preferences=p)
Market.customers.append(customer)
Products
The job of any good product is to position itself as closely as possible to customer preferences. The feature landscape can be thought of as a layer on top of the preference landscape, where products are placed according to their features.
Each product has one set of features which we generate randomly using the max and min value from the customer preferences.
min_pref = min(all_preferences)
max_pref = max(all_preferences)
n = SETTINGS.nr_of_preferences
for i in range(SETTINGS.starting_products):
fe = [random.randint(min_pref, max_pref) for _ in range(n)]
product = Product(_id=i, features=fe)
Market.append(product)
The above code generates X number of products with a list of features that matches the length of the number of preferences each customer has.
Matching customers and products
We already know that the customer will choose the product with the lowest distance between he/hers set of preferences and the products in the market.
We don’t want customers to settle for the lesser of two evils. That’s why we set a reasonable maximum distance, so they only see products that truly fit their needs.
if l1_distance(product.features, customer.preferences) > settings.threshold:
return
else:
consider.append(product)
Improving features
When a product is first introduced into a market, it is rarely perfect. Combined with the fact that markets evolve, this forces the product to gradually adjust its features over time.
We model this by calculating the mean of the nearest customers and slowly shifting the product’s features toward that mean at each step.
def improve_feature(features, nearest_customers, alpha=0.2):
mean = mean_preferences(nearest_customers)
j = random.randrange(len(features))
features[j] += (mean[j] - features[j]) * alpha
return features
The function improve_feature
selects one random value from the feature list and moves it toward the average preference of the nearest customers. The parameter alpha
controls how strongly that value shifts.
Creating new features
A product that enters a market without nearby customer clusters needs to pivot quickly by performing a random walk within the feature landscape trying new features.
Products repeat this process until they either land near a customer cluster or die after a set number of unsuccessful attempts—like a madman wandering in circles.
def create_feature(features, min_pref, max_pref):
j = random.randrange(len(features))
features[j] = random.randint(min_pref, max_pref)
return features
Copying features
When a product reaches product market fit, new products try to imitate that success by copying a subset of its features.
A new product samples X features from the set of top performers with the least competition, then applies a small jitter to each copied feature to avoid exact duplication. This imitation only applies to products that have not yet entered the market
def copy_feature(features, competitor_features, jitter=1):
features = features[:] # copy so we don’t overwrite input
j = random.randrange(len(features))
features[j] = competitor_features[j] + random.randint(-jitter, jitter)
return features
Killing products
If a product fails to fit within a market after X steps, it is considered dead. This happens when sales decline significantly over an extended period or when product–market fit is never reached—an unsuccessful madman’s jump.
def kill_product(product):
if product.stats.last_x_steps(SETTINGS.steps_until_death):
product.alive = False
Adjustable parameters
Number of customers (size of market)
The total number of customers in the customer landscape determines how many products can realistically fit within a market.
Number of customer clusters
In fashion, there are many clusters that sit close to each other. A company making jeans for one target group can often satisfy other jeans loving people with a few changes to their design.
In contrast, the bottled water market has very few clusters, and they are close together. Most people don’t have strong preferences when it comes to plain drinking water
Cluster Standard Deviation
This parameter adjusts how spread out the clusters are. A higher standard deviation means less distinct clusters with more overlap.
In practice, high spread represents markets where single products can reach many different customers.
Number of preferences
The total number of preferences determines the distance between a product’s features and customer preferences.
The more preferences there are, the more complex a product tends to be. A bottle of water might only have one or two relevant features:
Bottle_X.features = [1, 1] ← Water, Plastic bottle
Bottle_X.features = [1, 2] ← Water, Glass bottle
A car, on the other hand, combines aesthetics, brand, and functionality, producing an almost fractal variety of possibilities:
Car_X.features = [2, 6, 7, 8, 2, 5 …] ← Model, color, top speed, interior, size …
Distance threshold
What distance does between preferences and the product features is the customer willing to accept.
if l1_distance(product.features, customer.preferences) > distance_threshold:
return
else:
consider.append(product)
This can be translated to “customer pickiness” and affects how far a product can be positioned from different customer clusters and still generate sales.
Running the code
Code 👉 https://github.com/obergxdata/simulate_pmf
Clusters over time
Lets take a look at an example simulation with 500 steps.
Step 1
We are running the simulation with starting_products = 1 so the first step spawns one new product (1) near two clusters.
Product 1 acquires 6 customers which are in the range of the distance_threshold. To acquire more customers, Product 1 needs to move closer to the center.
Step 250
It looks like Product 1 has positioned itself more beneficial now but at the same time competitors has entered the space, Product 6 is very close and is hogging some of the sales by being a better fit for some customers at the top of the clusters.
Step 500
It’s a sad day—or perhaps I should say, a sad step. Product 1 is gone. He tried to pivot, searching for new clusters through innovation, but his move toward the top-left proved fruitless.
Was it greed? Was it survival? We may never know.
Now, Product 9 and Product 6 have moved in and dominate the position he once held.
And here we see sales over time, with the gray dotted line marking products that have died. This cycle repeats until the system settles into an equilibrium—eventually forming a stable market of six surviving products.
Final words
Agent-based modeling is a powerful way to understand complex systems. There’s something almost magical about building a simulation and watching patterns emerge from simple rules.
Beyond research, these simulations have clear commercial potential. For example, in e-commerce, one could simulate how changes in churn, pricing, visitor flows, or ad spend ripple through the system and affect the bottom line.
Why do these things excite me? Maybe I just need to get some friends.