Photo by Jean-christophe Gougeon on Unsplash

Estimate the average price of gas and play with simple MCMC model

Simulare il prezzo medio del carburante

Filippo Valle
4 min readAug 9, 2023

--

After the recent decision by the Italian government mandating gas stations to publicly display the average prices of fuel. This article tries to simulate this situation by employing a Monte Carlo simulation approach, we endeavour to understand the potential implications of this policy change on the dynamics of fuel pricing. I constructed a simplified model that captures the interplay between gas stations and drivers, shedding light on how fuel prices might fluctuate over time. This exploration provides an intriguing perspective on the intricate relationship between regulatory measures, market behavior, and consumer choices in the realm of fuel consumption.

The code

Here I will present the full code of this simulation.

from enum import Enum
import random
import math
from statistics import mean, stdev
import plotly.graph_objects as go
from plotly.subplots import make_subplots

class strategies(Enum):
UP = lambda x: x+0.1 #min(2, x+0.1)
STILL = lambda x: x
DOWN = lambda x: max(1, x-0.1)

class Car():
def __init__(self) -> None:
self.tank_size = 1 + random.random()*2

class Station():
def __init__(self) -> None:
self.cars = [Car() for _ in range(N_cars)]
self.price = 1.7 + random.random()*0.4 - 0.2
self.strategy = self.last_strategy = strategies.STILL
self.last_profit = len(self.cars)

def get_random_strategy(self):
r = random.random()
if r < 1./3.:
return strategies.UP
elif r < 2./3:
return strategies.STILL
else:
return strategies.DOWN

def _apply_strategy(self):
self.price = self.strategy(self.price)

@property
def profit(self):
return sum([c.tank_size * self.price for c in self.cars])

def update(self):
if self.profit < self.last_profit * 0.8:
self.strategy = strategies.DOWN
elif self.profit > self.last_profit * 1.2:
self.strategy = strategies.UP
if random.random() < 0.2:
self.strategy = self.get_random_strategy()
self._apply_strategy()
self.last_strategy = self.strategy
self.last_profit = self.profit

def __len__(self):
return len(self.cars)

class Road():
def __init__(self) -> None:
self.stations = [Station() for _ in range(N_Stations)]

@property
def prices(self)->list:
return [s.price for s in self.stations]

@property
def avg_price(self)->float:
return mean(self.prices)

def update(self):
for s in self.stations:
s.update()

def __getitem__(self, i:int):
return self.stations[i]

def __len__(self):
return len(self.stations)

def update_cars():
alpha = 1000
def get_neigh(i):
if i == 0:
return 1
if i == len(road)-1:
return i - 2
if random.random() < 0.5:
return i-1
else:
return i+1
avg_price = road.avg_price
for i, s in enumerate(road):
tomove = []
for ic, car in enumerate(s.cars):
neigh = get_neigh(i)
diff = (s.price - road[neigh].price)/s.price
#diff = (avg_price - road[neigh].price) / s.price
if diff > 0:
tomove.append((ic, neigh))
continue
elif random.random() < math.exp(alpha * diff):
tomove.append((ic, neigh))
continue

_removed = 0
for _car in tomove:
car = s.cars[_car[0]-_removed]
road[_car[1]].cars.append(car)
s.cars.pop(_car[0]-_removed)
_removed +=1

def update_stations():
road.update()

observables = {}
observables["prices"]=[]
observables["avg_price"]=[]
N_Stations = 1000
N_cars = 500
T = range(150)
road = Road()

print([round(s.price,2) for s in road])
print([len(s) for s in road])

for t in T:
update_cars()
update_stations()
observables["prices"].append(road.prices)

print([round(s.price,2) for s in road])
print([len(s) for s in road])

fig = make_subplots(1, 2)

#price vs T

fig.add_scatter(x=list(T), y=[mean(p)for p in observables["prices"]],
error_y={"type": "data", "array": [
stdev(p) for p in observables["prices"]]},
row=1, col=1, name="average")

for i in range(len(road)):
fig.add_scatter(x=list(T), y=[p[i] for p in observables["prices"]], opacity=0.1, line_color="gray", mode="lines", row=1, col=1)


#cars vs T
fig.add_scatter(
x=[s.price for s in road],
y=[len(s) for s in road],
mode="markers",
row=1, col=2)

fig.show()

Rules

Stations

Stations are implemented to maximise their profit. Their profit is simplistically assumed to be the price times the number of cars.

@property
def profit(self):
return sum([c.tank_size * self.price for c in self.cars])

Cars

Cars are implemented to move from their current station if they found a cheaper one in the neighbourhood.

Stochasticity

In order to make the model stochastic the cars move are Monte Carlo defined. Moreover, 10% of the stations change their price randomly over time. All the values are initialised randomly.

The drivers knowledge

Cars can be defined so that their drivers are aware only of the price of the nearest and second-nearest station

diff = (s.price - road[neigh].price)/s.price

Or, on the other hand, the drivers can be aware of the average price of all the stations

diff = (avg_price - road[neigh].price) / s.price

Results

Left: red line price when average price is exposed blue line median price with legacy conditions. Right: number of cars at a station with a given price. Image by author

Within the context of this hyper-simplified model, we turn our attention to the simulation results. Specifically, when considering the scenario where drivers are cognizant of the average fuel price, a discernible trend emerges. Over the course of the simulation, we observe a noteworthy uptrend in the median price of fuel. This finding prompts us to reflect on the potential implications of consumer awareness and its influence on market dynamics. In fact expose the average price affects both the drivers, but also the gas stations which are encouraged to increase the price over time, in fact a price small greater than the average will barely affect the number of cars that goes to that station (the price is almost the average one, right?), but will increase the profits.

--

--

Filippo Valle

Interested in physics, ML application, community detection and coding. I have a Ph.D. in Complex Systems for Life Sciences