Estimate the average price of gas and play with simple MCMC model
Simulare il prezzo medio del carburante
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
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.