22from .__common__ import *
33
44
5- __all__ = ["from_arff" , "to_arff" ]
5+ __all__ = ["from_arff" , "is_arff" , " to_arff" ]
66
77
8- def from_arff (dsff , path = None , target = TARGET_NAME , missing = MISSING_TOKEN ):
9- """ Populate the DSFF file from an ARFF file. """
10- d , features = [], {}
11- with open (path ) as f :
12- relation , attributes , data = False , [False , False ], False
13- for n , l in enumerate (f , 1 ):
14- l , pf = l .strip (), f"Line { n } : "
15- # the file shall start with "@RELATION"
16- if not relation :
17- if l .startswith ("@RELATION " ):
18- relation = True
19- try :
20- dsff ['title' ] = re .match (r"@RELATION\s+('[^']*'|\"[^\"]*\")$" , l ).group (1 ).strip ("'\" " )
21- continue
22- except Exception as e :
23- raise BadInputData (f"{ pf } failed on @RELATION ({ e } )" )
24- else :
25- raise BadInputData (f"{ pf } did not find @RELATION" )
26- # get metadata and feature descriptions from comments
27- if l .startswith ("%" ):
28- if re .match (r"^\%\s+metadata\s*\:\s*\{.*\}$" , l ):
29- dsff .write (metadata = literal_eval (l .split (":" , 1 )[1 ]))
30- elif (m := re .match (r"^\%\s+(.*?)\s*\:\s*(.*?)$" , l )):
31- name , descr = m .groups ()
32- features [name ] = descr
33- continue
34- # then ignore blank lines
35- if l == "" :
36- if attributes [0 ] and not attributes [1 ]:
37- # close the atributes block
38- attributes [1 ] = True
39- n_cols = len (d [0 ])
40- continue
41- if l .startswith ("@ATTRIBUTE " ):
42- if not attributes [0 ]:
43- attributes [0 ] = True
44- if len (d ) == 0 :
45- # start the attributes block
46- d .append ([])
47- if attributes [1 ]:
48- raise BadInputData (f"{ pf } found @ATTRIBUTE out of the attributes block)" )
8+ def _parse (text_or_fh , target = TARGET_NAME , missing = MISSING_TOKEN ):
9+ d , features , metadata , title = [], {}, {}, ""
10+ relation , attributes , data = False , [False , False ], False
11+ for n , l in enumerate (t .splitlines () if isinstance (t := text_or_fh , str ) else t ):
12+ l , pf = l .strip (), f"Line { n } : "
13+ # the file shall start with "@RELATION"
14+ if not relation :
15+ if l .startswith ("@RELATION " ):
16+ relation = True
4917 try :
50- header = re .match (r"@ATTRIBUTE\s+([^\s]+)\s+[A-Z]+$" , l ).group (1 )
51- if header == "class" :
52- header = target
53- d [0 ].append (header )
54- continue
55- except AttributeError :
56- raise BadInputData (f"{ pf } failed on @ATTRIBUTE (bad type)" )
57- if not data :
58- if l == "@DATA" :
59- data = True
18+ title = re .match (r"@RELATION\s+('[^']*'|\"[^\"]*\")$" , l ).group (1 ).strip ("'\" " )
6019 continue
61- else :
62- raise BadInputData (f"{ pf } did not find @DATA where expected" )
63- row = list (map (lambda x : x .strip ("'\" " ), re .split (r",\s+" , l )))
64- if len (row ) != n_cols :
65- raise BadInputData (f"{ pf } this row does not match the number of columns" )
66- d .append (row )
20+ except Exception as e :
21+ raise BadInputData (f"{ pf } failed on @RELATION ({ e } )" )
22+ else :
23+ raise BadInputData (f"{ pf } did not find @RELATION" )
24+ # get metadata and feature descriptions from comments
25+ if l .startswith ("%" ):
26+ if re .match (r"^\%\s+metadata\s*\:\s*\{.*\}$" , l ):
27+ metadata = literal_eval (l .split (":" , 1 )[1 ])
28+ elif (m := re .match (r"^\%\s+(.*?)\s*\:\s*(.*?)$" , l )):
29+ name , descr = m .groups ()
30+ features [name ] = descr
31+ continue
32+ # then ignore blank lines
33+ if l == "" :
34+ if attributes [0 ] and not attributes [1 ]:
35+ # close the atributes block
36+ attributes [1 ] = True
37+ n_cols = len (d [0 ])
38+ continue
39+ if l .startswith ("@ATTRIBUTE " ):
40+ if not attributes [0 ]:
41+ attributes [0 ] = True
42+ if len (d ) == 0 :
43+ # start the attributes block
44+ d .append ([])
45+ if attributes [1 ]:
46+ raise BadInputData (f"{ pf } found @ATTRIBUTE out of the attributes block)" )
47+ try :
48+ header = re .match (r"@ATTRIBUTE\s+([^\s]+)\s+[A-Z]+$" , l ).group (1 )
49+ if header == "class" :
50+ header = target
51+ d [0 ].append (header )
52+ continue
53+ except AttributeError :
54+ raise BadInputData (f"{ pf } failed on @ATTRIBUTE (bad type)" )
55+ if not data :
56+ if l == "@DATA" :
57+ data = True
58+ continue
59+ else :
60+ raise BadInputData (f"{ pf } did not find @DATA where expected" )
61+ row = list (map (lambda x : x .strip ("'\" " ), re .split (r",\s+" , l )))
62+ if len (row ) != n_cols :
63+ raise BadInputData (f"{ pf } this row does not match the number of columns" )
64+ d .append (row )
6765 for i in range (n_cols ):
6866 values = []
6967 for j , row in enumerate (d ):
@@ -76,9 +74,26 @@ def from_arff(dsff, path=None, target=TARGET_NAME, missing=MISSING_TOKEN):
7674 if "" .join (sorted (set (values ))) in ["0" , "1" , "01" ]:
7775 for j , row in enumerate (d ):
7876 if j > 0 :
79- row [i ] = {'0' : "False" , '1' : "True" }[row [i ]]
80- dsff .write (data = d )
81- dsff .write (features = features )
77+ row [i ] = {'0' : "False" , '1' : "True" }[row [i ]]
78+ return d , features , metadata , title
79+
80+
81+ def from_arff (dsff , path = None , target = TARGET_NAME , missing = MISSING_TOKEN ):
82+ """ Populate the DSFF file from an ARFF file. """
83+ with open (path ) as f :
84+ d , ft , md , t = _parse (f , target , missing )
85+ dsff ['title' ] = t
86+ dsff .write (data = d , features = ft , metadata = md )
87+
88+
89+ @text_or_path
90+ def is_arff (text ):
91+ """ Check if the input text or path is a valid ARFF. """
92+ try :
93+ _parse (ensure_str (text ))
94+ return True
95+ except (BadInputData , UnicodeDecodeError ):
96+ return False
8297
8398
8499def to_arff (dsff , path = None , target = TARGET_NAME , exclude = DEFAULT_EXCL , missing = MISSING_TOKEN , text = False ):
0 commit comments