Aller au contenu

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.

import functools

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 : map et filter retournent des itérateurs (lazy evaluation)
  • Bibliothèques : itertools, toolz, fn.py pour la programmation fonctionnelle avancée
  • Langages fonctionnels purs : Haskell, Elm, Clojure

Auteurs : Florian Mathieu, Enzo Frémeaux, Thimothée Decooster

Licence CC BY NC

Licence Creative Commons
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.