147 lines
5.1 KiB
Python
147 lines
5.1 KiB
Python
"""Optimisation du trajet boulot <> dodo
|
|
|
|
Les but de ce _programme_ est de déterminer la combinaison de transports
|
|
optimale sur un mois donné pour minimiser le coût récurrent de trajet
|
|
vers et depuis le travail.
|
|
|
|
Les 3 modes de transport sont :
|
|
- Voiture individuelle
|
|
- Transports en communs (sans abonnement)
|
|
- Transports en communs (avec abonnement)
|
|
|
|
On pourra envisager d'ajouter le covoiturage quand l'offre pour ce mode
|
|
sera plus développée / fiable dans le coin.
|
|
"""
|
|
|
|
from itertools import product
|
|
from datetime import datetime, date, timedelta
|
|
|
|
import pandas as pd
|
|
|
|
|
|
def easter_dates(thisdate):
|
|
"""This function returns a 3-tuple containing, for the year of thisdate,
|
|
Easter day, Ascension and Pentecost as datetime.
|
|
It is computed with the Butcher algorithm, from Wikipedia.
|
|
"""
|
|
y = thisdate.year
|
|
_, n = divmod(y, 19) # cycle de Méton
|
|
c, u = divmod(y, 100) # centaine et range de l'année
|
|
s, t = divmod(c, 4) # siècle bissextile
|
|
p, _ = divmod((c+8), 25) # cycle de proemptose
|
|
q, _ = divmod((c-p+1), 3) # proemptose
|
|
_, e = divmod((19*n+c-s-q+15), 30) # épacte
|
|
b, d = divmod(u, 4) # année bissextile
|
|
_, l = divmod((2*t+2*b-e-d+32), 7) # lettre dominicale
|
|
h, _ = divmod((n+11*e+22*l), 451) # correction
|
|
m, j = divmod((e+l-7*h+114), 31) # mois et quantième du Samedi saint
|
|
|
|
easter = date(y, m, j+2) # Fêté le lendemain, lundi
|
|
ascension = easter + timedelta(days=38) # 40 jrs après Pâques, jeudi
|
|
pentecost = easter + timedelta(days=49) # Fêté le lendemain, lundi
|
|
|
|
return easter, ascension, pentecost
|
|
|
|
|
|
def is_business_day(thisdate, bridge=True):
|
|
"""This function returns True if the given date is not a public holiday
|
|
nor a weekend. If bridge is set to True, returns True if the date is not
|
|
between two days off (weekend / public holiday).
|
|
"""
|
|
# Public holidays
|
|
easter, ascension, pentecost = easter_dates(thisdate)
|
|
public_holidays = {
|
|
date(thisdate.year, 1, 1), # 1er janvier jour de l'an
|
|
date(thisdate.year, 5, 1), # 1er mai fête du travail
|
|
date(thisdate.year, 5, 8), # 8 mai fête de la Victoire
|
|
date(thisdate.year, 7, 14), # 14 juillet fête nationale
|
|
date(thisdate.year, 8, 15), # 15 août Assomption
|
|
date(thisdate.year, 11, 1), # 1er novembre Toussaint
|
|
date(thisdate.year, 11, 11), # 11 novembre armistice
|
|
date(thisdate.year, 12, 25), # 25 décembre Noël
|
|
easter, # lundi de Pâques
|
|
ascension, # jeudi de l'Ascension
|
|
pentecost, # lundi de Pentecôte
|
|
}
|
|
if bridge:
|
|
td = timedelta(days=1)
|
|
hb = not is_business_day(thisdate-td, bridge=False) # Holiday Before
|
|
hn = not is_business_day(thisdate+td, bridge=False) # Holiday Next
|
|
bridged = hb and hn
|
|
else:
|
|
bridged = False
|
|
|
|
holiday = thisdate in public_holidays
|
|
business = thisdate.weekday() < 5 and not (holiday or bridged)
|
|
|
|
return business
|
|
|
|
|
|
# On pourrait en faire un générateur...
|
|
def combinations(nb_items, total, start_at=0):
|
|
"""This function returns a colleciton of nb_items-tuples so
|
|
that their sum is always equal to total.
|
|
"""
|
|
r = product(range(start_at, total+1), repeat=nb_items)
|
|
r = list(filter(lambda c: sum(c) == total, r))
|
|
return r
|
|
|
|
|
|
def count_business_days(year, month, holidays={}):
|
|
"""This function returns the number of working daysin the given month."""
|
|
bd_count = 0
|
|
for i in range(1,32):
|
|
try:
|
|
thisdate = date(year, month, i)
|
|
except(ValueError):
|
|
# no more days in month
|
|
break
|
|
if is_business_day(thisdate) and not thisdate in holidays:
|
|
bd_count += 1
|
|
return bd_count
|
|
|
|
|
|
if __name__ == "__main__":
|
|
"""Les données
|
|
|
|
Pas de coût initial pour le T+ ou la Voiture (_on ne prend pas en compte
|
|
l'entretien/l'assurance, au moins dans un premier temps_). Le Navigo est
|
|
payé en début de mois, à moitié prix car remboursé par l'employeur.
|
|
|
|
Le coût récurrent du Navigo doit prendre en compte le prix de l'essence
|
|
pour la mobylette, puis on a un prix unitaire pour les T+ et une consom-
|
|
-mation rapportée au prix du diesel pour la voiture.
|
|
|
|
* Voiture : 5,6 l/100 à 1,44 €/l et pour 2x38 km
|
|
* 6,13 €/A-R
|
|
* Mobylette : 3,7 l/100 à 1,56 €/l et pour 2x10 km
|
|
* 1,15 €/A-R
|
|
"""
|
|
modes = ('Ticket T+', 'Navigo', 'Voiture')
|
|
cout_initial = (0.0, 35.0, 0.0)
|
|
cout_recurent = (5.96, 1.15, 6.13)
|
|
|
|
annee, mois = 2018, 6
|
|
|
|
jrs_ouvres = count_business_days(annee, mois)
|
|
nb_modes = len(modes)
|
|
|
|
cout_mix = list()
|
|
comb_mix = combinations(nb_modes, jrs_ouvres)
|
|
for mix in comb_mix:
|
|
cout = 0
|
|
for mode in range(nb_modes):
|
|
# TODO: utiliser une expression ternaire
|
|
if mix[mode]:
|
|
cout += cout_initial[mode]
|
|
cout += mix[mode]*cout_recurent[mode]
|
|
cout_mix.append(cout)
|
|
|
|
bilan = [[m for m in modes] + ['Cout']]
|
|
#bilan.append([])
|
|
for i, mix in enumerate(comb_mix):
|
|
bilan.append([*mix,cout_mix[i]])
|
|
|
|
bilan_df = pd.DataFrame(bilan)
|
|
print(bilan_df)
|