Análisis de estado de ánimo de una persona basado en escuchas de Last.FM

En este trabajo se busca determinar el estado de ánimo de una persona, en la última semana, basándonos en sus escuchas registradas en la plataforma Last.fm.

Para ello, seguiremos este proceso:

  1. Obtener las canciones que ha escuchado el usuario en la última semana.
  2. Obtener las letras de las canciones de la plataforma Genius.
  3. Analizar los estados de ánimo predominantes en las letras de las canciones.
  4. Visualizar los resultados con gráficas.

In [1]:
from lastfm import lastfm_api
from genius import genius_api
from analysis import mood_analysis
from visualization import emotion_visualization

from datetime import date, datetime, timedelta

# To format the dictionaries.
from pprint import pprint

# To print the plots in the notebook.
import psutil
from plotly.offline import init_notebook_mode
import plotly.io as pio

init_notebook_mode(connected=False)

1. Last.FM: Obtener las canciones que ha escuchado el usuario en la última semana

Last.FM Logo

Last.fm es una red social que registra la música que escuchan sus usuarios. Ofrece una gran variedad de estadísticas, como los más escuchados, géneros preferidos... También es capaz de hacer recomendaciones en base a las escuchas: artistas similares, canciones que le puedan gustar, etc.

En nuestro caso, solo nos centraremos en obtener las canciones escuchadas por el usuario. Para ello, primero necesitamos proveer un usuario de Last.FM válido.

In [2]:
user = input()
print(user)
Starkie785
In [3]:
# Check if the provided user exists.
print(f'Retrieving information from the user \'{user}\'')

userInfo = lastfm_api.user_info(user)

if 'error' in userInfo:
    print(f"{userInfo['message']} - {user}")
    exit()
else:
    print(f"The user '{user}' is a valid user.")
Retrieving information from the user 'Starkie785'
The user 'Starkie785' is a valid user.

A continuación, obtenemos las escuchas en la última semana, agrupadas por artista:

In [4]:
# Get the tracks from the last 7 days.
today = date.today()
lastWeekDate = datetime(today.year, today.month, today.day) - timedelta(days = 7)

user_tracks = lastfm_api.user_tracks_from_date(user, lastWeekDate)
songs_by_artist = lastfm_api.group_tracks_by_artist(user_tracks)

print(f"Found {len(user_tracks)} songs by {len(songs_by_artist)} different artists.")
Last.FM - Reading songs listened since '2021-03-27 00:00:00' by 'Starkie785'.
Last.FM - Read 14 out of 14 tracks.
Found 14 songs by 5 different artists.
In [5]:
first_artist = list(songs_by_artist.keys())[0]
pprint(songs_by_artist[first_artist])
{'name': 'Arcane Roots',
 'songs': {'If Nothing Breaks, Nothing Moves': [datetime.datetime(2021, 4, 3, 12, 45, 9)]}}

2. Genius - Obtener las letras de las canciones

Genius Logo

Una vez hemos obtenido las escuchas, el siguiente paso es obtener las letras de las canciones. Usaremos la API de Genius, una base de datos de letras.

Las canciones instrumentales o aquellas cuya letra no encontremos serán ignoradas.

In [6]:
lyrics_by_artist = genius_api.lyrics_by_artists(songs_by_artist)
Genius - Searching lyrics of 'Arcane Roots'.
Genius - Searching lyrics of 'Slayer'.
Genius - Searching lyrics of 'Avenged Sevenfold'.
Genius - Searching lyrics of 'Epica'.
Genius - Searching lyrics of 'Cult of Luna'.
Genius - The song 'Cult of Luna - Disharmonia' was not found.
Genius - The song 'Cult of Luna - The One' was not found.
In [7]:
artist = lyrics_by_artist[0]
print(f"Name: {artist['name']}")

first_song = list(artist['songs'].keys())[0] 
print(f"Song: {first_song}\n")
print(f"Date listened: {artist['songs'][first_song]['date_listened']}")
print(artist['songs'][first_song]['lyrics'])
Name: Arcane Roots
Song: If Nothing Breaks, Nothing Moves

