ProblemSet3MySolution.py

#

Music Computation Code from September 26, 2017

#

My solution to Problem Set 3

#

My approach was to plot the percentage of ascending departures from a pitch as a function of distance from the median. As pitch height increases relative to the median, the percentage of ascending departures should decrease. Multiple plots are generated with differing constraints on the size of the approaching interval. Comparing these different plots can yield insight into the relative effects of regression to the mean and post-skip reversal for skips of various sizes.

from music21 import *
from collections import Counter
import os
#

matplotlib and numpy

#

"Matplotlib is a Python 2D plotting library which produces publication quality figures in a variety of hardcopy formats and interactive environments across platforms. For simple plotting the pyplot module provides a MATLAB-like interface." See https://matplotlib.org/ for more details.

import matplotlib.pyplot as plt
#

NumPy is the fundamental package for scientific computing with Python. See http://www.numpy.org/ for more details.

import numpy as np
#

We will also go over some of the basic uses of Matplotlib and NumPy in class.

#

Basic class for the experiment

#

Class that brings together four pieces of information about every pitch (current): its predecessor (previous), successor (following), and the median pitch (median) of the song in which current occurs.

class PitchNeighborhood(object):
#
    def __init__(self, previous, current, following, median):
        self.previous = previous
        self.current = current
        self.following = following
        self.median = median
#

Basic methods for obtaining intervals from the previous pitch, to the following pitch, and from the median pitch.

    def getFromPrevious(self):
        return self.current - self.previous
#
    def getToNext(self):
        return self.following - self.current
#
    def getFromMedian(self):
        return self.current - self.median
#

Create a list of PitchNeighborhoods for (almost) every pitch

#

Given a flattened Stream of notes, creates an instance of the PitchNeighborhood class for each pitch and adds the class to the list neighborhoods.

def update_neighborhoods(neighborhoods, song):
#
    pitches = [n.pitch.ps for n in song]
#

NumPy has a function for finding the median of list or NumPy array.

    med_pitch = np.median(pitches)
#

Ignore if the median pitch is not an actual pitch. This avoids keeping track of fractional distances from the median. Only 22 out of 704 songs are omitted.

    if int(med_pitch) != med_pitch:
        return
#

Create PitchNeighborhood for every pitch (other than the beginning and ending pitches in each song) and append to the neighborhoods list.

    for i in range(1, len(pitches) - 1):
        p = PitchNeighborhood(pitches[i-1], pitches[i], pitches[i+1], med_pitch)
        neighborhoods.append(p)
#

Main function for generating figures

#

Function to test regression to the mean versus post-skip reversal. neighborhoods is a list of instances of the PitchNeighborhood class. leaps is a list of numbers corresponding to plots of the graphs. If a member of leaps, n, is positive, the corresponding plot includes data only for those pitches approached by an ascent of at least n. For negative n, the plot includes data only for those pitches approached by a descent of at least abs(n). If n is zero, the plot includes data for all pitches.

def experiment(neighborhoods, leaps, figure):
#

Create Counter objects to keep track of frequencies of ascents and all all non-unison motions. This is to calculate percentages of non-unison motions that ascend.

    ascents = Counter()
    ascents_descents = Counter()
#

Details for plotting the figures.

    plt.figure(figure)
    plt.subplot(121)

    
    for leap in leaps:
#

sign is a NumPy function for determining the sign of a number.
>>> np.sign(-2)
-1
>>> np.sign(2)
1
>>> np.sign(0)
0

        sign = np.sign(leap)
#

Loop to update the ascents and ascents_descents counters with pitches that are approached by an interval of at least size leap.

        for p in neighborhoods:
#

Filters the data to be included in the plot.

            if sign * p.getFromPrevious() >= sign * leap:
#

Pitches left by an ascent.

                if p.getToNext() > 0:
                    ascents.update([p.getFromMedian()])
#

Pitches not followed by the same pitch.

                if p.getToNext() != 0:
                    ascents_descents.update([p.getFromMedian()])
#

The following loop deletes any keys in the ascents_descents counter if there are fewer than 10 instances of the key. Keys in the counters correspond to distances from the median. Deleting the keys is to avoid plotting data with a very small sample size.

#

Note that this for loop is iterating over a list of keys in the counter. Since we are potentially altering the counter (by deleting keys), it would create problem to iterate over the keys directly with
for key in ascents_descents.

        for key in list(ascents_descents):
            if ascents_descents[key] < 10:
                del ascents_descents[key]
#

Create values for x- and y-axes of the figure.

        xvalues = sorted(ascents_descents)
        yvalues = [round(float(ascents[x]) / ascents_descents[x], 2)
                   for x in xvalues]
#

Generate legend for the figure.

        if leap == 0:
            lab = 'All pitches'
        elif leap > 0:
            lab = 'Approached by ascent of ' + str(leap) + ' or more'
        else:
            lab = 'Approached by descent of ' + str(leap) + ' or more'
#

Create the plot corresponding to the current value of leap.

        plt.plot(xvalues, yvalues, label = lab)
#

Clear the two counters for the next value of leap.

        ascents.clear()
        ascents_descents.clear()
#

Label axes, provide title, and place legend in the figure.

    plt.xlabel('Distance of pitch from median')
    plt.ylabel('Percent of motions from pitch that ascend')
    plt.title('Regression to the mean')
    plt.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.)
#

Write figure to the hard drive.

    plt.savefig('Figure ' + str(figure))
#

Pulling it all together

#

Parse songs in the 'boehme' subdirectory. (Substitute the appropriate file path for your computer.)

dir_name = '/Users/ccallender/Desktop/europa/deutschl/boehme/'
filenames = [filename for filename in os.listdir(dir_name)]
paths = [dir_name + filename for filename in filenames if filename.endswith('.krn')]
songs = [converter.parse(path).flat.notes for path in paths]
print("Songs loaded")
#

Create and update neighborhoods for each song.

neighborhoods = []
for song in songs:
    update_neighborhoods(neighborhoods, song)
#

Figure 1: All pitches regardless of approach. Base plot demonstrating influence of regression to the mean

figure = 1
experiment(neighborhoods, [0], figure)
#

Figures 2-5: Base plot plus pitches approached by leaps of at least a minor third, perfect fourth, perfect fifth, and minor sixth, respectively.

for leap in [3, 5, 7, 8]:
    figure += 1
    experiment(neighborhoods, [0, leap, -leap], figure)