A Toy Model for Market Dynamics: Building a Agent-Based Frameworks
Introduction
I am applying for a job where the primary task involves working on and implementing agent-based simulations. It has been a while since I last worked with object-oriented coding, so I thought I would brush up on my basic skills by creating a small project based on a simple market simulation.
My background is primarily in mathematics, economics, and statistics. At the computer science department at KU, there don’t seem to be many courses that focus on agent-based simulation approaches. However, I recall attending a talk organized by the group “Kritiske Politter” a long time ago. The speaker was a professor from the physics department who, as a hobby, created simulations of markets. His main critique of traditional economics was the reliance on steady-state assumptions, which are quite common in the field. He argued that natural systems often exhibit fluctuations, even when they are in balance.
Rabbits and Foxes
To give an example: If one simulates an island with rabbits and foxes, there will be fluctuations in the populations. When there are many rabbits, the fox population increases, which then leads to a decline in rabbits. This, in turn, causes a decline in foxes, allowing the rabbit population to grow again, and the cycle continues. In this case, a perfect equilibrium is unrealistic; instead, the equilibrium is a range in which the number of foxes and rabbits alternates over time.
I find this field fascinating. One of my primary concerns is not just creating rational agents but making them realistic. In my economics courses, a lot of different techniques and models were presented to answer central questions. The argument often was that if these models capture the essence of some problem, they are useful. To some extent, I agree with this argument. However, I disagree with making assumptions solely for the purpose of solving the steady state of a system. This is why the agent-based approach is so interesting to me—it resembles the way physicists would approach a problem.
This Project
In this small project, I will attempt to create a basic framework for simulating agents. When I was first introduced to object-oriented programming, we worked on something similar to the rabbit-and-fox scenario.
The Code Will Be Written in Python Since the company appears to use Python, I will carry out the simulation using this language.
If I were building an extensive simulation framework for agents, I might consider using C++ due to its performance advantages. The key idea behind agent-based simulations is the interaction between agents, which makes it an iterative process. While Python may not be the fastest for such loops, parallelized programming could improve performance.
Although I have experience with parallelization in R, I imagine Python offers more sophisticated methods. For instance, dividing agents into clusters that interact and processing these simultaneously could be an interesting approach. Learning and implementing this would be a valuable experience, although it exceeds my current skill level. In R, parallelization often involves copying the entire dataset to avoid writing conflicts; it would be exciting to explore more efficient solutions.
Summary
In summary, agent-based approaches are incredibly interesting and would be enjoyable to work on. I believe this problem provides a great opportunity to refresh my object-oriented programming skills. These simulations rely on Monte Carlo methods as their underlying mathematical approach, which makes them challenging to test.
A Simple Simulation of Markets
There is likely a lot of theory on agent-based simulations, but since I am new to this field, I’ll start with a simple simulation of a market. This is a toy example, so if you’re reading this and think some deep theoretical insights can be drawn from this model (a common approach in economic modeling), remember: I HAVE NO DATA.
During my microeconomics courses, I worked extensively on building theoretical models and creating narratives or conclusions based on them—without ever looking at real data. While it’s relatively easy to create models of agents, building realistic agents is far more challenging.
If you are interested in examples of non-rational behavior in humans, I recommend reading Nudge or Predictably Irrational. The latter describes experiments in detail, which I personally enjoy.
Since this job is based in Copenhagen, I’ll simulate “Strøget,” the historic shopping district in the city center.
The Environment (Strøget)
Since the company is located in Copenhagen, I thought it would be fun to use the city center as the topic of this exercise.
If we limit Strøget to the area between “Storkespringvandet” and the entrance near City Hall, we can imagine it as a long corridor with shops on both sides. To simplify, we’ll assume there’s one shop at each location along this corridor.
Thus, the environment can be represented as a long chain of shops. Agents can start from either end and pass through the corridor.
The Shopper
From personal experience, it seems that nobody goes to Strøget to buy necessities—it’s more about purchasing nice-to-have items and doing light shopping. I’ll assume people arrive at Strøget with a certain amount of money (cash) they want to spend.
I created some preference classes, such as Food, Clothing, and Books. By making these preferences into classes, I can tailor purchase patterns for each category.
Food: This is consumed only once, meaning if someone buys food, they won’t purchase it again. Cash: The shopper’s cash balance decreases with each purchase. Position: Tracks where the shopper is located in the chain of shops. Presence: Indicates whether the shopper is still on the street. When a shopper encounters a shop that matches their preferences, they will buy something with a given probability—but only if they can afford it.
The Shop Agents
Shop agents will offer products at a given price in specific categories. To keep things simple, each shop offers one product in one category.
Simulations The simulation is managed via the Street class.
To simplify, shops are ordered by their placement along the street. I haven’t implemented checks for errors in the ordering since this is just a toy example.
The simulation progresses with a tick function, allowing the system to update step by step.
Overall Overall, I like this construction. Instead of storing positions in a variable, I could have moved the object itself or used pointers to save computational costs related to copying.
Implementasion
In this section, I have written the code for the implementation.
Classes
Below, my classes and the central construction can be found.
Code
import randomclass Shop:def__init__(self, placement: int, category: str, price: int):""" Represents a shop in the market. :param placement: Position of the shop along the market. :param category: The type of items sold by the shop (e.g., 'Food', 'Books'). :param price: The price of the items sold. """self.placement = placementself.category = categoryself.price = priceself.units_sold=0def sold_unit(self):self.units_sold +=1def turnover(self):returnself.units_sold*self.price"""The belov class represent prefference a cosumer could have"""class food:def__init__(self,type,buy_prop,have_eaten=False,units_bought=0):self.type=typeself.buy_prop=buy_propself.have_eaten=have_eatenself.units_bought=units_boughtdef will_buy(self): return_obj=Falseif(self.have_eaten==False): return_obj= random.random()<self.buy_propif(return_obj==True):self.have_eaten=Trueself.units_bought+=1return (return_obj)class cloth:def__init__(self,type,buy_prop,units_bought=0):self.type=typeself.buy_prop=buy_propself.units_bought=units_boughtdef will_buy(self): return_obj=random.random()<self.buy_propif return_obj==True: self.units_bought+=1return return_objclass book:def__init__(self,type,buy_prop,units_bought=0):self.type=typeself.buy_prop=buy_propself.units_bought=units_boughtdef will_buy(self): return_obj=random.random()<self.buy_propif return_obj==True: self.units_bought+=1return return_objclass Shopper:def__init__(self,placement: int,direction,cash: float,preferences: list):""" Represents a shopper in the market. :param placement: Current position of the shopper. :param direction: Direction of movement (-1 for left, 1 for right). :param cash: Amount of money the shopper has. :param gender: Gender of the shopper (used for preferences). :param preferences: Dictionary of categories and buy probabilities. """self.placement = placementself.direction = directionself.cash = cashself.preferences = preferences self.has_eaten =Falseself.has_eaten_dessert =Falseself.shoper_pressent_at_market=Truedef update_position(self):"""This function update the postion of the shopper and updates if their are shoper pressent at market"""self.placement=self.placement+self.directionreturndef visit_shop(self, shop: Shop):""" Interact with a shop and decide whether to buy. :param shop: The shop to interact with. """# find if shope is in the prefrence did_buy=Falsefor index inrange(len(self.preferences)):if(self.preferences[index].type==shop.category):if(shop.price<self.cash): did_buy=self.preferences[index].will_buy()if (did_buy==True):self.cash=self.cash-shop.price shop.sold_unit()self.update_position()return did_buyclass shopping_street:def__init__(self,list_of_shops,list_of_shopers):# List of shoops should be ordere by placementself.list_of_shops=list_of_shopsself.list_of_shopers=list_of_shopersself.min=list_of_shops[0].placementself.max=list_of_shops[(len(list_of_shops)-1)].placementdef run_tick_shopper(self,index):"""This function will run one iteration of the simulation for a given agent"""if (self.list_of_shopers[index].shoper_pressent_at_market)==True:self.list_of_shopers[index].visit_shop(self.list_of_shops[index])if(self.min<(self.list_of_shopers[index].placement) orself.list_of_shopers[index].placement<(self.max)):self.list_of_shopers[index].shoper_pressent_at_market=Falsedef run_tick(self):"""This function will run one iteration of the simulation for all agents"""for ind inrange(len(self.list_of_shopers)):self.run_tick_shopper(ind)def get_unit(self): unit_sold=0for ind inrange(len(self.list_of_shops)): unit_sold +=self.list_of_shops[ind].units_soldreturn(unit_sold)def get_total_turnover(self): turnover=0for ind inrange(len(self.list_of_shops)): turnover +=self.list_of_shops[ind].turnover()return(turnover)
Unit Testing
Below, my Unit test, can be seen
Code
# unit testimport unittestdef test_Shop_class(): test_Shop = Shop(2,"men cloth",100) test_Shop.sold_unit()assert(test_Shop.units_sold==1)assert(test_Shop.units_sold*test_Shop.price==test_Shop.turnover())returntest_Shop_class()def test_cloth_class(): test_cloth = cloth(type="men cloth", buy_prop=1)assert test_cloth.will_buy() ==Trueassert test_cloth.units_bought==1returntest_cloth_class()def test_book_class(): test_book = book(type="book store", buy_prop=1)assert test_book.will_buy() ==Trueassert test_book.units_bought==1returntest_book_class()def test_food_class(): test_food=food(type="Fastfood",buy_prop=1)assert(test_food.have_eaten)==False# Should buy since buy prop = 1 assert(test_food.will_buy()==True)#assert(test_food.units_bought==1)# have eaten = True assert(test_food.have_eaten)==True#should not buy since have eatenassert(test_food.will_buy()==False)returntest_food_class()def test_Shopper_class(): test_Shop = Shop(2,"men cloth",100) test_food=food(type="Fastfood",buy_prop=1) test_cloth = cloth(type="men cloth", buy_prop=1) test_Shopper=Shopper(placement=2,direction=-1,cash=1000,preferences=[test_food,test_cloth])assert test_Shopper.placement==2# The visit function should change the cash and update the units sold if the shop is in the preferencesassert(test_Shopper.visit_shop(test_Shop))assert test_Shopper.placement==1assert test_Shopper.preferences[1].units_bought==1assert test_Shopper.cash==1000-test_Shop.priceassert test_Shopper.placement==1# Test of shope not in preferences test_Shop_2=Shop(1,"book",100)assert test_Shopper.visit_shop(test_Shop_2) ==False# sinceassert test_Shopper.cash==1000-test_Shop.priceassert test_Shopper.placement==0returntest_Shopper_class()def test_shopping_street_class(): test_Shop_1 = Shop(2,"men cloth",100) test_shop_2=Shop(1,"book",50) test_food=food(type="Fastfood",buy_prop=1) test_cloth = cloth(type="men cloth", buy_prop=1) test_Shopper_1=Shopper(placement=2,direction=-1,cash=1000,preferences=[test_food,test_cloth]) test_Shopper_2=Shopper(placement=1,direction=1,cash=1000,preferences=[test_food,test_cloth]) test_shopping_street=shopping_street(list_of_shops=[test_Shop_1,test_shop_2],list_of_shopers=[test_Shopper_1,test_Shopper_2])# No book should be hold sold # The mens cloth should be soldassert test_shopping_street.list_of_shopers[0].placement==2assert test_shopping_street.list_of_shopers[1].placement==1assert test_shopping_street.list_of_shops[0].units_sold==0assert test_shopping_street.list_of_shops[1].units_sold==0 test_shopping_street.run_tick()assert test_shopping_street.list_of_shopers[0].placement==1assert test_shopping_street.list_of_shopers[1].placement==2assert test_shopping_street.list_of_shops[0].units_sold==1assert test_shopping_street.list_of_shops[1].units_sold==0 test_shopping_street.run_tick() test_shopping_street.run_tick()assert test_shopping_street.list_of_shopers[0].placement==0assert test_shopping_street.list_of_shopers[1].placement==3assert test_shopping_street.list_of_shops[0].units_sold==2assert test_shopping_street.list_of_shops[1].units_sold==0 test_shopping_street.run_tick()assert test_shopping_street.list_of_shopers[0].placement==0assert test_shopping_street.list_of_shopers[1].placement==3assert test_shopping_street.list_of_shops[0].units_sold==2assert test_shopping_street.list_of_shops[1].units_sold==0assert test_shopping_street.get_total_turnover()==2*100assert test_shopping_street.get_unit()returntest_shopping_street_class()
Simulation
In this code, I implemented a small simulation to validate the framework. I plotted the units sold and the turnover. Since this is a Monte Carlo simulation, there is no theoretical distribution for comparison
Code
import matplotlib as pltdef simulate_street(): shops = [ Shop(placement=0, category="food", price=50), Shop(placement=1, category="cloth", price=200), Shop(placement=2, category="book", price=150), Shop(placement=3, category="food", price=100), Shop(placement=4, category="cloth", price=300), Shop(placement=5, category="book", price=250) ] ticks=len(shops) preferences_1 = [ food(type="food", buy_prop=0.8), cloth(type="cloth", buy_prop=0.5), book(type="book", buy_prop=0.6) ] preferences_2 = [ food(type="food", buy_prop=0.6), cloth(type="cloth", buy_prop=0.3), book(type="book", buy_prop=0.9) ] shoppers = [ Shopper(placement=0, direction=1, cash=500, preferences=random.choice([preferences_1, preferences_2])), Shopper(placement=5, direction=-1, cash=800, preferences=random.choice([preferences_1, preferences_2])), Shopper(placement=2, direction=1, cash=300, preferences=random.choice([preferences_1, preferences_2])) ]# Initialize the shopping street street = shopping_street(list_of_shops=shops, list_of_shopers=shoppers)for _ inrange(ticks): street.run_tick()# Return a dictionary with the resultsreturn {"Total_units": street.get_unit(),"Total_turnover": street.get_total_turnover() }#plot distibution of sale and and units soldTotal_units=[]Total_turnover=[]for i inrange(100): sim_obj=simulate_street() Total_units.append(sim_obj["Total_units"]) Total_turnover.append(sim_obj["Total_turnover"])import matplotlib.pyplot as plt# Create two subplotsfig, axes = plt.subplots(2, 1, figsize=(8, 10)) # 2 rows, 1 column# Histogram for List Aaxes[0].hist(Total_units, bins=5, color='blue', alpha=0.7)axes[0].set_title('Histogram of Total units')axes[0].set_xlabel('Value')axes[0].set_ylabel('Frequency')# Histogram for List Baxes[1].hist(Total_turnover, bins=5, color='orange', alpha=0.7)axes[1].set_title('Histogram of Total turnover')axes[1].set_xlabel('Value')axes[1].set_ylabel('Frequency')# Adjust layoutplt.tight_layout()# Show plotsplt.show()
No Conclusions from the Model
As stated at the beginning, this project was designed purely as an exercise in thinking about good structures for this type of model, rather than drawing any conclusions from the simulation.
Technical Notes and Suggestions for Improvement
One potential improvement to the structure I created is how data is stored in the objects’ classes.
Currently, all data is encapsulated within individual objects, which works for tracking properties specific to each agent or shop. However, if I were interested in aggregated data—such as the total units sold—I would need to compute this after each tick by iterating through all the relevant objects.
A more efficient approach could involve maintaining a centralized summary of key metrics during the simulation. For example:
Total Units Sold: Incremented directly when a sale occurs.
Revenue by Category: Updated in real time when a product is purchased.
This way, aggregated data would be readily available without requiring extra computation after each tick. It would also make it easier to monitor and analyze trends as the simulation progresses.
When building simulations or using object-oriented programming, many of the structures can be designed in various ways. For example, the buy function could be implemented as part of the shopper agent or as part of the environment.
Maintaining a well-organized structure is crucial, especially when managing dynamic effects, which can quickly become complex. This is particularly true in economic systems where actors are trained to consider both upstream and downstream effects.
Downstream Effects
I believe downstream effects would be simpler to implement. To track these efficiently, I would use a directed graph or a reactive graph (as described in Mastering Shiny). The reactive graph approach could be more efficient for managing dependencies and dynamic changes in the system.
Parallelization Challenges
A minor technical detail: as the code is written now, parallelizing the simulation would be challenging. Although the shoppers do not interact directly, they interact with the same shops. This creates a potential issue when writing to shared variables, such as the units sold in a shop. If multiple shoppers update this variable simultaneously, errors could occur.
I am particularly interested in learning how to address these kinds of problems effectively. For example, using locks or other concurrency control methods might help, but exploring more sophisticated solutions could be valuable.