Date listened: [datetime.datetime(2021, 4, 3, 12, 45, 9)]
Give me the song and I swear that I'll sing it
Like never before
All of my life I have watched this moment
Go right through the door
So don't you save a single breath
For me when I'm gone
Cause there will be no lungs inside my chest
And nothing inside of my heart

So I'm standing in front of you, to sing like I was the first
But I can't even look at you, and I barely remember the words
YEAH

So give me a moment, because I'm finding a way in the dark
The time we have stolen, kept us from falling
But now we're falling apart

So give me the song and I swear that I'll sing it
Like never before
All of my life I have watched this moment
Go right through the door
So don't you save a single breath
For me when I'm gone
Cause there will be no lungs inside my chest
And nothing inside of my heart
Nothing inside of my heart

Tied up to a stake, before another disaster
I'll roll and step and step, into my own fucking armour
YEAH

So I'm standing in front of you
To sing like I was the first
But I can't even look at you
And I barely remember the words
How many years will it take 'til we fade out
All of my fears have been sta ring at all my doubts
Nobody cares if you're standing or lying down
When all you had is taken away
RUN IT

So give me the song and I swear that I'll sing it
Like never before
All of my life I have watched this moment
Go right through the door
So don't you save a single breath
For me when I'm gone
Cause there will be no lungs inside my chest
And nothing inside of my heart
Nothing inside of my heart

All the dreams I had are clawing like a hand around my throat
There will be a crown upon my head
There will be a crown upon me

3. NRCLex - Analisis de emociones de las letras

Para analizar las emociones de las letras de las canciones, vamos a utilizar la librería NCRLex. Esta librería toma de base la colección de palabras en inglés EmoLex, compilada por la National Research Council (NRC) de Canada.

Esta colección asigna a las palabras que la componen una o más emociones del modelo de Plutchik. Es un modelo creado por Robert Plutchik en 1980 y que tiene la forma de una rueda. Cuenta con 8 emociones básicas: alegría, confianza, miedo, sorpresa, tristeza, aversión, ira y anticipación. Estas emociones básicas pueden combinarse para definir emociones compuestas más concretas, como el amor o los remordimientos.

Rueda de las emociones de Plutchik

En la colección, las palabras aparecen de la siguiente forma: 'abandoned': ['anger', 'fear', 'sadness']

De cada una de las canciones, calcularemos la prevalencia de las emociones y las devolveremos como un vector.

Por ejemplo: [0.0, 0.09230769230769231, 0.0, 0.0, 0.18461538461538463, 0.0, 0.0, 0.1076923076923077]

In [8]:
song_emotions_by_artist = mood_analysis.analyse_lyrics(lyrics_by_artist)

emotion_by_user = { user: song_emotions_by_artist }
In [9]:
artist = song_emotions_by_artist[1]
print(f"Name: {artist['name']}")

first_song = list(artist['songs'].keys())[0] 
print(f"Song: {first_song}\n")


print(song_emotions_by_artist[0]['categories'])
print(artist['songs'][first_song])
Name: Arcane Roots
Song: If Nothing Breaks, Nothing Moves

['joy', 'trust', 'fear', 'surprise', 'sadness', 'disgust', 'anger', 'anticipation']
{'emotions': [0.12307692307692308, 0.16923076923076924, 0.015384615384615385, 0.015384615384615385, 0.13846153846153847, 0.03076923076923077, 0.07692307692307693, 0.09230769230769231], 'date_listened': [datetime.datetime(2021, 4, 3, 12, 45, 9)]}

Solo nos queda agregar los datos para poder visualizarlos. Vamos a agregarlos de dos formas distintas:

  • Agregamos los datos por días, usando la fecha de escucha de cada canción.
  • Agregamos los datos del total de la semana, sumando los datos por datos por dias.

En ambos casos, sumamos todos los pesos de las emociones obtenidos y normalizamos el vector. El resultado será el estado anímico del usuario durante la semana completa, y su estado en cada día concreto.

In [10]:
user_emotions = mood_analysis.normalize_user_emotions(song_emotions_by_artist, lastWeekDate)

