Daser Ecce Nunc Benedicite Notebook with CRIM Intervals

Author

Richard Freedman

Published

January 19, 2025

A Digital Supplement for “Lasso takes a Second Look: An Imitation of an Imitation Mass?”

Marcel Klinke (Heidelberg University) and Richard Freedman (Haverford College)

This digital resource supplements our study of the connections between Ludwig Daser’s four-voice motet, Ecce nunc benedicite and two related imitation masses that borrow from it: one by Daser himself and another by his music court successor, Orlando di Lasso. In it, we use CRIM Intervals, a Python library that builds upon Mike Cuthbert’s excellent music21 to reveal connections and contrasts among these pieces as a corpus.

CRIM Intervals, briefly stated, is a pattern finding engine for musical scores, with an emphasis on the kinds of melodic, harmonic, and contrapuntal patterns found in Renaissance polyphony. It has been developed as a primary data analysis tool for Citations: The Renaissance Imitation Mass (http://crimproject.org and https://sites.google.com/haverford.edu/crim-project/home), directed by Freedman in partnership with dozens of researchers and students around the world, and has been used successfully for a variety of projects put qualitative and quantitative approaches to the study of these repertories into conversation with each other. Read the results of this work via the CRIM Essays and Experiments, which assembles recent work by over two-dozen scholars and students from around the world.

Our Digital Inquiry in Brief

The core of our argument about the Masses by Daser and Lasso appear in the body of our published paper. Here we show the code behind our work, along with charts, networks, and annotated musical examples that are best represented in their interactive, digital form.

In particular you will find:

Melodic Ngrams

  • Activity Maps of Melodic Ngrams. Ngrams are strings of length ‘n’, in this case consisting of successions of diatonic intervals in each voice part. Here we take a selective view, focusing on ngrams that occur after a rest or section break, since these provide the clearest indication of where points of imitation occur. These ‘entry’ ngrams (as they are called in CRIM Intervals) are a reliable way of comparing the treatment of melodic ideas across the corpus. The activity maps show us where each entry ngram appears in each piece, and so allows us to see where pieces are similar (and not).
  • Networks of Pieces based on Shared Ngrams. Here we take a higher level view of connections between movements: nodes represent the individual movements, and edges represent the ngrams they share. Such representations can tell help is divide the corpus into ‘communities’ based on shared melodic relatedness.

Cadential Patterns

  • Cadence Radar Plots. These take a global view of the cadences in a set of pieces, without regard to their succession in time. They are a good way to understand the tonal ‘fingerprint’ of a piece.
  • Cadence Progress Plots. Now the cadences are seen in order with types and tones identified by distinctive color and shape. These show us when sets of related pieces (such as Kyrie movements) follow similar cadential plans.
  • Cadence Inventories. Tables that give highly detailed information about each cadence in each piece (in their order of appearance) along with a link to a rendering of the passage in graphical notation (with the structural notes highlighted for review and study.
  • Networks of Pieces based on Pairs of Cadences. Here we look at successive pairs of cadences to reveal connections among pieces.

From Jupyter Notebook to HTML Output

Here we use CRIM Intervals in a Jupyter Notebook, which allows us to run all our functions in a browser-like environment. There are two kinds of ‘cells’ in these notebooks:

  • Markdown cells (like the one you are reading), which can contain commentary of various kinds
  • Code cells (which variously import files, process them, and produce results)

The results of this work are in turn saved as interactive HTML (and published to the web in the form you are reading) using Quarto, a powerful system for transforming code into various publication forms. By default in the Quarto system we hide the code cells. To view the details click the interactive triangle wherever you see Code. The code cannot be run in the HTML version, but it will help you understand how the various representations are produced.

Import Python Libraries

Our first step is to import all of the relevant Python libraries needed for our project.

Code
# intervals imports
from crim_intervals import * 
from crim_intervals import importScore 
from crim_intervals import main_objs
import crim_intervals.visualizations as viz

# import other libraries
from community import community_louvain
from copy import deepcopy
from IPython.display import SVG
import altair as alt
import glob as glob
import numpy as np
import os
import pandas as pd
import re
import networkx as nx
import requests

# code for Quarto and the charting tools
alt.renderers.enable('default')
import plotly.io as pio
pio.renderers.default = "plotly_mimetype+notebook_connected"

import warnings
warnings.filterwarnings('ignore')
    

Import the MEI Files to Analyze

CRIM Intervals works with a variety of encoded scores. Here in the CRIM Project we use Music Encoding Initiative (MEI) files.

Here we find the files (as stored in a local folder of our Jupyter Hub) and identify the model (model_file) and corresponding pairs of related Mass movements (mass_files_pairs). These sets are used in subsequent steps of our analysis.

Code
file_list = glob.glob('Music_Files/*')

# the model
model_file = file_list[-1]

# function to pair the mass files
def sort_filenames(file_list):
    # Sort the filenames based on the last two digits of the main part
    return sorted(file_list, key=lambda x: int(re.search(r'\d{1}$', x.rsplit('.', 1)[0]).group()))

mass_files_pairs = sort_filenames(file_list[:-2])


Exploring Melodic Ngrams

Here are the settings used in this experiment. These determine the kind and length of the ngrams, along with some other basic conditions for our search. Learn more about these settings via the CRIM Intervals tutorials for ngrams.

  • n=2 ==> determines the number of intervals in each ngram
  • kind='d' ==> the type of interval–in this case diatonic
  • thematic = True ==> when thematic is ‘true’ we return only the ngrams that are repeated
  • anywhere = False ==> when anywhere is set to ‘false’ we return only the ngrams that occur after a rest or after a double bar or other sectional break
  • combineUnisons=False==> when combineUnisons is ‘true’ we combine repeated notes; here we do the opposite

How to Read the Activity Maps

The ngrams are shown as a kind of diagrammatic score:

  • Each ngram appears as a solid band of color according to the voice in which it appears. The uppermost voice it at the top of the chart; the lowest voice at the bottom (see the voicepart labels at the left). The ‘key’ at the top and right shows which color corresponds to which ngram. Note: in pieces with more than 10 different ngrams, some colors repeat or are hard to distinguish from each other.

  • The ngrams are arranged in sequence from left (the start of the piece) to the right (the end). The scale at the bottom of the chart shows the count of quarter-note values from start to finish. In music21 (and thus in CRIM Intervals) these ‘offset’ values help us track events in time. In the 4/2 bars used in our editions, each 8 ‘offset’ values correspond to a full bar. Thus each span of 50 offsets (as shown in the x-axis of the chart) represents about six bars of notation. Note that (as in many computational languages, the first offset is ‘0’. Also note that each piece (regardless of its total duration) is scaled so that the first offset is at the left of the chart and the last offset is at the right. Thus pieces of quite different length appear to have the same overall dimensions.

  • Each ngram is associated with a distinct color, and the relative placement of the ngrams among the voices can be used to imagine the kinds of contrapuntal patterns heard at each moment in the piece. Note: in pieces with more than 10 different ngrams, some colors repeat or are hard to distinguish from each other.

  • The activity charts are dynamic: click on a given color to isolate that ngram to see where it appears in the piece.

Melodic Ngrams in Daser’s Motet

Code
# the ngram conditions
n=2
combineUnisons=False
kind='d'
thematic = True
anywhere = False

# load the model_file as CRIM Intervals object
model = importScore(model_file)

# find entries for model according to the conditions set above
nr = model.notes(combineUnisons=combineUnisons)
mel = model.melodic(df=nr, kind=kind, compound=False, unit=0, end=False)
mod_mel_ngrams = model.ngrams(df=mel, n=n, exclude=['Rest'])
mod_entry_ngrams = model.entries(df=mel, n=n, thematic=thematic, anywhere=anywhere, exclude=['Rest'])
mod_mel_ngrams_duration = model.durations(df=mel, n=n, mask_df=mod_entry_ngrams)
mod_entries_stack = list(mod_entry_ngrams.stack().unique())

# print details about the piece and show the chart
print(model.metadata['composer'])
print(model.metadata['title'])
display(viz.plot_ngrams_heatmap(mod_entry_ngrams, mod_mel_ngrams_duration, 
                        selected_patterns=mod_entries_stack,
                        voices=[],
                        includeCount=True))
Ludovicus Daser
Ecce nunc benedicite

Activity Map of Ngrams in Daser’s Motet

Comparing Melodic Ngrams in the the Motet with Those in the Mass Movements

Here we produce ‘pairs’ of charts, comparing each movement of each mass with the motet–for instance we compare the motet with Daser’s Kyrie, then with Lasso’s. Then we move to the Gloria, etc.

In this case we limit the results such that only the ngrams that appear in both the motet and each given Mass movement are shown. These are maps of the matching ngrams, and thus tell us what the Mass movements share with their model.

Code
# fig-cap: "Activity Maps for the Motet compared with Each Mass Movement"

# the ngram conditions
n=2
combineUnisons=False
kind='d'
thematic = True
anywhere = False


# find entries for model, which is the last piece in our list
model = importScore(model_file)
nr = model.notes(combineUnisons=combineUnisons)
mel = model.melodic(df=nr, kind=kind, compound=False, unit=0, end=False)
mod_mel_ngrams = model.ngrams(df=mel, n=n, exclude=['Rest'])
mod_entry_ngrams = model.entries(df=mel, n=n, thematic=thematic, anywhere=anywhere, exclude=['Rest'])
mod_mel_ngrams_duration = model.durations(df=mel, n=n, mask_df=mod_entry_ngrams)
mod_entries_stack = list(mod_entry_ngrams.stack().unique())

# find entries mass movements.  
# here we will display the results so that the motet is compared with each Kyrie, then each Gloria, etc
# in each case Lasso's Mass Movement will come first, then Daser's

mass_movements = mass_files_pairs
for movement in mass_movements:       
    m_movement = importScore(movement)
    nr = m_movement.notes(combineUnisons=combineUnisons)
    mel = m_movement.melodic(df=nr, kind=kind, compound=False, unit=0, end=False)
    mass_mvmt_mel_ngrams = m_movement.ngrams(df=mel, n=n)
    mass_mvmt_entries = m_movement.entries(df=mel, n=n, thematic=thematic, anywhere=anywhere)
    mass_mvmt_mel_ngrams_duration = m_movement.durations(df=mel, n=n, mask_df=mass_mvmt_entries)
    mass_mvmt_entries_stack = mass_mvmt_entries.stack()
      
    # find shared entries
    shared_entries = list(set(mass_mvmt_entries_stack).intersection(mod_entries_stack))
    
    # print model metadata and chart
    print(model.metadata['composer'])
    print(model.metadata['title'])
    
    display(viz.plot_ngrams_heatmap(mod_entry_ngrams, 
                                    mod_mel_ngrams_duration, 
                        selected_patterns=shared_entries,
                        voices=[], 
                                    includeCount=False))
      
    # # print mass movement metadata and chart
    print(m_movement.metadata['composer'])
    print(m_movement.metadata['title'])
    display(viz.plot_ngrams_heatmap(mass_mvmt_entries, 
                                    mass_mvmt_mel_ngrams_duration, 
                                selected_patterns=shared_entries,
                                voices=[], 
                                    includeCount=False)) 
Ludovicus Daser
Ecce nunc benedicite
Ludovi[cus] Daser
Daser Missa Ecce nunc Benedicite:  Kyrie
Ludovicus Daser
Ecce nunc benedicite
Orlandus Lassus
Lasso Missa Ecce nunc benedicite:  Kyrie
Ludovicus Daser
Ecce nunc benedicite
Ludovi[cus] Daser
Daser Missa Ecce nunc Benedicite: Gloria
Ludovicus Daser
Ecce nunc benedicite
Orlandus Lassus
Lasso Missa Ecce nunc benedicite: Gloria
Ludovicus Daser
Ecce nunc benedicite
Ludovi[cus] Daser
Daser Missa Ecce nunc Benedicite:  Credo
mei.base: WARNING: Importing <tie> without @startid and @endid is not yet supported.
Ludovicus Daser
Ecce nunc benedicite
Orlandus Lassus
Lasso Missa Ecce nunc benedicite: Credo
Ludovicus Daser
Ecce nunc benedicite
Ludovi[cus] Daser
Daser Missa Ecce nunc Benedicite: Sanctus
Ludovicus Daser
Ecce nunc benedicite
Ludovi[cus] Daser
Daser Missa Ecce nunc Benedicite: Agnus Dei
Ludovicus Daser
Ecce nunc benedicite
Orlandus Lassus
Lasso Missa Ecce nunc benedicite: Agnus

Interpreting the Activity Maps

Learning to read these kinds of charts takes some time. But after careful review of them we can conclude that:

  • The Kyrie movements show a strikingly clear relationship to the motet, albeit with a limited number of soggetti used in each case. But they are the same soggetti in each case!

  • The Sanctus and Agnus movements are quite selective in their borrowing. Again, we see very similar borrowing between Daser and Lasso.

  • The Gloria and Credo movements are more subtle and complex in their borrowing. It is nevertheless notable that the first soggetto of the model does not figure in prominently in these movements.


A Network of Pieces based on Shared Ngrams

Now let’s look at the corpus as a network of pieces. Networks are useful for the ways in which they make connections among items visible. Remember that:

The items with the most connections will tend to appear at the center of the graph.

In this network, the nodes are the pieces and edges represent the ngrams they share.

The thickness of the edges varies with the number of shared ngrams

The colors distinguish ‘communities’ of pieces that are highly related. These are created using the Louvain Communities Detection algorithm.

As a reminder, here are the basic ngram settings used in this network:

n=2
combineUnisons=False
kind='d'
thematic = True
anywhere = False
Code
# the ngram conditions
n=2
combineUnisons=False
kind='d'
thematic = True
anywhere = False

# now we build a list of all of the ngrams in each piece
list_series = []
for item in file_list:
    piece = importScore(item)
    title = piece.metadata['composer'] + ": " + piece.metadata['title']
    # find entries for model
    nr = piece.notes(combineUnisons=combineUnisons)
    mel = piece.melodic(df=nr, kind=kind, compound=False, unit=0, end=False)
    mod_mel_ngrams = piece.ngrams(df=mel, n=n, exclude=['Rest'])
    mod_entry_ngrams = piece.entries(df=mel, n=n, thematic=thematic, anywhere=anywhere, exclude=['Rest'])
    mod_entry_ngrams.fillna('')
    mod_entry_ngrams['title'] = title
    # Set the new column as the index
    mod_entry_ngrams.set_index(['title'], inplace=True)
    stacked_entries = mod_entry_ngrams.stack()
    list_series.append(stacked_entries)

data = pd.concat(list_series)

# and now, a network in which the nodes are the pieces and edges represent the ngrams they share.  
# the thickness of the edges varies with the number of shared ngrams
# the colors distinguish 'communities' of pieces that are highly related

df = pd.DataFrame(data)
df = df.reset_index()
df.drop('level_1', axis=1, inplace=True)
df = df.rename(columns={0: 'ngram'})

#define the function to convert tuples to strings
def convertTuple(tup):
    out = ""
    if isinstance(tup, tuple):
        out = '_'.join(tup)
    return out  
# clean the tuples
df['ngram'] = df['ngram'].apply(convertTuple)

# Group by 'ngram' and extract a list of unique titles for each group
grouped_titles = df.groupby('ngram')['title'].unique().reset_index(name='titles')

# Generate all pairs of titles for each group
all_pairs = []
for _, row in grouped_titles.iterrows():
    pairs = list(combinations(row['titles'], 2))
    all_pairs.append((row['ngram'], pairs))

# Create a new DataFrame with the results
result_df = pd.DataFrame(all_pairs, columns=['ngram', 'title_pairs'])
# remove the empty pairs
df_filtered = result_df[result_df['title_pairs'].apply(len) > 0]

# explode the complicated lists of tuples, effectively 'tyding' the data
exploded_df = df_filtered.explode('title_pairs')

# get the counts of each pair, which provides the basis of the weights
pair_counts = exploded_df['title_pairs'].value_counts()

# limit to high scoring pairs (>3)
pair_counts = pair_counts[pair_counts >= 6]

# Adding Louvain Communities
def add_communities(G):
    G = deepcopy(G)
    partition = community_louvain.best_partition(G)
    nx.set_node_attributes(G, partition, "group")
    return G

# Create an empty NetworkX graph
G = nx.Graph()


# Add nodes and assign weights to edges
for pair, count in pair_counts.items():
    # Directly unpacking the tuple into node1 and node2
    node1, node2 = pair
    # Adding nodes if they don't exist already
    if node1 not in G.nodes:
        G.add_node(node1)
    if node2 not in G.nodes:
        G.add_node(node2)
    # Adding edge with weight
    G.add_edge(node1, node2, weight=count)

# Adjusting edge thickness based on weights
for edge in G.edges(data=True):
    edge[2]['width'] = edge[2]['weight']
    
G = add_communities(G)

# set display parameters
ngram_map = Network(notebook=True,
                   width="800",
                          height="800",
                          bgcolor="black", 
                          font_color="white")

# Set the physics layout of the network
ngram_map.set_options("""
{
"physics": {
"enabled": true,
"forceAtlas2Based": {
    "springLength": 1
},
"solver": "forceAtlas2Based"
}
}
""")

ngram_map.from_nx(G)
# # return the network
ngram_map.show("ngram_map.html")
mei.base: WARNING: Importing <tie> without @startid and @endid is not yet supported.

Network of the Daser-Lasso Corpus based on Shared Ngrams

Interpreting the Ngram Network

Networks like these can be challenging to interpret.

The thickness of the lines (edges) of connection tells us how many of the ngrams are shared between various movements (nodes). The Gloria and Credo movements share a particularly large number of connections, despite that they are also the longest movements. This suggests that the two movements are particular close.

The colors represent some latent communities of connectedness, as identified by the Louvain Community Detection Algorithm. The details of that method are too complex to detail, but the suggestion that two Kyries in turn share much with Lasso’s Gloria makes sense (since the latter movement borrows extensively from Daser’s model, and the two Kyries do, too.

The inverted form of Soggetto 3 found in Lasso’s Credo is almost certainly not part of this network, since such representations only show us things that are shared among movements, not things unique to indivdual pieces.

Read more about Network graphs and CRIM Intervals via the Tutorials pages.


Exploring Cadences

CRIM Intervals can identify Renaissance cadence types. It does this in steps:

  • First we find all the ‘contrapuntal’ (also called ‘modular’) ngrams in a piece; a contrapuntal ngram records the harmonic and melodic intervals that are made when two voices sound together. Learn more about contrapuntal/modular ngrams in the CRIM Intervals Tutorials

  • Then we compare each of these ngrams with a library cadence formulas, each of which corresponds to a particular contrapuntal pattern.

  • Each cadence we identify is in turn added to a summary table that reports the cadence type, the final goal tone, the specific voice roles involved, and contextual information about the place of the cadence in the piece and relative to others. The table also includes a useful link that renders the cadence in graphical notation, with the relevant active notes of the cadence highlighted in red. Click on the value in the CadType column to view a given cadence.

Learn more about the CRIM intervals cadence finder here.

You might also want to two essays produced by CRIM participants:

A Radar Plot of Cadence Types and Tones

These plots help us see the ‘tonal signature’ of each piece. The positions around the edge of the circle represent the tones (or tones and types). The distance from the center varies with the number of cadences of that type.

Here we show two kinds of plots for each pair of movements (Kyrie, Gloria, etc) in relation to the model:

  • The simple plots show just the final tone of the cadences. As noted above, the distance from the center tells us how many cadences there are of each type. The resulting shapes are fairly easy to see and interpret.

  • The complex plots show the type of cadence as well as the final tone. The resulting spaces are quite elaborate, but they convey more detailed information about the similarities and differences among the pieces.

In all cases the overall impression is more important than the precise details of counts, since the cadence method sometimes results in both false positives (things we would not accept as examples of the given type) and false negatives (legitimate cadences that the tool could not identify).

If combinedType is True you will see all the cadence subtype details. If displayAll is True you will see all possible cadence tones.

corpus.compareCadenceRadarPlots(
                                combinedType = False, 
                                displayAll = False, 
                                renderer = "iframe")
Code
movement_ids = ['_1', '_2', '_3', '_4', '_5']

model =  'Music_Files/CRIM_Model_0051.mei'
file_paths = glob.glob('Music_Files/*.*')
# The string we're looking for
for movement_id in movement_ids:
    # Filter the list of file paths
    filtered_paths = [path for path in file_paths if str(movement_id) in path]
    filtered_paths.append(model)
    corpus = CorpusBase(filtered_paths)

    corpus.compareCadenceRadarPlots(
                                combinedType = False, 
                                displayAll = True, 
                                renderer = "iframe")
    
    corpus.compareCadenceRadarPlots(
                                combinedType = True, 
                                displayAll = True, 
                                renderer = "iframe")
    
    print('______________________________________________________________________________________________________________________________')

A Radar Plot of Cadence Types and Tones in the Corpus

______________________________________________________________________________________________________________________________
______________________________________________________________________________________________________________________________
mei.base: WARNING: Importing <tie> without @startid and @endid is not yet supported.
______________________________________________________________________________________________________________________________
______________________________________________________________________________________________________________________________
______________________________________________________________________________________________________________________________

Interpretation of The Radar Plots

The Kyrie plots show a very close correspondence between the proportion and tone of cadences in each Kyrie and in the model.

In the Gloria movements, Daser’s cadential footprint aligns almost perfectly with that of his motet. Lasso’s differs slightly (with somewhat more G than D cadences), but follows the same overall countours in both charts.

The Credo movements are each much longer than the motet, and so their overall footprint is larger. They show the same overall shape. The similarity between the simple plots of two Mass movements is striking. But the complex cadence plot reveals that Lasso has departed significantly from Daser’s Mass and his model alike, adding cadences to A and C with new types.

The Sanctus here Lasso’s movement contains far fewer cadences than either Daser’s motet or his Mass Sanctus. But it is notable that Lasso’s Sanctus follows closely the shape of Daser’s, and not the motet.

The Agnus dei movements likewise show the affinity of the two masses to each other. The complex plot confirms how much these resemble each other, and how much they depart from the tonal shape of the motet.

Cadence Progress Plots

Here we show the cadences in each piece as they occur in time, . The various tones and types are represented via distinctive position and markings.

In these charts, each plot features three pieces: the motet and the relevant pair of movements from the two masses.

  • The color legend (top left) shows which cadence markers belong to which piece.
  • The shape legend (top right) shows the type of cadence represented by each shape.
  • The y-axis shows the final tone of each cadence.
  • The x-axis shows the relative position of each cadence from beginning (at the left) to the end (at the right).

Note that since the x-axis scale is always adjusted to match the full length of each piece, we should not expect the corresponding cadences to perfectly overlap. Instead we look for ‘parallel’ repeating patterns in different colors, as shown in this example, where we note a similar succession of colors and shapes in green, orange and blue:

screenshot_3763.png
Code
movement_ids = ['_1', '_2', '_3', '_4', '_5']

model =  'Music_Files/CRIM_Model_0051.mei'
file_paths = glob.glob('Music_Files/*.*')
# The string we're looking for
for movement_id in movement_ids:
    # Filter the list of file paths
    filtered_paths = [path for path in file_paths if str(movement_id) in path]
    filtered_paths.append(model)
    corpus = CorpusBase(filtered_paths)
    corpus.compareCadenceProgressPlots(includeType = True)

mei.base: WARNING: Importing <tie> without @startid and @endid is not yet supported.

Interpreting the Progress Plots

The progress plots contain much new information about the succession of cadences no less than their distribution.

  • In the case of the Kyrie movements, we see striking parallels between the mass movements (the succession of cadences to D, G, C in blue and orange).
  • The Gloria movements show a pair of strikingly similar Phrygian cadences to A that have now analogue in the motet.
  • The Credo movements show some clear connections (see the almost completely overlapping symbols in orange and blue). But Lasso’s Credo also departs from both the model and Daser’s mass in its use of cadences to A and even E. The latter is especially unexpected in the context of a G-final piece.
  • The Sanctus movements are rather different from each other, most obviously because of the small number of cadences in Lasso’s piece. On the other hand we notice a strong set of parallels and overlaps between Daser’s Sanctus and his motet.
  • The Agnus dei movements, as we have noticed in the case of the Radar Plots are quite similar to each other, and also have far fewer cadences overall than the motet.

Table of Cadences with Annotated Scores

CRIM Intervals can produce a summary of cadences with a dynamic ‘linked example’.

Click the CadType cell in each row to see that particular example rendered with red highlights indicating which patterns and voices were identified by the algorithm as the cadence in question.

Code
# We also need to load the files from GIT, for the LinkExample function

prefix = 'https://raw.githubusercontent.com/CRIM-Project/CRIM-online/refs/heads/dev/crim/static/mei/MEI_4.0/'

sorted_files = [model_file] + mass_files_pairs
for path in sorted_files:
    piece_id = os.path.basename(path) 
    url = prefix + piece_id  
    piece = importScore(url)
    cd=piece.cadences()
    
    # filter for a given type or tone
    # cd = cd.loc[cd['CVFs'] == 'CTb']
    
    print(piece_id)
    print(piece.metadata['composer'])
    print(piece.metadata['title'])
    piece.linkExamples(df=cd, piece_url=url)
CRIM_Model_0051.mei
Ludwig Daser
Ecce nunc benedicite
CadType LeadingTones CVFs Low RelLow Tone RelTone TSig Measure Beat Sounding Progress SinceLast ToNext
Last
66.0 Clausula Vera 0.0 TC F3 -M2 D P5 4/2 9.0 2.0 4.0 0.108553 66.0 6.0
72.0 Clausula Vera 1.0 CT D3 -P4 D P5 4/2 10.0 1.0 4.0 0.118421 6.0 12.0
84.0 Clausula Vera 1.0 CT G3 P1 G P8 4/2 11.0 3.0 4.0 0.138158 12.0 28.0
112.0 Authentic 1.0 CTB G3 P1 G P8 4/2 15.0 1.0 4.0 0.184211 28.0 36.0
148.0 Clausula Vera 1.0 CT D4 P5 D P5 4/2 19.0 3.0 2.0 0.243421 36.0 12.0
160.0 Clausula Vera 1.0 TC E4 M6 G P8 4/2 21.0 1.0 3.0 0.263158 12.0 24.0
184.0 Clausula Vera 1.0 CT D3 -P4 D P5 4/2 24.0 1.0 3.0 0.302632 24.0 40.0
224.0 Clausula Vera 1.0 tCT G3 P1 G P8 4/2 29.0 1.0 4.0 0.368421 40.0 12.0
236.0 Clausula Vera 1.0 CT C3 -P5 C P4 4/2 30.0 3.0 4.0 0.388158 12.0 20.0
256.0 Altizans Only 1.0 ATx F4 m7 C P4 4/2 33.0 1.0 3.0 0.421053 20.0 24.0
280.0 Authentic 1.0 tCB C3 -P5 C P4 4/2 36.0 1.0 4.0 0.460526 24.0 16.0
296.0 Authentic 1.0 CTB G3 P1 G P8 4/2 38.0 1.0 4.0 0.486842 16.0 20.0
316.0 Abandoned Authentic 0.0 Czx G4 P8 G P8 4/2 40.0 3.0 2.0 0.519737 20.0 28.0
344.0 Clausula Vera 0.0 CT G3 P1 D P5 4/2 44.0 1.0 4.0 0.565789 28.0 12.0
356.0 Clausula Vera 0.0 TC B-3 m3 D P5 4/2 45.0 3.0 4.0 0.585526 12.0 12.0
368.0 Evaded Authentic -1.0 cB D3 -P4 NaN NaN 4/2 47.0 1.0 4.0 0.605263 12.0 12.0
380.0 NaN NaN CTP D3 -P4 A M2 4/2 48.0 3.0 3.0 0.625000 12.0 36.0
416.0 Evaded Authentic 1.0 CTb G3 P1 D P5 4/2 53.0 1.0 4.0 0.684211 36.0 12.0
428.0 Clausula Vera 1.0 tCT D3 -P4 D P5 4/2 54.0 3.0 4.0 0.703947 12.0 20.0
448.0 Clausula Vera 1.0 CT G3 P1 G P8 4/2 57.0 1.0 3.0 0.736842 20.0 16.0
464.0 Clausula Vera 1.0 CT C3 -P5 C P4 4/2 59.0 1.0 2.0 0.763158 16.0 12.0
476.0 Abandoned Authentic -1.0 cx E4 M6 NaN NaN 4/2 60.0 3.0 2.0 0.782895 12.0 8.0
484.0 Evaded Clausula Vera -1.0 Tcx E4 M6 NaN NaN 4/2 61.0 3.0 2.0 0.796053 8.0 8.0
492.0 Evaded Clausula Vera -1.0 Tc A3 M2 NaN NaN 4/2 62.0 3.0 3.0 0.809211 8.0 8.0
500.0 Evaded Altizans Only -1.0 aT G3 P1 NaN NaN 4/2 63.0 3.0 4.0 0.822368 8.0 20.0
520.0 Clausula Vera 1.0 CT G3 P1 G P8 4/2 66.0 1.0 4.0 0.855263 20.0 96.0
CRIM_Mass_0052_1.mei
Roland de Lassus
Missa Ecce nunc benedicite: Kyrie
CadType LeadingTones CVFs Low RelLow Tone RelTone TSig Measure Beat Sounding Progress SinceLast ToNext
Last
39.0 Evaded Altizans Only -1 aT D3 -P4 NaN NaN 4/2 5.0 4.5 5 0.139286 39.0 25.0
64.0 Authentic 1 CTB D3 -P4 D P5 4/2 9.0 1.0 5 0.228571 25.0 16.0
80.0 Clausula Vera 1 CT G3 P1 G P8 4/2 11.0 1.0 6 0.285714 16.0 36.0
116.0 Clausula Vera 1 CT D4 P5 D P5 4/2 15.0 3.0 3 0.414286 36.0 3.0
119.0 Altizans Only 1 AT D4 P5 G P8 4/2 15.0 4.5 4 0.425000 3.0 9.0
128.0 Abandoned Clausula Vera 1 zC E4 M6 G P8 4/2 17.0 1.0 3 0.457143 9.0 12.0
140.0 Clausula Vera 1 CT A3 M2 C P4 4/2 18.0 3.0 5 0.500000 12.0 36.0
176.0 Authentic 1 CuTB D3 -P4 D P5 4/2 23.0 1.0 6 0.628571 36.0 52.0
228.0 Evaded Authentic 1 CTb B-3 m3 D P5 4/2 29.0 3.0 4 0.814286 52.0 12.0
240.0 Clausula Vera 1 CT D3 -P4 D P5 4/2 31.0 1.0 5 0.857143 12.0 24.0
264.0 Clausula Vera 1 CT G3 P1 G P8 4/2 34.0 1.0 5 0.942857 24.0 24.0
CRIM_Mass_0051_1.mei
Ludwig Daser
Missa Ecce nunc benedicite: Kyrie
CadType LeadingTones CVFs Low RelLow Tone RelTone TSig Measure Beat Sounding Progress SinceLast ToNext
Last
80.0 Authentic 1 CTB G2 -P8 G P8 4/2 11.0 1.0 4.0 0.285714 80.0 48.0
128.0 Clausula Vera 1 CT D4 P5 D P5 4/2 17.0 1.0 2.0 0.457143 48.0 12.0
140.0 Abandoned Clausula Vera 1 zC G3 P1 G P8 4/2 18.0 3.0 3.0 0.500000 12.0 12.0
152.0 Clausula Vera 1 CT A3 M2 C P4 4/2 20.0 1.0 4.0 0.542857 12.0 80.0
232.0 Evaded Authentic 1 CTb B-3 m3 D P5 4/2 30.0 1.0 4.0 0.828571 80.0 12.0
244.0 Clausula Vera 0 tACT D3 -P4 D P5 4/2 31.0 3.0 4.0 0.871429 12.0 20.0
264.0 Clausula Vera 1 CT G3 P1 G P8 4/2 34.0 1.0 3.0 0.942857 20.0 32.0
CRIM_Mass_0052_2.mei
Roland de Lassus
Missa Ecce nunc benedicite: Gloria
CadType LeadingTones CVFs Low RelLow Tone RelTone TSig Measure Beat Sounding Progress SinceLast ToNext
Last
68.0 Authentic 1 CTB D3 -P4 D P5 4/2 9.0 3.0 6 0.090426 68.0 80.0
148.0 Evaded Authentic 1 Cu G3 P1 G P8 4/2 19.0 3.0 5 0.196809 80.0 20.0
168.0 Clausula Vera 1 CT D3 -P4 D P5 4/2 22.0 1.0 4 0.223404 20.0 24.0
192.0 Clausula Vera 1 CT D4 P5 D P5 4/2 25.0 1.0 4 0.255319 24.0 48.0
240.0 Authentic 1 CTuB G3 P1 G P8 4/2 31.0 1.0 6 0.319149 48.0 24.0
264.0 Phrygian Clausula Vera 1 CT A3 M2 A M2 4/2 34.0 1.0 4 0.351064 24.0 32.0
296.0 Authentic 1 CuTB D3 -P4 D P5 4/2 38.0 1.0 6 0.393617 32.0 40.0
336.0 Evaded Authentic 1 TCb E4 M6 G P8 4/2 43.0 1.0 4 0.446809 40.0 12.0
348.0 Authentic 1 CB C3 -P5 C P4 4/2 44.0 3.0 6 0.462766 12.0 88.0
436.0 Authentic 1 CB D4 P5 D P5 4/2 55.0 3.0 4 0.579787 88.0 12.0
448.0 Clausula Vera 1 CT D4 P5 D P5 4/2 57.0 1.0 4 0.595745 12.0 12.0
460.0 Clausula Vera 1 CT D4 P5 D P5 4/2 58.0 3.0 4 0.611702 12.0 4.0
464.0 Authentic 1 QCB G3 P1 G P8 4/2 59.0 1.0 4 0.617021 4.0 16.0
480.0 Evaded Authentic 1 Cb G3 P1 D P5 4/2 61.0 1.0 4 0.638298 16.0 36.0
516.0 Clausula Vera 1 CT D3 -P4 D P5 4/2 65.0 3.0 6 0.686170 36.0 86.0
602.0 Phrygian Clausula Vera 1 CT C3 -P5 E M6 4/2 76.0 2.0 5 0.800532 86.0 10.0
612.0 Clausula Vera 1 CQT G3 P1 G P8 4/2 77.0 3.0 4 0.813830 10.0 56.0
668.0 Authentic 1 CtTB C3 -P5 C P4 4/2 84.0 3.0 6 0.888298 56.0 56.0
724.0 Authentic 1 CB D3 -P4 D P5 4/2 91.0 3.0 5 0.962766 56.0 12.0
736.0 Clausula Vera 1 CT B2 -m6 G P8 4/2 93.0 1.0 6 0.978723 12.0 24.0
CRIM_Mass_0051_2.mei
Ludwig Daser
Missa Ecce nunc benedicite: Gloria
CadType LeadingTones CVFs Low RelLow Tone RelTone TSig Measure Beat Sounding Progress SinceLast ToNext
Last
40.0 Evaded Authentic -1.0 cB G3 P1 NaN NaN 4/2 6.0 1.0 4.0 0.058824 40.0 24.0
64.0 Authentic 1.0 CuTB D3 -P4 D P5 4/2 9.0 1.0 4.0 0.094118 24.0 56.0
120.0 Authentic 1.0 CTB G2 -P8 G P8 4/2 16.0 1.0 4.0 0.176471 56.0 28.0
148.0 Clausula Vera 1.0 CT D4 P5 D P5 4/2 19.0 3.0 2.0 0.217647 28.0 24.0
172.0 Clausula Vera 0.0 CT D3 -P4 D P5 4/2 22.0 3.0 4.0 0.252941 24.0 44.0
216.0 Abandoned Authentic 1.0 Cxzx G4 P8 G P8 4/2 28.0 1.0 1.0 0.317647 44.0 28.0
244.0 Phrygian Clausula Vera 1.0 CT A3 M2 A M2 4/2 31.0 3.0 4.0 0.358824 28.0 36.0
280.0 Authentic 1.0 CuTB D3 -P4 D P5 4/2 36.0 1.0 4.0 0.411765 36.0 40.0
320.0 Authentic 1.0 tCB C3 -P5 C P4 4/2 41.0 1.0 4.0 0.470588 40.0 56.0
376.0 Clausula Vera 0.0 CT G3 P1 D P5 4/2 48.0 1.0 4.0 0.552941 56.0 12.0
388.0 Clausula Vera 0.0 TC B-3 m3 D P5 4/2 49.0 3.0 4.0 0.570588 12.0 13.0
401.0 Phrygian Clausula Vera 1.0 TC F3 -M2 B M3 4/2 51.0 1.5 4.0 0.589706 13.0 7.0
408.0 Phrygian 1.0 CTP D3 -P4 A M2 4/2 52.0 1.0 4.0 0.600000 7.0 28.0
436.0 Authentic 1.0 CTB C3 -P5 C P4 4/2 55.0 3.0 4.0 0.641176 28.0 12.0
448.0 Clausula Vera 1.0 CT G3 P1 G P8 4/2 57.0 1.0 4.0 0.658824 12.0 52.0
500.0 Evaded Authentic 1.0 CTb B-3 m3 D P5 4/2 63.0 3.0 3.0 0.735294 52.0 16.0
516.0 Clausula Vera 0.0 CT D3 -P4 D P5 4/2 65.0 3.0 4.0 0.758824 16.0 20.0
536.0 Clausula Vera 1.0 CTx G3 P1 G P8 4/2 68.0 1.0 2.0 0.788235 20.0 16.0
552.0 Clausula Vera 1.0 CT C3 -P5 C P4 4/2 70.0 1.0 2.0 0.811765 16.0 20.0
572.0 Evaded Authentic 1.0 TCb C4 P4 G P8 4/2 72.0 3.0 4.0 0.841176 20.0 20.0
592.0 NaN NaN ApTx F4 m7 C P4 4/2 75.0 1.0 3.0 0.870588 20.0 20.0
612.0 Clausula Vera 0.0 CT G3 P1 G P8 4/2 77.0 3.0 4.0 0.900000 20.0 52.0
664.0 Clausula Vera 1.0 CT G3 P1 G P8 4/2 84.0 1.0 2.0 0.976471 52.0 32.0
CRIM_Mass_0052_3.mei
Orlando di Lasso
Missa Ecce nunc: Credo
CadType LeadingTones CVFs Low RelLow Tone RelTone TSig Measure Beat Sounding Progress SinceLast ToNext
Last
56.0 Authentic 1 CTB D3 -P4 D P5 4/2 8.0 1.0 6.0 0.047619 56.0 40.0
96.0 Authentic 1 CTB G2 -P8 G P8 4/2 13.0 1.0 6.0 0.081633 40.0 32.0
128.0 Evaded Clausula Vera 1 Ct D3 -P4 D P5 4/2 17.0 1.0 6.0 0.108844 32.0 8.0
136.0 Clausula Vera 1 CT G3 P1 G P8 4/2 18.0 1.0 6.0 0.115646 8.0 8.0
144.0 Authentic 1 tCB D3 -P4 D P5 4/2 19.0 1.0 4.0 0.122449 8.0 12.0
156.0 Abandoned Authentic 0 Cx B3 M3 D P5 4/2 20.0 3.0 3.0 0.132653 12.0 44.0
200.0 Authentic 1 CuTB D3 -P4 D P5 4/2 26.0 1.0 5.0 0.170068 44.0 12.0
212.0 Clausula Vera 1 tCT C3 -P5 C P4 4/2 27.0 3.0 5.0 0.180272 12.0 12.0
224.0 Clausula Vera 1 TC E4 M6 G P8 4/2 29.0 1.0 4.0 0.190476 12.0 16.0
240.0 Abandoned Authentic 1 Czxx G4 P8 G P8 4/2 31.0 1.0 3.0 0.204082 16.0 8.0
248.0 Clausula Vera 1 CT D3 -P4 D P5 4/2 32.0 1.0 6.0 0.210884 8.0 16.0
264.0 Clausula Vera 1 CT G3 P1 G P8 4/2 34.0 1.0 5.0 0.224490 16.0 40.0
304.0 Authentic 1 CTB D3 -P4 D P5 4/2 39.0 1.0 6.0 0.258503 40.0 24.0
328.0 Authentic 1 CTB C3 -P5 C P4 4/2 42.0 1.0 6.0 0.278912 24.0 56.0
384.0 Authentic 1 CB G3 P1 G P8 4/2 49.0 1.0 6.0 0.326531 56.0 24.0
408.0 Authentic 1 CTB D3 -P4 D P5 4/2 52.0 1.0 6.0 0.346939 24.0 48.0
456.0 Authentic 1 CB G3 P1 G P8 4/2 58.0 1.0 4.0 0.387755 48.0 92.0
548.0 Authentic 1 CB C3 -P5 C P4 4/2 69.0 3.0 4.0 0.465986 92.0 20.0
568.0 Phrygian Clausula Vera 1 CT E3 -m3 E M6 4/2 72.0 1.0 4.0 0.482993 20.0 4.0
572.0 Authentic 1 CB A3 M2 A M2 4/2 72.0 3.0 4.0 0.486395 4.0 16.0
588.0 Clausula Vera 1 CT G3 P1 G P8 4/2 74.0 3.0 4.0 0.500000 16.0 24.0
612.0 Clausula Vera 1 CT B-4 m10 D P5 4/2 77.0 3.0 3.0 0.520408 24.0 24.0
636.0 Authentic 1 CB G3 P1 G P8 4/2 80.0 3.0 4.0 0.540816 24.0 8.0
644.0 Authentic 1 BC G3 P1 G P8 4/2 81.0 3.0 4.0 0.547619 8.0 8.0
652.0 Evaded Authentic 1 Cb E4 M6 G P8 4/2 82.0 3.0 4.0 0.554422 8.0 8.0
660.0 Authentic 1 CB G4 P8 G P8 4/2 83.0 3.0 4.0 0.561224 8.0 68.0
728.0 Authentic 1 CtTB D3 -P4 D P5 4/2 92.0 1.0 6.0 0.619048 68.0 36.0
764.0 Clausula Vera 1 TC C4 P4 C P4 4/2 96.0 3.0 4.0 0.649660 36.0 32.0
796.0 Authentic 1 CTB C3 -P5 C P4 4/2 100.0 3.0 5.0 0.676871 32.0 60.0
856.0 Authentic 1 CB A3 M2 A M2 4/2 108.0 1.0 4.0 0.727891 60.0 84.0
940.0 Clausula Vera 1 TC G4 P8 G P8 4/2 118.0 3.0 2.0 0.799320 84.0 44.0
984.0 Authentic 1 CTB C3 -P5 C P4 4/2 124.0 1.0 6.0 0.836735 44.0 48.0
1032.0 Phrygian Clausula Vera 1 CT E3 -m3 E M6 4/2 130.0 1.0 4.0 0.877551 48.0 4.0
1036.0 Authentic 1 CTB A2 -m7 A M2 4/2 130.0 3.0 5.0 0.880952 4.0 24.0
1060.0 Altizans Only 1 AT G3 P1 D P5 4/2 133.0 3.0 4.0 0.901361 24.0 16.0
1076.0 Clausula Vera 1 TC C4 P4 C P4 4/2 135.0 3.0 5.0 0.914966 16.0 108.0
CRIM_Mass_0051_3.mei
Ludwig Daser
Missa Ecce nunc benedicite: Credo
CadType LeadingTones CVFs Low RelLow Tone RelTone TSig Measure Beat Sounding Progress SinceLast ToNext
Last
40.0 Evaded Authentic -1.0 cB G3 P1 NaN NaN 4/2 6.0 1.0 4.0 0.030303 40.0 24.0
64.0 Authentic 1.0 CuTB D3 -P4 D P5 4/2 9.0 1.0 4.0 0.048485 24.0 40.0
104.0 Authentic 1.0 CTB G2 -P8 G P8 4/2 14.0 1.0 4.0 0.078788 40.0 36.0
140.0 Clausula Vera 1.0 CT D4 P5 D P5 4/2 18.0 3.0 2.0 0.106061 36.0 20.0
160.0 Authentic 0.0 CB D3 -P4 D P5 4/2 21.0 1.0 4.0 0.121212 20.0 32.0
192.0 Clausula Vera 1.0 CT C3 -P5 C P4 4/2 25.0 1.0 4.0 0.145455 32.0 40.0
232.0 NaN NaN CTP D3 -P4 A M2 4/2 30.0 1.0 4.0 0.175758 40.0 8.0
240.0 Authentic 1.0 tCB D3 -P4 D P5 4/2 31.0 1.0 4.0 0.181818 8.0 20.0
260.0 Evaded Authentic 1.0 TCb E4 M6 G P8 4/2 33.0 3.0 3.0 0.196970 20.0 12.0
272.0 Clausula Vera 1.0 TC G3 P1 G P8 4/2 35.0 1.0 2.0 0.206061 12.0 16.0
288.0 Altizans Only 1.0 AT C3 -P5 G P8 4/2 37.0 1.0 4.0 0.218182 16.0 12.0
300.0 Clausula Vera 1.0 CTx G3 P1 G P8 4/2 38.0 3.0 3.0 0.227273 12.0 60.0
360.0 Clausula Vera 1.0 QCT C3 -P5 C P4 4/2 46.0 1.0 4.0 0.272727 60.0 80.0
440.0 Clausula Vera 1.0 TC G4 P8 G P8 4/2 56.0 1.0 2.0 0.333333 80.0 24.0
464.0 Clausula Vera 1.0 TC D4 P5 D P5 4/2 59.0 1.0 2.0 0.351515 24.0 44.0
508.0 Clausula Vera 0.0 tCT D3 -P4 D P5 4/2 64.0 3.0 4.0 0.384848 44.0 12.0
520.0 Clausula Vera 1.0 CxTx D4 P5 D P5 4/2 66.0 1.0 2.0 0.393939 12.0 76.0
596.0 Clausula Vera 1.0 CT D3 -P4 D P5 4/2 75.0 3.0 2.0 0.451515 76.0 32.0
628.0 Clausula Vera 1.0 CT C3 -P5 C P4 4/2 79.0 3.0 2.0 0.475758 32.0 44.0
672.0 Clausula Vera 1.0 TC G3 P1 G P8 4/2 85.0 1.0 3.0 0.509091 44.0 52.0
724.0 Clausula Vera 1.0 CTx D4 P5 D P5 4/2 91.0 3.0 2.0 0.548485 52.0 12.0
736.0 Authentic 0.0 BC G3 P1 G P8 4/2 93.0 1.0 3.0 0.557576 12.0 8.0
744.0 Evaded Authentic 1.0 Cb E4 M6 G P8 4/2 94.0 1.0 3.0 0.563636 8.0 8.0
752.0 Clausula Vera 1.0 CT G4 P8 G P8 4/2 95.0 1.0 2.0 0.569697 8.0 36.0
788.0 Clausula Vera 1.0 CT C4 P4 C P4 4/2 99.0 3.0 2.0 0.596970 36.0 68.0
856.0 Clausula Vera 1.0 CTx D4 P5 D P5 4/2 108.0 1.0 2.0 0.648485 68.0 32.0
888.0 Authentic 1.0 TCB G3 P1 G P8 4/2 112.0 1.0 3.0 0.672727 32.0 40.0
928.0 Clausula Vera 1.0 CT C3 -P5 C P4 4/2 117.0 1.0 4.0 0.703030 40.0 32.0
960.0 Evaded Authentic 1.0 CuTx G3 P1 G P8 4/2 121.0 1.0 3.0 0.727273 32.0 56.0
1016.0 Clausula Vera 1.0 CxTx A3 M2 A M2 4/2 128.0 1.0 2.0 0.769697 56.0 32.0
1048.0 Evaded Altizans Only -1.0 aT F3 -M2 NaN NaN 4/2 132.0 1.0 4.0 0.793939 32.0 8.0
1056.0 Authentic 1.0 CB C3 -P5 C P4 4/2 133.0 1.0 4.0 0.800000 8.0 24.0
1080.0 Abandoned Authentic 0.0 Czx G4 P8 G P8 4/2 136.0 1.0 2.0 0.818182 24.0 28.0
1108.0 Clausula Vera 0.0 CT G3 P1 D P5 4/2 139.0 3.0 4.0 0.839394 28.0 28.0
1136.0 NaN NaN CP D3 -P4 A M2 4/2 143.0 1.0 4.0 0.860606 28.0 40.0
1176.0 Abandoned Authentic 0.0 Cx G4 P8 G P8 4/2 148.0 1.0 2.0 0.890909 40.0 16.0
1192.0 Clausula Vera 0.0 CT D3 -P4 D P5 4/2 150.0 1.0 4.0 0.903030 16.0 4.0
1196.0 Clausula Vera 1.0 CxTx G3 P1 G P8 4/2 150.0 3.0 2.0 0.906061 4.0 16.0
1212.0 Evaded Altizans Only -1.0 aT C3 -P5 NaN NaN 4/2 152.0 3.0 3.0 0.918182 16.0 8.0
1220.0 Evaded Altizans Only -1.0 aT C4 P4 NaN NaN 4/2 153.0 3.0 3.0 0.924242 8.0 8.0
1228.0 Evaded Clausula Vera -1.0 Tc A3 M2 NaN NaN 4/2 154.0 3.0 3.0 0.930303 8.0 8.0
1236.0 Evaded Altizans Only -1.0 aT G3 P1 NaN NaN 4/2 155.0 3.0 4.0 0.936364 8.0 68.0
1304.0 Clausula Vera 1.0 CxTx G3 P1 G P8 4/2 164.0 1.0 2.0 0.987879 68.0 32.0

A Network of Cadences

Here we render the pieces as a network based on their shared cadences. In this case the simple information about tone or type alone (and tone and type combined) will not be sufficient for us to understand much about the connections among compositions. A more subtle way of tracing the ‘cadential similarity’ is to find shared pairs of cadences.

In python we can do this with a few lines of code. Working with the Tone, CadType and CVFs (or ‘cadential voice functions’) columns of our results, we can:

  • combine these features into a single categorical ‘combined’ tag
  • combine successive pairs of cadences into a ‘paired_cadences’ tag with the shift() method in Pandas.

Here is what the code looks like:

# create a new column that combines data from several others, so we get a complex cadence type
corpus_cadences['combined'] = corpus_cadences['Tone'] + "_" + corpus_cadences['CadType'] + "_" + corpus_cadences['CVFs']
# and now combine a given cadence tag with the following one
corpus_cadences['paired_cadences'] = corpus_cadences['combined'] + "_" + corpus_cadences['combined'].shift(-1)

# fill in the last cell, so there is no NAN, and that we get the final cadence
corpus_cadences['paired_cadences'].iloc[-1] = corpus_cadences['combined'].iloc[-1]

After the groupby operations, we then can return a table that shows the number of times that any pair of pieces shares one of these pairs of cadences. Here is a sample:

count
title_pairs 
(Daser Missa Ecce nunc Benedicite: Sanctus, Daser Missa Ecce nunc Benedicite: Credo)    4
(Lasso Missa Ecce nunc benedicite: Kyrie, Lasso Missa Ecce nunc benedicite: Credo)  3
(Ecce nunc benedicite, Daser Missa Ecce nunc Benedicite: Credo) 3
(Daser Missa Ecce nunc Benedicite: Gloria, Ecce nunc benedicite)    2
(Lasso Missa Ecce nunc benedicite: Sanctus, Lasso Missa Ecce nunc benedicite: Credo)    2
Code

# create a new column that combines data from several others, so we get a complex cadence type
corpus_cadences['combined'] = corpus_cadences['Tone'] + "_" + corpus_cadences['CadType'] + "_" + corpus_cadences['CVFs']

corpus_cadences['paired_cadences'] = corpus_cadences['combined'] + "_" + corpus_cadences['combined'].shift(-1)

# fill in the last cell, so there is no NA and that we get the final cadence
corpus_cadences['paired_cadences'].iloc[-1] = corpus_cadences['combined'].iloc[-1]
# group the cadences by this combined tag and get the titles
grouped_titles = corpus_cadences.groupby('paired_cadences')['Title'].unique().reset_index(name='titles')

# Generate all pairs of titles for each group
all_pairs = []
for _, row in grouped_titles.iterrows():
    pairs = list(combinations(row['titles'], 2))
    all_pairs.append((row['paired_cadences'], pairs))

# Create a new DataFrame with the results
result_df = pd.DataFrame(all_pairs, columns=['paired_cadences', 'title_pairs'])

# remove the empty pairs
df_filtered = result_df[result_df['title_pairs'].apply(len) > 0]

# explode the complicated lists of tuples, effectively 'tyding' the data
exploded_df = df_filtered.explode('title_pairs')

# get the counts of each pair, which provides the basis of the weights
pair_counts = exploded_df['title_pairs'].value_counts()

# Adding Louvain Communities
def add_communities(G):
    G = deepcopy(G)
    partition = community_louvain.best_partition(G)
    nx.set_node_attributes(G, partition, "group")
    return G

# Create an empty NetworkX graph
G = nx.Graph()

# Add nodes and assign weights to edges
for pair, count in pair_counts.items():
    # Directly unpacking the tuple into node1 and node2
    node1, node2 = pair
    # Adding nodes if they don't exist already
    if node1 not in G.nodes:
        G.add_node(node1)
    if node2 not in G.nodes:
        G.add_node(node2)
    # Adding edge with weight
    G.add_edge(node1, node2, weight=count)

# Adjusting edge thickness based on weights
for edge in G.edges(data=True):
    edge[2]['width'] = edge[2]['weight']
    
G = add_communities(G)

# set display parameters
cad_network = Network(notebook=True,
                   width="800",
                          height="800",
                          bgcolor="black", 
                          font_color="white")

# Set the physics layout of the network
cad_network.set_options("""
{
"physics": {
"enabled": true,
"forceAtlas2Based": {
    "springLength": 1
},
"solver": "forceAtlas2Based"
}
}
""")

cad_network.from_nx(G)
# # return the network
cad_network.show("cad_network.html")

Interpretation of Cadence Network

The network in particular shows the centrality of the motet (which shares a number of pairs with all the other mass movements.

But note, too, how the Kyrie and Gloria movements of the masses fall into different communities of similarity. This means *they are more similar to each other than to other movements in the corpus**.