Files
TermNSI/Paradigmes/TP_SpotifyWrapped.md

414 lines
13 KiB
Markdown

# 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.
```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)
```
---
## Partie 5 : Bonus — Récursivité
### 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
---
Auteurs : Florian Mathieu, Enzo Frémeaux, Thimothée Decooster
Licence CC BY NC
<a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/4.0/"><img alt="Licence Creative Commons" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-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-nc-sa/4.0/">Licence Creative Commons Attribution - Pas d'Utilisation Commerciale - Partage dans les Mêmes Conditions 4.0 International</a>.