Last.fm Clocks

 

Comparing my active music-listening times over the years.


Comparing my active music-listening times over the years.

Intro

Going back to my last.fm data, I wanted to figure out the times of day I listened to the most music throughout the years. For now, the way I approached it was to generate polar plots with the binned frequencies of the song counts across the hours.

Development

Loading data

We can get started with the data already cleaned as described in my previous post in which the CSV looks like this:

We load the data and delete repeated entries (as some rows seem to be duplicated by the scrobbler from time to time):

(yLo, yHi) = ((2019, 1), (2020, 1))
(WIDTH, HEIGHT, RESOLUTION) = (3840, 2160, 500)
HOURS_OFFSET = 6
##############################################################################
# Read artists file
##############################################################################
data = pd.read_csv(
    stp.DATA_PATH + stp.USR + '_cln.csv',
    parse_dates=[3]
)
data = data.drop_duplicates()

Filtering dates

Now, we filter the specific range of time (year and month) that we are going to analyze using datetime objects:

data = data.drop_duplicates()
msk = [
    (
        (i.date() >= datetime.date(yLo[0], yLo[1], 1)) and 
        (i.date() < datetime.date(yHi[0], yHi[1], 1))
    ) 
    if (type(i) is not float) else (False) for i in data['Date']
]
dates = data.loc[msk]["Date"]

Binning Frequencies

Once we have these filtered entries, we can bin them by hour and then count the times a song was played in the hourly interval:

hoursPlays = sorted([i.hour for i in dates if (type(i) is not float)], reverse=True)
hoursFreq = [hoursPlays.count(hD) for hD in list(range(23, -1, -1))]

Polar Plot

We are ready for the fun part. The easiest way to create a visualization that resembles a clock, is to do a polar plot. To do this, we divide the “slices” into 24 pieces and scale the radius of each slice to the playcount corresponding to the time interval. Additionally, we can create a color palette that scales with the same count to emphasize the difference between the slices:

N = 24
(minFreq, maxFreq) = (min(hoursFreq), max(hoursFreq))
fig = figure(figsize=(8, 8), dpi=RESOLUTION)
ax = fig.add_axes([0.2, 0.1, 0.8, 0.8], polar=True)
step=  2*np.pi/N
(theta, radii, width) = (
    np.arange(0.0+step, 2*np.pi+step, step),
    hoursFreq,
    2*np.pi/24 -.001
)
bars = ax.bar(
    theta, radii, width=width, 
    bottom=0.0, zorder=25, edgecolor='#ffffff77', lw=.75
)
rvb = aux.colorPaletteFromHexList(
    ['#bbdefb', '#64b5f6', '#2196f3', '#1976d2', '#0d47a1', '#001d5d']
)
for r, bar in zip(radii, bars):
    bar.set_facecolor(rvb(r/(np.max(hoursFreq)*1)))
    bar.set_alpha(0.75)

This gets us halfway there but, as we can see, this is still neither attractive, nor informative:

Axes Styling

For our final step, we are going to rotate the axes, clean them up, print a title, and add some auxiliary lines to make the plot easier to read:

shades = 12
step =  np.pi/shades
ax.bar(
    np.arange(0.0+step, 2*np.pi+step, step), 1.15*maxFreq,
    width=np.pi/shades,
    alpha=.2, edgecolor="black", ls='-', lw=.5,
    zorder=-1
)
ax.set_theta_zero_location("N")
fig.patch.set_facecolor('#ffffff')
ax.set_ylim(0, maxFreq*1.0035)
ax.set_yticks(np.arange(0, maxFreq, maxFreq*.25))
ax.set_yticklabels([])
ax.set_xticks(np.arange(np.pi*2, 0, -np.pi*2/24))
ax.set_xticklabels(np.arange(0, 24, 1))
[ax.grid(which='major', axis=j, color='#000000', alpha=0, lw=.5, ls='--', zorder=15) for j in ['x', 'y']]
ax.tick_params(direction='in', pad=10)
ax.tick_params(axis="x", labelsize=17.5, colors='#000000ff')

Gallery

Code repo