2026-06-18 19:03:42 +02:00
# TP : Créer son Spotify Wrapped - Programmation Fonctionnelle
2026-01-17 23:10:49 +01:00
> **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.
```python
# 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.
```python
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).
```python
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)"` .
```python
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".
```python
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).
```python
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.
```python
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.
```python
import functools
```
**Question 3.1** : Calculez le temps total d'écoute en secondes.
```python
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.
```python
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` .
```python
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` .
```python
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.
```python
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é.
```python
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.
```python
# 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.
```python
# 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.
```python
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.
```python
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 )
```
---
2026-06-18 19:03:42 +02:00
## Partie 5 : Bonus - Récursivité
2026-01-17 23:10:49 +01:00
### Exercice 7 : Implémenter reduce sans boucle
**Question 7** : Réécrivez la fonction `reduce` en utilisant la récursivité.
```python
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
---
2026-07-02 15:01:24 +02:00
Auteurs : Florian Mathieu, Enzo Frémeaux, Timothée Decoster
2026-01-17 23:10:49 +01:00
2026-08-11 23:29:02 +02:00
Licence CC BY-SA
2026-01-17 23:10:49 +01:00
2026-08-11 23:29:02 +02:00
<a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/"><img alt="Licence Creative Commons" style="border-width:0" src="https://i.creativecommons.org/l/by-sa/4.0/88x31.png" /></a> <br />Ce cours est mis à disposition selon les termes de la <a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/">Licence Creative Commons Attribution - Partage dans les Mêmes Conditions 4.0 International</a>.