Source code for music21.features.outputFormats
from __future__ import annotations
import pathlib
import typing as t
import unittest
from music21 import environment
from music21 import exceptions21
if t.TYPE_CHECKING:
from music21.features.base import DataSet
environLocal = environment.Environment('features.outputFormats')
class OutputFormatException(exceptions21.Music21Exception):
pass
[docs]
class OutputFormat:
'''
Provide output for a DataSet, which is passed in as an initial argument.
'''
def __init__(self, dataSet: DataSet|None = None) -> None:
# assume a two-dimensional array
self.ext: str = '' # store a file extension if necessary
# pass a data set object
self._dataSet = dataSet
[docs]
def getHeaderLines(self) -> list:
'''
Get the header as a list of lines.
'''
return [] # define in subclass
[docs]
def getString(self, includeClassLabel: bool = True, includeId: bool = True,
lineBreak: str = '\n') -> str:
return '' # define in subclass
[docs]
def write(self, fp: str|pathlib.Path|None = None,
includeClassLabel: bool = True, includeId: bool = True) -> str|pathlib.Path:
'''
Write the file. If no file path is given, a temporary file will be written.
'''
if fp is None:
fp = environLocal.getTempFile(suffix=self.ext)
if not str(fp).endswith(self.ext):
raise OutputFormatException('Could not get a temp file with the right extension')
with open(fp, 'w', encoding='utf-8') as f:
f.write(self.getString(includeClassLabel=includeClassLabel,
includeId=includeId))
return fp
[docs]
class OutputTabOrange(OutputFormat):
'''
Tab delimited file format used with Orange.
For more information, see:
https://orange3.readthedocs.io/projects/orange-data-mining-library/en/latest/tutorial/data.html#saving-the-data
'''
def __init__(self, dataSet: DataSet|None = None) -> None:
super().__init__(dataSet=dataSet)
self.ext = '.tab'
[docs]
def getHeaderLines(self, includeClassLabel: bool = True,
includeId: bool = True) -> list[list[str]]:
# noinspection PyShadowingNames
'''
Get the header as a list of lines.
>>> f = [features.jSymbolic.ChangesOfMeterFeature]
>>> ds = features.DataSet()
>>> ds.addFeatureExtractors(f)
>>> of = features.outputFormats.OutputTabOrange(ds)
>>> for x in of.getHeaderLines(): print(x)
['Identifier', 'Changes_of_Meter']
['string', 'discrete']
['meta', '']
>>> ds = features.DataSet(classLabel='Composer')
>>> ds.addFeatureExtractors(f)
>>> of = features.outputFormats.OutputTabOrange(ds)
>>> for x in of.getHeaderLines(): print(x)
['Identifier', 'Changes_of_Meter', 'Composer']
['string', 'discrete', 'discrete']
['meta', '', 'class']
'''
if self._dataSet is None: # pragma: no cover
raise OutputFormatException('cannot get header lines without a DataSet')
post = []
post.append(self._dataSet.getAttributeLabels(
includeClassLabel=includeClassLabel, includeId=includeId))
# second row metadata
row = []
for x in self._dataSet.getDiscreteLabels(
includeClassLabel=includeClassLabel, includeId=includeId):
if x is None: # this is a string entry
row.append('string')
elif x is True: # if True, it is discrete
row.append('discrete')
else:
row.append('continuous')
post.append(row)
# third row metadata
row = []
for x in self._dataSet.getClassPositionLabels(includeId=includeId):
if x is None: # the id value
row.append('meta')
elif x is True: # if True, it is the class column
row.append('class')
else:
row.append('')
post.append(row)
return post
[docs]
def getString(self, includeClassLabel: bool = True, includeId: bool = True,
lineBreak: str = '\n') -> str:
'''
Get the complete DataSet as a string with the appropriate headers.
'''
if self._dataSet is None: # pragma: no cover
raise OutputFormatException('cannot get a string without a DataSet')
msg = []
header = self.getHeaderLines(includeClassLabel=includeClassLabel,
includeId=includeId)
data = header + self._dataSet.getFeaturesAsList(
includeClassLabel=includeClassLabel)
for row in data:
sub = []
for e in row:
sub.append(str(e))
msg.append('\t'.join(sub))
return lineBreak.join(msg)
[docs]
class OutputCSV(OutputFormat):
'''
Comma-separated value list.
'''
def __init__(self, dataSet: DataSet|None = None) -> None:
super().__init__(dataSet=dataSet)
self.ext = '.csv'
[docs]
def getHeaderLines(self, includeClassLabel: bool = True,
includeId: bool = True) -> list[list[str]]:
'''
Get the header as a list of lines.
>>> f = [features.jSymbolic.ChangesOfMeterFeature]
>>> ds = features.DataSet(classLabel='Composer')
>>> ds.addFeatureExtractors(f)
>>> of = features.outputFormats.OutputCSV(ds)
>>> of.getHeaderLines()[0]
['Identifier', 'Changes_of_Meter', 'Composer']
'''
if self._dataSet is None: # pragma: no cover
raise OutputFormatException('cannot get header lines without a DataSet')
post = []
post.append(self._dataSet.getAttributeLabels(
includeClassLabel=includeClassLabel, includeId=includeId))
return post
[docs]
def getString(self, includeClassLabel: bool = True, includeId: bool = True,
lineBreak: str = '\n') -> str:
if self._dataSet is None: # pragma: no cover
raise OutputFormatException('cannot get a string without a DataSet')
msg = []
header = self.getHeaderLines(includeClassLabel=includeClassLabel,
includeId=includeId)
data = header + self._dataSet.getFeaturesAsList(
includeClassLabel=includeClassLabel, includeId=includeId)
for row in data:
sub = []
for e in row:
sub.append(str(e))
msg.append(','.join(sub))
return lineBreak.join(msg)
[docs]
class OutputARFF(OutputFormat):
'''
An ARFF (Attribute-Relation File Format) file.
See
https://web.archive.org/web/20160212022757/http://weka.wikispaces.com/ARFF+%28stable+version%29
for more details
>>> oa = features.outputFormats.OutputARFF()
>>> oa.ext
'.arff'
'''
def __init__(self, dataSet: DataSet|None = None) -> None:
super().__init__(dataSet=dataSet)
self.ext = '.arff'
[docs]
def getHeaderLines(self, includeClassLabel: bool = True,
includeId: bool = True) -> list[str]:
'''
Get the header as a list of lines.
>>> f = [features.jSymbolic.ChangesOfMeterFeature]
>>> ds = features.DataSet(classLabel='Composer')
>>> ds.addFeatureExtractors(f)
>>> of = features.outputFormats.OutputARFF(ds)
>>> for x in of.getHeaderLines(): print(x)
@RELATION Composer
@ATTRIBUTE Identifier STRING
@ATTRIBUTE Changes_of_Meter NUMERIC
@ATTRIBUTE class {}
@DATA
'''
if self._dataSet is None: # pragma: no cover
raise OutputFormatException('cannot get header lines without a DataSet')
post = []
# get three parallel lists
attrs = self._dataSet.getAttributeLabels(
includeClassLabel=includeClassLabel, includeId=includeId)
discreteLabels = self._dataSet.getDiscreteLabels(
includeClassLabel=includeClassLabel, includeId=includeId)
classLabels = self._dataSet.getClassPositionLabels(includeId=includeId)
post.append(f'@RELATION {self._dataSet.getClassLabel()}')
for i, attrLabel in enumerate(attrs):
discrete = discreteLabels[i]
classLabel = classLabels[i]
if not classLabel: # a normal attribute
if discrete is None: # this is an identifier
post.append(f'@ATTRIBUTE {attrLabel} STRING')
elif discrete is True:
post.append(f'@ATTRIBUTE {attrLabel} NUMERIC')
else: # this needs to be a NOMINAL type
post.append(f'@ATTRIBUTE {attrLabel} NUMERIC')
else:
values = self._dataSet.getUniqueClassValues()
joined = ','.join(str(v) for v in values)
post.append('@ATTRIBUTE class {' + joined + '}')
# include start of data declaration
post.append('@DATA')
return post
[docs]
def getString(self, includeClassLabel: bool = True, includeId: bool = True,
lineBreak: str = '\n') -> str:
if self._dataSet is None: # pragma: no cover
raise OutputFormatException('cannot get a string without a DataSet')
msg = []
header = self.getHeaderLines(includeClassLabel=includeClassLabel,
includeId=includeId)
for row in header:
msg.append(row)
data = self._dataSet.getFeaturesAsList(
includeClassLabel=includeClassLabel)
# data is separated by commas
for row in data:
sub = []
for e in row:
sub.append(str(e))
msg.append(','.join(sub))
return lineBreak.join(msg)
class Test(unittest.TestCase):
def testARFFNumericClassValues(self):
'''
Regression test: numeric (non-string) class values must not crash
ARFF header generation. Previously the class declaration was built
with ','.join(values), which raised a TypeError when the class
values were ints or floats rather than strings.
AI-assisted (Claude).
'''
from music21 import converter
from music21 import features
ds = features.DataSet(classLabel='Meter')
ds.addFeatureExtractors(features.extractorsById(['r31']))
s1 = converter.parse('tinynotation: 4/4 c4 d e f')
s2 = converter.parse('tinynotation: 3/4 c4 d e')
# integer class values, not strings
ds.addMultipleData([s1, s2], classValues=[3, 4])
ds.process()
self.assertEqual(ds.getUniqueClassValues(), [3, 4])
of = features.outputFormats.OutputARFF(dataSet=ds)
classLines = [line for line in of.getHeaderLines()
if line.startswith('@ATTRIBUTE class')]
self.assertEqual(classLines, ['@ATTRIBUTE class {3,4}'])
# building the full string must not raise either
self.assertIn('@ATTRIBUTE class {3,4}', of.getString())
if __name__ == '__main__':
import music21
music21.mainTest()