# 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')

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]

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, , 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)

```