TP : Créer son Spotify Wrapped — Programmation Fonctionnelle
Thème : Paradigmes de programmation, fonctions lambda, map, filter, reduce
Contexte
Chaque fin d'année, Spotify Wrapped révèle aux utilisateurs leurs statistiques d'écoute : artistes préférés, genres favoris, minutes écoutées... Ce résumé personnalisé est devenu un phénomène viral sur les réseaux sociaux.
Dans ce TP, vous allez créer votre propre mini Wrapped en utilisant exclusivement la programmation fonctionnelle. Vous découvrirez comment les géants du streaming traitent des milliards de données grâce à ce paradigme.
Règle d'or du TP : Aucune boucle for ou while n'est autorisée ! Uniquement map, filter, reduce et la récursivité.
Partie 1 : Les données
Les écoutes
Voici un extrait de l'historique d'écoute d'un utilisateur. Chaque écoute est représentée par un dictionnaire.
# Données d'écoute (à copier dans votre fichier)
ecoutes = [
{"titre": "Blinding Lights", "artiste": "The Weeknd", "duree": 203, "genre": "pop", "date": "2026-01-15"},
{"titre": "Bohemian Rhapsody", "artiste": "Queen", "duree": 354, "genre": "rock", "date": "2026-01-15"},
{"titre": "Bad Guy", "artiste": "Billie Eilish", "duree": 194, "genre": "pop", "date": "2026-01-16"},
{"titre": "Stairway to Heaven", "artiste": "Led Zeppelin", "duree": 482, "genre": "rock", "date": "2026-01-16"},
{"titre": "Shape of You", "artiste": "Ed Sheeran", "duree": 234, "genre": "pop", "date": "2026-01-17"},
{"titre": "Smells Like Teen Spirit", "artiste": "Nirvana", "duree": 279, "genre": "rock", "date": "2026-01-17"},
{"titre": "Levitating", "artiste": "Dua Lipa", "duree": 203, "genre": "pop", "date": "2026-01-18"},
{"titre": "Back in Black", "artiste": "AC/DC", "duree": 255, "genre": "rock", "date": "2026-01-18"},
{"titre": "Blinding Lights", "artiste": "The Weeknd", "duree": 203, "genre": "pop", "date": "2026-01-19"},
{"titre": "drivers license", "artiste": "Olivia Rodrigo", "duree": 242, "genre": "pop", "date": "2026-01-19"},
{"titre": "Lose Yourself", "artiste": "Eminem", "duree": 326, "genre": "rap", "date": "2026-01-20"},
{"titre": "HUMBLE.", "artiste": "Kendrick Lamar", "duree": 177, "genre": "rap", "date": "2026-01-20"},
{"titre": "Blinding Lights", "artiste": "The Weeknd", "duree": 203, "genre": "pop", "date": "2026-01-21"},
{"titre": "Watermelon Sugar", "artiste": "Harry Styles", "duree": 174, "genre": "pop", "date": "2026-01-21"},
{"titre": "Enter Sandman", "artiste": "Metallica", "duree": 332, "genre": "metal", "date": "2026-01-22"},
{"titre": "Nothing Else Matters", "artiste": "Metallica", "duree": 388, "genre": "metal", "date": "2026-01-22"},
{"titre": "Thriller", "artiste": "Michael Jackson", "duree": 358, "genre": "pop", "date": "2026-01-23"},
{"titre": "Billie Jean", "artiste": "Michael Jackson", "duree": 294, "genre": "pop", "date": "2026-01-23"},
{"titre": "Anti-Hero", "artiste": "Taylor Swift", "duree": 200, "genre": "pop", "date": "2026-01-24"},
{"titre": "Flowers", "artiste": "Miley Cyrus", "duree": 200, "genre": "pop", "date": "2026-01-24"},
]
Partie 2 : Fonctions de base
Exercice 1 : Extraction de données avec map
La fonction map(fonction, liste) applique une fonction à chaque élément d'une liste.
Question 1.1 : Écrivez une fonction lambda get_titre qui extrait le titre d'une écoute.
get_titre = lambda ecoute: # À compléter
# Test
titres = list(map(get_titre, ecoutes))
print(titres[:3]) # ['Blinding Lights', 'Bohemian Rhapsody', 'Bad Guy']
Question 1.2 : Écrivez une fonction lambda get_duree_minutes qui convertit la durée (en secondes) en minutes (arrondi à 2 décimales).
get_duree_minutes = lambda ecoute: # À compléter
# Test
durees = list(map(get_duree_minutes, ecoutes))
print(durees[:3]) # [3.38, 5.9, 3.23]
Question 1.3 : Créez une fonction formater_ecoute qui retourne une chaîne au format "Titre - Artiste (Xmin)".
formater_ecoute = lambda e: # À compléter
# Test
formatees = list(map(formater_ecoute, ecoutes))
print(formatees[0]) # "Blinding Lights - The Weeknd (3.38min)"
Exercice 2 : Filtrage avec filter
La fonction filter(fonction, liste) conserve uniquement les éléments pour lesquels la fonction retourne True.
Question 2.1 : Filtrez les écoutes pour ne garder que les morceaux de genre "pop".
est_pop = lambda ecoute: # À compléter
ecoutes_pop = list(filter(est_pop, ecoutes))
print(f"Nombre de morceaux pop : {len(ecoutes_pop)}") # 12
Question 2.2 : Filtrez les écoutes de plus de 5 minutes (300 secondes).
est_long = lambda ecoute: # À compléter
ecoutes_longues = list(filter(est_long, ecoutes))
print(f"Morceaux longs : {len(ecoutes_longues)}") # 5
Question 2.3 : Créez un filtre paramétrable filtre_genre(genre) qui retourne une fonction lambda.
def filtre_genre(genre):
"""
Retourne une fonction lambda qui filtre par genre.
C'est une fonction d'ordre supérieur !
"""
return lambda ecoute: # À compléter
# Test
ecoutes_rock = list(filter(filtre_genre("rock"), ecoutes))
ecoutes_rap = list(filter(filtre_genre("rap"), ecoutes))
print(f"Rock: {len(ecoutes_rock)}, Rap: {len(ecoutes_rap)}") # Rock: 4, Rap: 2
Exercice 3 : Agrégation avec reduce
La fonction reduce(fonction, liste, valeur_initiale) combine tous les éléments en une seule valeur.
Question 3.1 : Calculez le temps total d'écoute en secondes.
temps_total = functools.reduce(
lambda acc, ecoute: # À compléter,
ecoutes,
0
)
print(f"Temps total : {temps_total} secondes") # 5301 secondes
Question 3.2 : Convertissez ce temps en heures et minutes.
heures = temps_total // 3600
minutes = (temps_total % 3600) // 60
print(f"Temps d'écoute : {heures}h {minutes}min") # 1h 28min
Question 3.3 : Trouvez le morceau le plus long en utilisant reduce.
plus_long = functools.reduce(
lambda acc, ecoute: # À compléter,
ecoutes
)
print(f"Plus long : {plus_long['titre']} ({plus_long['duree']}s)")
# Plus long : Stairway to Heaven (482s)
Partie 3 : Statistiques avancées
Exercice 4 : Comptage par catégorie
Question 4.1 : Comptez le nombre d'écoutes par genre en utilisant reduce.
def compter_par_genre(ecoutes):
"""
Retourne un dictionnaire {genre: nombre_ecoutes}.
Utilise reduce pour accumuler les comptages.
"""
return functools.reduce(
lambda acc, e: {**acc, e["genre"]: acc.get(e["genre"], 0) + 1},
ecoutes,
{}
)
stats_genres = compter_par_genre(ecoutes)
print(stats_genres) # {'pop': 12, 'rock': 4, 'rap': 2, 'metal': 2}
Question 4.2 : Adaptez cette fonction pour compter les écoutes par artiste.
def compter_par_artiste(ecoutes):
"""Retourne un dictionnaire {artiste: nombre_ecoutes}."""
# À compléter (inspirez-vous de compter_par_genre)
pass
stats_artistes = compter_par_artiste(ecoutes)
print(stats_artistes)
# {'The Weeknd': 3, 'Queen': 1, 'Billie Eilish': 1, ...}
Question 4.3 : Trouvez l'artiste le plus écouté.
def artiste_prefere(stats_artistes):
"""
Retourne le nom de l'artiste avec le plus d'écoutes.
Utilise reduce sur les items du dictionnaire.
"""
return functools.reduce(
lambda acc, item: # À compléter,
stats_artistes.items()
)[0]
top_artiste = artiste_prefere(stats_artistes)
print(f"Artiste préféré : {top_artiste}") # The Weeknd
Exercice 5 : Chaînage fonctionnel (Pipeline)
L'intérêt de la programmation fonctionnelle est de chaîner les opérations.
Question 5.1 : Calculez le temps total d'écoute des morceaux pop uniquement.
# En une seule expression chaînée
temps_pop = functools.reduce(
lambda acc, e: acc + e["duree"],
filter(lambda e: e["genre"] == "pop", ecoutes),
0
)
print(f"Temps pop : {temps_pop // 60} minutes")
Question 5.2 : Listez les titres des morceaux rock de plus de 4 minutes, triés par durée décroissante.
# Pipeline : filter -> filter -> map -> sorted
titres_rock_longs = list(map(
lambda e: e["titre"],
sorted(
filter(
lambda e: e["duree"] > 240,
filter(
lambda e: e["genre"] == "rock",
ecoutes
)
),
key=lambda e: e["duree"],
reverse=True
)
))
print(titres_rock_longs)
# ['Stairway to Heaven', 'Bohemian Rhapsody', 'Smells Like Teen Spirit']
Question 5.3 : Créez une fonction pipeline qui compose plusieurs fonctions.
def pipeline(*fonctions):
"""
Retourne une fonction qui applique les fonctions en séquence.
pipeline(f, g, h)(x) équivaut à h(g(f(x)))
"""
return lambda x: functools.reduce(
lambda acc, f: f(acc),
fonctions,
x
)
# Exemple d'utilisation
traitement = pipeline(
lambda data: filter(lambda e: e["genre"] == "pop", data),
lambda data: map(lambda e: e["titre"], data),
list
)
print(traitement(ecoutes)[:3]) # ['Blinding Lights', 'Bad Guy', 'Shape of You']
Partie 4 : Génération du Wrapped
Exercice 6 : Créer le résumé final
Question 6 : Créez une fonction generer_wrapped qui produit un dictionnaire avec toutes les statistiques.
def generer_wrapped(ecoutes):
"""
Génère un résumé Wrapped complet de manière fonctionnelle.
:param ecoutes: (list) liste des écoutes
:return: (dict) statistiques complètes
"""
# Temps total
temps_total = functools.reduce(lambda acc, e: acc + e["duree"], ecoutes, 0)
# Comptages
stats_artistes = compter_par_artiste(ecoutes)
stats_genres = compter_par_genre(ecoutes)
# Top artiste
top_artiste = # À compléter
# Top genre
top_genre = # À compléter
# Morceau le plus écouté (celui qui apparaît le plus)
stats_titres = functools.reduce(
lambda acc, e: {**acc, e["titre"]: acc.get(e["titre"], 0) + 1},
ecoutes,
{}
)
top_titre = # À compléter
# Morceau le plus long
plus_long = # À compléter
return {
"nombre_ecoutes": len(ecoutes),
"temps_total_minutes": round(temps_total / 60, 1),
"artiste_prefere": top_artiste,
"genre_prefere": top_genre,
"titre_prefere": top_titre,
"morceau_plus_long": plus_long["titre"],
"stats_genres": stats_genres,
"top_5_artistes": dict(sorted(
stats_artistes.items(),
key=lambda x: x[1],
reverse=True
)[:5])
}
# Test
wrapped = generer_wrapped(ecoutes)
print("=" * 40)
print(" VOTRE WRAPPED 2026")
print("=" * 40)
print(f"Écoutes totales : {wrapped['nombre_ecoutes']}")
print(f"Temps d'écoute : {wrapped['temps_total_minutes']} minutes")
print(f"Artiste préféré : {wrapped['artiste_prefere']}")
print(f"Genre préféré : {wrapped['genre_prefere']}")
print(f"Titre préféré : {wrapped['titre_prefere']}")
print(f"Top 5 artistes : {list(wrapped['top_5_artistes'].keys())}")
print("=" * 40)
Partie 5 : Bonus — Récursivité
Exercice 7 : Implémenter reduce sans boucle
Question 7 : Réécrivez la fonction reduce en utilisant la récursivité.
def mon_reduce(fonction, liste, initial):
"""
Implémentation récursive de reduce.
:param fonction: (callable) fonction à 2 arguments (acc, elem)
:param liste: (list) liste à réduire
:param initial: valeur initiale de l'accumulateur
:return: résultat de la réduction
"""
if len(liste) == 0:
# À compléter : cas de base
pass
else:
# À compléter : appel récursif
pass
# Test
total = mon_reduce(lambda acc, x: acc + x, [1, 2, 3, 4, 5], 0)
print(f"Somme : {total}") # 15
Résumé des notions
| Fonction | Description | Exemple |
|---|---|---|
map(f, lst) |
Applique f à chaque élément | map(lambda x: x*2, [1,2,3]) → [2,4,6] |
filter(f, lst) |
Garde les éléments où f est True | filter(lambda x: x>2, [1,2,3]) → [3] |
reduce(f, lst, init) |
Combine tous les éléments | reduce(lambda a,x: a+x, [1,2,3], 0) → 6 |
lambda |
Fonction anonyme | lambda x: x + 1 |
| Pipeline | Composition de fonctions | f(g(h(x))) |
Pour aller plus loin
- Compréhensions de liste : Alternative pythonique à map/filter
- Générateurs :
mapetfilterretournent des itérateurs (lazy evaluation) - Bibliothèques :
itertools,toolz,fn.pypour la programmation fonctionnelle avancée - Langages fonctionnels purs : Haskell, Elm, Clojure
Auteurs : Florian Mathieu, Enzo Frémeaux, Thimothée Decooster
Licence CC BY NC
Ce cours est mis à disposition selon les termes de la Licence Creative Commons Attribution - Pas d'Utilisation Commerciale - Partage dans les Mêmes Conditions 4.0 International.