emotion_by_user = {user: user_emotions}
In [11]:
pprint(emotion_by_user)
{'Starkie785': {'days': {'01/04/2021': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
                         '02/04/2021': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
                         '03/04/2021': [0.25811674804430956,
                                        0.39129421225069283,
                                        0.3874771615717797,
                                        0.16983793611810546,
                                        0.5080284961504714,
                                        0.3261223170515412,
                                        0.3813615528481078,
                                        0.3023173100946483],
                         '27/03/2021': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
                         '28/03/2021': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
                         '29/03/2021': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
                         '30/03/2021': [0.13909381399694953,
                                        0.2470442326353309,
                                        0.5314154769102318,
                                        0.18819079740099473,
                                        0.6080830201235562,
                                        0.1701370698206028,
                                        0.26654998347199044,
                                        0.3633777609108831],
                         '31/03/2021': [0.0,
                                        0.0,
                                        0.0,
                                        0.0,
                                        0.0,
                                        0.0,
                                        0.0,
                                        0.0]},
                'total': [0.17818915679547004,
                          0.295343926557623,
                          0.49232145424552787,
                          0.18461510953503682,
                          0.5837004366646531,
                          0.22123121408731883,
                          0.30581077152546127,
                          0.3484074606504949]}}

4. Visualizaciones de los datos

Ya solo nos queda visualizar los datos. Se han elegido distintas gráficas para estudiarlos.

La primera de ellas es una visualización de serie temporal. En ella, vemos la evolución diaria de las emociones del usuario.

In [12]:
chart = emotion_visualization._create_time_series_chart(user, emotion_by_user[user], mood_analysis.emotions)

chart

La siguiente visualización es mediante un gráfico de tipo radar (radar chart), que nos permite comparar gráficamente los sentimientos del usuario:

In [13]:
chart = emotion_visualization._create_radar_chart(emotion_by_user, mood_analysis.emotions)

chart

Además, aprovechando esta misma visualización, podemos comparar distintos usuarios de la platforma.

NOTA: En este caso, se va a utilizar los datos de otro usuario calculado previamente. El script LastFmMoodDetection.py acepta varios usuarios simultaneos para hacer el análisis completo.

In [14]:
# Add precalculated user emotions.
emotion_by_user['MissLannet'] = {'total': [0.55886765, 0.35558085, 0.22091529, 0.09942512, 0.2337744 , 0.07233408, 0.16831641, 0.64368252]}

chart = emotion_visualization._create_radar_chart(emotion_by_user, mood_analysis.emotions)

chart

Si queremos comparar muchos usuarios, el radar chart nos puede parecer insuficiente. Es dificil compararlos ya que no se distingue apenas.

In [15]:
emotion_by_user['DartSoul'] = {'total': [0.28606621551798916, 0.44747382623474274, 0.3934163358050923, 0.27063167723415776, 0.4643379065242006, 0.23456629517295632, 0.32643895466563194, 0.33574089382072886]}

emotion_by_user['TechnoJazz'] = {'total': [0.499486660842346, 0.3710087165225994, 0.25125548453575264, 0.33280029821609003, 0.3972576962785898, 0.27001179693125393, 0.22408326698964579, 0.39755111950972094]}

emotion_by_user['newmailson'] = {'total': [0.5450923681249055, 0.3259429615008384, 0.36709638037321246, 0.19264969059702178, 0.41176630489645083, 0.2309836781485909, 0.31284530966533536, 0.3224665922092935]}

emotion_by_user['escalberto'] = {'total': [0.5185218580327058, 0.4070076127690966, 0.3598331468479219, 0.18713271962753733, 0.4200637514506998, 0.207653596200937, 0.3116937141650205, 0.29026676491915604]}
In [16]:
chart = emotion_visualization._create_radar_chart(emotion_by_user, mood_analysis.emotions)

chart

Entonces, para ver la similitud entre usuarios, vamos a usar una aproximacióm de Multidimension Scaling. Así, visualizando la matriz de proximidad, veremos cuanto se parecen los usuarios al dado.

In [17]:
chart = emotion_visualization._create_proximity_matrix_chart(emotion_by_user, mood_analysis.emotions)

chart