434 lines
13 KiB
Markdown
434 lines
13 KiB
Markdown
|
|
# Corrigé du TP Spotify Wrapped
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Partie 2 : Fonctions de base
|
||
|
|
|
||
|
|
### Exercice 1 : Extraction de données avec `map`
|
||
|
|
|
||
|
|
**Question 1.1** :
|
||
|
|
```python
|
||
|
|
get_titre = lambda ecoute: ecoute["titre"]
|
||
|
|
|
||
|
|
titres = list(map(get_titre, ecoutes))
|
||
|
|
print(titres[:3]) # ['Blinding Lights', 'Bohemian Rhapsody', 'Bad Guy']
|
||
|
|
```
|
||
|
|
|
||
|
|
**Question 1.2** :
|
||
|
|
```python
|
||
|
|
get_duree_minutes = lambda ecoute: round(ecoute["duree"] / 60, 2)
|
||
|
|
|
||
|
|
durees = list(map(get_duree_minutes, ecoutes))
|
||
|
|
print(durees[:3]) # [3.38, 5.9, 3.23]
|
||
|
|
```
|
||
|
|
|
||
|
|
**Question 1.3** :
|
||
|
|
```python
|
||
|
|
formater_ecoute = lambda e: f"{e['titre']} - {e['artiste']} ({round(e['duree']/60, 2)}min)"
|
||
|
|
|
||
|
|
formatees = list(map(formater_ecoute, ecoutes))
|
||
|
|
print(formatees[0]) # "Blinding Lights - The Weeknd (3.38min)"
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Exercice 2 : Filtrage avec `filter`
|
||
|
|
|
||
|
|
**Question 2.1** :
|
||
|
|
```python
|
||
|
|
est_pop = lambda ecoute: ecoute["genre"] == "pop"
|
||
|
|
|
||
|
|
ecoutes_pop = list(filter(est_pop, ecoutes))
|
||
|
|
print(f"Nombre de morceaux pop : {len(ecoutes_pop)}") # 12
|
||
|
|
```
|
||
|
|
|
||
|
|
**Question 2.2** :
|
||
|
|
```python
|
||
|
|
est_long = lambda ecoute: ecoute["duree"] > 300
|
||
|
|
|
||
|
|
ecoutes_longues = list(filter(est_long, ecoutes))
|
||
|
|
print(f"Morceaux longs : {len(ecoutes_longues)}") # 5
|
||
|
|
```
|
||
|
|
|
||
|
|
**Question 2.3** :
|
||
|
|
```python
|
||
|
|
def filtre_genre(genre):
|
||
|
|
"""
|
||
|
|
Retourne une fonction lambda qui filtre par genre.
|
||
|
|
C'est une fonction d'ordre supérieur !
|
||
|
|
"""
|
||
|
|
return lambda ecoute: ecoute["genre"] == genre
|
||
|
|
|
||
|
|
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`
|
||
|
|
|
||
|
|
```python
|
||
|
|
import functools
|
||
|
|
```
|
||
|
|
|
||
|
|
**Question 3.1** :
|
||
|
|
```python
|
||
|
|
temps_total = functools.reduce(
|
||
|
|
lambda acc, ecoute: acc + ecoute["duree"],
|
||
|
|
ecoutes,
|
||
|
|
0
|
||
|
|
)
|
||
|
|
print(f"Temps total : {temps_total} secondes") # 5301 secondes
|
||
|
|
```
|
||
|
|
|
||
|
|
**Question 3.2** :
|
||
|
|
```python
|
||
|
|
heures = temps_total // 3600
|
||
|
|
minutes = (temps_total % 3600) // 60
|
||
|
|
print(f"Temps d'écoute : {heures}h {minutes}min") # 1h 28min
|
||
|
|
```
|
||
|
|
|
||
|
|
**Question 3.3** :
|
||
|
|
```python
|
||
|
|
plus_long = functools.reduce(
|
||
|
|
lambda acc, ecoute: ecoute if ecoute["duree"] > acc["duree"] else acc,
|
||
|
|
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** : (fournie dans l'énoncé)
|
||
|
|
```python
|
||
|
|
def compter_par_genre(ecoutes):
|
||
|
|
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** :
|
||
|
|
```python
|
||
|
|
def compter_par_artiste(ecoutes):
|
||
|
|
"""Retourne un dictionnaire {artiste: nombre_ecoutes}."""
|
||
|
|
return functools.reduce(
|
||
|
|
lambda acc, e: {**acc, e["artiste"]: acc.get(e["artiste"], 0) + 1},
|
||
|
|
ecoutes,
|
||
|
|
{}
|
||
|
|
)
|
||
|
|
|
||
|
|
stats_artistes = compter_par_artiste(ecoutes)
|
||
|
|
print(stats_artistes)
|
||
|
|
# {'The Weeknd': 3, 'Queen': 1, 'Billie Eilish': 1, 'Led Zeppelin': 1, ...}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Question 4.3** :
|
||
|
|
```python
|
||
|
|
def artiste_prefere(stats_artistes):
|
||
|
|
"""Retourne le nom de l'artiste avec le plus d'écoutes."""
|
||
|
|
return functools.reduce(
|
||
|
|
lambda acc, item: item if item[1] > acc[1] else acc,
|
||
|
|
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)
|
||
|
|
|
||
|
|
**Question 5.1** : (fournie dans l'énoncé)
|
||
|
|
```python
|
||
|
|
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") # 42 minutes
|
||
|
|
```
|
||
|
|
|
||
|
|
**Question 5.2** : (fournie dans l'énoncé)
|
||
|
|
```python
|
||
|
|
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** : (fournie dans l'énoncé)
|
||
|
|
```python
|
||
|
|
def pipeline(*fonctions):
|
||
|
|
"""Retourne une fonction qui applique les fonctions en séquence."""
|
||
|
|
return lambda x: functools.reduce(
|
||
|
|
lambda acc, f: f(acc),
|
||
|
|
fonctions,
|
||
|
|
x
|
||
|
|
)
|
||
|
|
|
||
|
|
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
|
||
|
|
|
||
|
|
```python
|
||
|
|
import functools
|
||
|
|
|
||
|
|
def compter_par_artiste(ecoutes):
|
||
|
|
return functools.reduce(
|
||
|
|
lambda acc, e: {**acc, e["artiste"]: acc.get(e["artiste"], 0) + 1},
|
||
|
|
ecoutes,
|
||
|
|
{}
|
||
|
|
)
|
||
|
|
|
||
|
|
def compter_par_genre(ecoutes):
|
||
|
|
return functools.reduce(
|
||
|
|
lambda acc, e: {**acc, e["genre"]: acc.get(e["genre"], 0) + 1},
|
||
|
|
ecoutes,
|
||
|
|
{}
|
||
|
|
)
|
||
|
|
|
||
|
|
def generer_wrapped(ecoutes):
|
||
|
|
"""
|
||
|
|
Génère un résumé Wrapped complet de manière fonctionnelle.
|
||
|
|
"""
|
||
|
|
# 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 (celui avec le plus d'écoutes)
|
||
|
|
top_artiste = functools.reduce(
|
||
|
|
lambda acc, item: item if item[1] > acc[1] else acc,
|
||
|
|
stats_artistes.items()
|
||
|
|
)[0]
|
||
|
|
|
||
|
|
# Top genre
|
||
|
|
top_genre = functools.reduce(
|
||
|
|
lambda acc, item: item if item[1] > acc[1] else acc,
|
||
|
|
stats_genres.items()
|
||
|
|
)[0]
|
||
|
|
|
||
|
|
# 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 = functools.reduce(
|
||
|
|
lambda acc, item: item if item[1] > acc[1] else acc,
|
||
|
|
stats_titres.items()
|
||
|
|
)[0]
|
||
|
|
|
||
|
|
# Morceau le plus long
|
||
|
|
plus_long = functools.reduce(
|
||
|
|
lambda acc, e: e if e["duree"] > acc["duree"] else acc,
|
||
|
|
ecoutes
|
||
|
|
)
|
||
|
|
|
||
|
|
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])
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
# Exécution
|
||
|
|
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)
|
||
|
|
```
|
||
|
|
|
||
|
|
**Sortie attendue :**
|
||
|
|
```
|
||
|
|
========================================
|
||
|
|
VOTRE WRAPPED 2026
|
||
|
|
========================================
|
||
|
|
Écoutes totales : 20
|
||
|
|
Temps d'écoute : 88.4 minutes
|
||
|
|
Artiste préféré : The Weeknd
|
||
|
|
Genre préféré : pop
|
||
|
|
Titre préféré : Blinding Lights
|
||
|
|
Top 5 artistes : ['The Weeknd', 'Michael Jackson', 'Metallica', 'Queen', 'Billie Eilish']
|
||
|
|
========================================
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Partie 5 : Bonus — Récursivité
|
||
|
|
|
||
|
|
### Exercice 7 : Implémenter reduce sans boucle
|
||
|
|
|
||
|
|
```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:
|
||
|
|
# Cas de base : liste vide, on retourne l'accumulateur
|
||
|
|
return initial
|
||
|
|
else:
|
||
|
|
# Appel récursif : on applique la fonction au premier élément
|
||
|
|
# puis on continue avec le reste de la liste
|
||
|
|
nouvel_acc = fonction(initial, liste[0])
|
||
|
|
return mon_reduce(fonction, liste[1:], nouvel_acc)
|
||
|
|
|
||
|
|
|
||
|
|
# Tests
|
||
|
|
total = mon_reduce(lambda acc, x: acc + x, [1, 2, 3, 4, 5], 0)
|
||
|
|
print(f"Somme : {total}") # 15
|
||
|
|
|
||
|
|
produit = mon_reduce(lambda acc, x: acc * x, [1, 2, 3, 4, 5], 1)
|
||
|
|
print(f"Produit : {produit}") # 120
|
||
|
|
|
||
|
|
concatenation = mon_reduce(lambda acc, x: acc + x, ["a", "b", "c"], "")
|
||
|
|
print(f"Concaténation : {concatenation}") # "abc"
|
||
|
|
```
|
||
|
|
|
||
|
|
**Explication de la récursivité :**
|
||
|
|
|
||
|
|
Pour `mon_reduce(lambda acc, x: acc + x, [1, 2, 3], 0)` :
|
||
|
|
|
||
|
|
```
|
||
|
|
mon_reduce(f, [1, 2, 3], 0)
|
||
|
|
→ nouvel_acc = f(0, 1) = 1
|
||
|
|
→ mon_reduce(f, [2, 3], 1)
|
||
|
|
→ nouvel_acc = f(1, 2) = 3
|
||
|
|
→ mon_reduce(f, [3], 3)
|
||
|
|
→ nouvel_acc = f(3, 3) = 6
|
||
|
|
→ mon_reduce(f, [], 6)
|
||
|
|
→ return 6 (cas de base)
|
||
|
|
→ return 6
|
||
|
|
→ return 6
|
||
|
|
→ return 6
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Code complet fonctionnel
|
||
|
|
|
||
|
|
```python
|
||
|
|
"""
|
||
|
|
Spotify Wrapped - Version fonctionnelle complète
|
||
|
|
"""
|
||
|
|
|
||
|
|
import functools
|
||
|
|
|
||
|
|
# Données
|
||
|
|
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"},
|
||
|
|
]
|
||
|
|
|
||
|
|
# Fonctions utilitaires
|
||
|
|
compter = lambda cle: lambda data: functools.reduce(
|
||
|
|
lambda acc, e: {**acc, e[cle]: acc.get(e[cle], 0) + 1},
|
||
|
|
data,
|
||
|
|
{}
|
||
|
|
)
|
||
|
|
|
||
|
|
top = lambda stats: functools.reduce(
|
||
|
|
lambda acc, item: item if item[1] > acc[1] else acc,
|
||
|
|
stats.items()
|
||
|
|
)[0]
|
||
|
|
|
||
|
|
# Pipeline de génération du Wrapped
|
||
|
|
generer_wrapped = lambda data: {
|
||
|
|
"nombre_ecoutes": len(data),
|
||
|
|
"temps_total_minutes": round(functools.reduce(lambda a, e: a + e["duree"], data, 0) / 60, 1),
|
||
|
|
"artiste_prefere": top(compter("artiste")(data)),
|
||
|
|
"genre_prefere": top(compter("genre")(data)),
|
||
|
|
"titre_prefere": top(compter("titre")(data)),
|
||
|
|
"morceau_plus_long": functools.reduce(lambda a, e: e if e["duree"] > a["duree"] else a, data)["titre"],
|
||
|
|
}
|
||
|
|
|
||
|
|
# Exécution
|
||
|
|
wrapped = generer_wrapped(ecoutes)
|
||
|
|
print(wrapped)
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
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>.
|