Feature Selection Toolbox FST4
UTIA · Czech Academy of Sciences
Loading...
Searching...
No Matches
data_file_CSV.hpp
Go to the documentation of this file.
1#ifndef FSTDATAFILECSV_H
2#define FSTDATAFILECSV_H
3 /*
28=========================================================================
29Copyright and license:
30 * FST4 software (with the exception of any bundled or externally linked
31 third-party components) is Copyright (C) 2000-present Institute of
32 Information Theory and Automation of the Czech Academy of Sciences (UTIA).
33 * FST4 is free software: you can redistribute it and/or modify it under
34 the terms of the GNU Affero General Public License as published by the
35 Free Software Foundation, version 3. The full text of the license is in
36 the file LICENSE in the distribution root and at
37 https://www.gnu.org/licenses/agpl-3.0.html.
38 * FST4 is distributed in the hope that it will be useful, but WITHOUT ANY
39 WARRANTY; without even the implied warranty of MERCHANTABILITY or
40 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
41 License for more details.
42 * Closed-source use without the AGPL's copyleft obligations requires a
43 commercial license from UTIA; in case of interest write to
44 utia@utia.cas.cz. LICENSING.md in the distribution root explains both
45 options, the contributor license agreement that every contribution
46 requires, and the contacts (technical questions and pull requests:
47 somol@utia.cas.cz, with "FST" in the subject line).
48========================================================================== */
49
50#include <cstdio>
51#include <cstdlib>
52#include <cstring>
53#include <string>
54#include <vector>
55#include <iostream>
56#include "error.hpp"
57#include "global.hpp"
58
59namespace FST {
60
63 struct Data_File_CSV_Options {
64 std::string class_column;
65 std::string delimiter;
66 std::string header;
68 float missing_to;
70 Data_File_CSV_Options() : delimiter("auto"), header("auto"), missing_to_active(false), missing_to(0), missing_to_unpaired(false) {}
71 };
72
92 private:
93 std::string _fileName;
95
96 char delim;
97 bool hasHeader;
99 unsigned int columns;
100 std::vector<std::string> headerNames;
101
102 std::vector<std::vector<float> > rows;
103 std::vector<int> classIds;
104 std::vector<std::string> classNames;
105 std::vector<unsigned int> classSizes;
106 unsigned int skippedRows;
107 unsigned int mappedCells;
108 unsigned int mappedRows;
109 bool loaded;
110 public:
112 Data_File_CSV(std::string fileName, const Data_File_CSV_Options &_opts) : _fileName(fileName), opts(_opts),
113 delim(','), hasHeader(false), classColumn(-1), columns(0), skippedRows(0), mappedCells(0), mappedRows(0), loaded(false) {}
114
118 bool prepareFile(); // reads and parses the whole file
121 void sortRecordsByClass(); // regroups records into class-id order (stable)
122
123 unsigned int getNoOfClasses() {if (!loaded) throw fst_error("File not loaded"); return (unsigned int)classNames.size();}
124 unsigned int getNoOfFeatures() {if (!loaded) throw fst_error("File not loaded"); return columns-1;}
125 unsigned int getTotalSize() {if (!loaded) throw fst_error("File not loaded"); return (unsigned int)rows.size();}
126 unsigned int getClassSize(int clsId) {if (!loaded) throw fst_error("File not loaded"); if (clsId<0 || clsId>=(int)classSizes.size()) throw fst_error("getClassSize: class id out of range"); return classSizes[clsId];}
127 std::string getClassName(int clsId) {if (!loaded) throw fst_error("File not loaded"); if (clsId<0 || clsId>=(int)classNames.size()) return "-n/a-"; return classNames[clsId];}
131 float getFeatureValue(long record, int feature) {return rows[record][feature];}
132 private:
135 bool readLine( FILE *f, std::vector<char> &buff ) {
136 size_t len = 0; buff[0] = 0;
137 while (fgets(buff.data()+len, (int)(buff.size()-len), f) != nullptr) {
138 len += strlen(buff.data()+len);
139 if ((len > 0 && buff[len-1] == '\n') || feof(f)) return true;
140 if (len >= buff.size()-2) buff.resize(2*buff.size());
141 }
142 return len > 0;
143 }
144
146 static std::string trimTok(const std::string &s) {
147 size_t b = 0, e = s.size();
148 while (b < e && (s[b]==' ' || s[b]=='\t' || s[b]=='\r')) b++;
149 while (e > b && (s[e-1]==' ' || s[e-1]=='\t' || s[e-1]=='\r')) e--;
150 return s.substr(b, e-b);
151 }
152
155 std::vector<std::string> splitLine(const char *line) const {
156 std::vector<std::string> out; std::string cur; bool inq = false;
157 for (const char *p = line; *p; p++) {
158 const char c = *p;
159 if (inq) { if (c=='"') { if (p[1]=='"') {cur += '"'; p++;} else inq = false; } else cur += c; }
160 else if (c=='"') inq = true;
161 else if (c==delim) { out.push_back(trimTok(cur)); cur.clear(); }
162 else cur += c;
163 }
164 out.push_back(trimTok(cur));
165 return out;
166 }
167
169 bool isMissing(const std::string &tok) const {
170 std::string u; u.reserve(tok.size());
171 for (size_t i = 0; i < tok.size(); i++) u += (char)toupper((unsigned char)tok[i]);
172 return u=="" || u=="?" || u=="NA" || u=="N/A" || u=="NAN" || u=="NULL";
173 }
174
180 bool parseNumeric(const std::string &tok, float &out) const {
181 if (tok.empty()) return false;
182 std::string t = tok;
183 if (delim != ',' && t.find('.') == std::string::npos)
184 for (size_t i = 0; i < t.size(); i++) if (t[i]==',') t[i]='.';
185 const char *s = t.c_str(); char *end = nullptr;
186 const double v = strtod(s, &end);
187 if (end == s) return false;
188 while (*end==' ' || *end=='\t') end++;
189 if (*end) return false;
190 out = (float)v; return true;
191 }
192
197 void resolveDialect(const char *firstLine);
203 void resolveClassAndHeader(const std::vector<std::string> &toks);
204 };
205
206
207 inline void Data_File_CSV::resolveDialect(const char *firstLine) {
208 if (opts.delimiter=="comma") delim = ',';
209 else if (opts.delimiter=="semicolon") delim = ';';
210 else if (opts.delimiter=="tab") delim = '\t';
211 else { // auto: count candidates outside double quotes, comma-last tie-break (see the \note
212 // on the declaration for why a tie must not go to the comma)
213 int cnt[3] = {0,0,0}; bool inq = false; // [0]=tab [1]=semicolon [2]=comma
214 for (const char *p = firstLine; *p; p++) {
215 if (*p=='"') inq = !inq;
216 else if (!inq) { if (*p=='\t') cnt[0]++; else if (*p==';') cnt[1]++; else if (*p==',') cnt[2]++; }
217 }
218 int best = 0; if (cnt[1] > cnt[best]) best = 1; if (cnt[2] > cnt[best]) best = 2;
219 delim = (best==0) ? '\t' : (best==1) ? ';' : ',';
220 }
221 }
222
223 inline void Data_File_CSV::resolveClassAndHeader(const std::vector<std::string> &toks) {
224 columns = (unsigned int)toks.size();
225 if (columns < 2) throw fst_error("Data_File_CSV: fewer than 2 columns found on the first line (need at least one feature column and the class column; check --csv-delimiter).",1);
226
227 // class column spec: "" = last, integer = 0-based index, otherwise = header name
228 bool byName = false;
229 if (opts.class_column.empty()) classColumn = (int)columns-1;
230 else {
231 char *end = nullptr;
232 const long ix = strtol(opts.class_column.c_str(), &end, 10);
233 if (end != opts.class_column.c_str() && *end == 0) {
234 if (ix < 0 || ix >= (long)columns) throw fst_error("Data_File_CSV: --csv-class index "+opts.class_column+" out of range (file has "+std::to_string((long long)columns)+" columns, 0-based).",1);
235 classColumn = (int)ix;
236 } else byName = true; // resolved below, once the header row is confirmed
237 }
238
239 // header presence
240 if (byName && opts.header=="no") throw fst_error("Data_File_CSV: --csv-class names a header column but --csv-header is 'no'.",1);
241 if (byName || opts.header=="yes") hasHeader = true;
242 else if (opts.header=="no") hasHeader = false;
243 else { // auto: line 1 is a header iff any non-class field fails numeric-or-missing parse
244 hasHeader = false;
245 for (unsigned int ci = 0; ci < columns; ci++) {
246 if ((int)ci == classColumn) continue;
247 float v;
248 if (!isMissing(toks[ci]) && !parseNumeric(toks[ci], v)) { hasHeader = true; break; }
249 }
250 }
251
252 if (hasHeader) {
253 headerNames = toks;
254 if (byName) {
255 classColumn = -1;
256 for (unsigned int ci = 0; ci < columns; ci++) {
257 std::string a, b;
258 for (size_t i = 0; i < toks[ci].size(); i++) a += (char)toupper((unsigned char)toks[ci][i]);
259 for (size_t i = 0; i < opts.class_column.size(); i++) b += (char)toupper((unsigned char)opts.class_column[i]);
260 if (a == b) { classColumn = (int)ci; break; }
261 }
262 if (classColumn < 0) {
263 std::cout << "Header columns:"; for (unsigned int ci = 0; ci < columns; ci++) std::cout << " '" << headerNames[ci].c_str() << "'"; std::cout << std::endl;
264 throw fst_error("Data_File_CSV: --csv-class column '"+opts.class_column+"' not found in the header (available names listed above).",1);
265 }
266 }
267 }
268 }
269
271 FILE *f = fopen(_fileName.c_str(), "rt");
272 if (f == nullptr) throw fst_error("Data_File_CSV file read problem.",1);
273 std::vector<char> buff(8192); // grown by readLine() as needed — no line-length cap
274
275 bool first = true;
276 long lineNo = 0, dataRowNo = 0;
277 while (readLine(f, buff)) {
278 lineNo++;
279 char *line = buff.data();
280 if (first && (unsigned char)line[0]==0xEF && (unsigned char)line[1]==0xBB && (unsigned char)line[2]==0xBF) line += 3; // Excel UTF-8 BOM
281 size_t len = strlen(line);
282 while (len > 0 && (line[len-1]=='\n' || line[len-1]=='\r')) line[--len] = 0;
283 { // a line of pure whitespace/delimiter residue counts as empty (Excel emits trailing ",,,"
284 // lines for deleted rows); on line 1 the delimiter is not resolved yet, so every candidate
285 // delimiter counts as residue there
286 bool blank = true;
287 for (size_t i = 0; i < len; i++) {
288 const char ch = line[i];
289 if (ch==' ' || ch=='\t') continue;
290 if (!first && ch==delim) continue;
291 if (first && (ch==',' || ch==';')) continue;
292 blank = false; break;
293 }
294 if (blank) continue;
295 }
296
297 if (first) {
298 first = false;
299 resolveDialect(line);
300 std::vector<std::string> toks = splitLine(line);
302 if (hasHeader) continue; // line 1 consumed as the header row
303 // no header: line 1 is data — fall through and parse it below
304 }
305
306 std::vector<std::string> toks = splitLine(line);
307 if (toks.size() != columns) {
308 fclose(f);
309 throw fst_error("Data_File_CSV: line "+std::to_string((long long)lineNo)+" has "+std::to_string((long long)toks.size())+" columns, expected "+std::to_string((long long)columns)+" (check --csv-delimiter and quoting).",1);
310 }
311 dataRowNo++;
312
313 // missing-value scan first: by default the whole row is skipped, loudly; with
314 // missing_to_active the missing FEATURE tokens are replaced by the code in the parse
315 // loop below and the row is kept — only a missing CLASS label still skips (a row
316 // without a label cannot be assigned to any class)
317 {
318 int missCol = -1;
319 for (unsigned int ci = 0; ci < columns; ci++) if (isMissing(toks[ci])) { missCol = (int)ci; break; }
320 if (missCol >= 0 && !opts.missing_to_active) {
321 std::cout << "Skipping data row " << dataRowNo << " (line " << lineNo << "): missing value in column " << missCol+1 << std::endl;
322 skippedRows++;
323 continue;
324 }
325 if (missCol >= 0 && isMissing(toks[classColumn])) {
326 std::cout << "Skipping data row " << dataRowNo << " (line " << lineNo << "): missing class label in column " << classColumn+1 << " (--csv-missing-to maps feature values only)" << std::endl;
327 skippedRows++;
328 continue;
329 }
330 }
331
332 std::vector<float> vals; vals.reserve(columns-1);
333 unsigned int rowMapped = 0;
334 for (unsigned int ci = 0; ci < columns; ci++) {
335 if ((int)ci == classColumn) continue;
336 float v;
337 if (opts.missing_to_active && isMissing(toks[ci])) { v = opts.missing_to; rowMapped++; } // keep the row, code the cell
338 else if (!parseNumeric(toks[ci], v)) { // malformed stays fatal (not a missing marker — those were skipped above or mapped here)
339 fclose(f);
340 throw fst_error("Data_File_CSV: line "+std::to_string((long long)lineNo)+" column "+std::to_string((long long)(ci+1))+": feature value '"+toks[ci]+"' is not numeric.",1);
341 }
342 vals.push_back(v);
343 }
344 if (rowMapped) { mappedCells += rowMapped; mappedRows++; }
345
346 // class label: exact trimmed token, id by first appearance
347 const std::string &cls = toks[classColumn];
348 int id = -1;
349 for (unsigned int k = 0; k < classNames.size(); k++) if (classNames[k] == cls) { id = (int)k; break; }
350 if (id < 0) { id = (int)classNames.size(); classNames.push_back(cls); classSizes.push_back(0); }
351 classSizes[id]++;
352 rows.push_back(std::move(vals));
353 classIds.push_back(id);
354 }
355 fclose(f);
356
357 if (rows.empty()) throw fst_error("Data_File_CSV: no data rows loaded"+std::string(skippedRows ? " (all rows had missing values)" : "")+".",1);
358
359 loaded = true;
360 std::cout << "CSV file: " << _fileName.c_str()
361 << " (delimiter " << (delim=='\t' ? "TAB" : delim==';' ? "';'" : "','")
362 << ", header " << (hasHeader ? "yes" : "no")
363 << ", class column " << classColumn+1;
364 if (hasHeader) std::cout << " '" << headerNames[classColumn].c_str() << "'";
365 std::cout << " of " << columns << ")" << std::endl;
366 std::cout << "Classes count: " << classNames.size() << std::endl;
367 for (unsigned int k = 0; k < classNames.size(); k++)
368 std::cout << " class " << k << " = '" << classNames[k].c_str() << "' (" << classSizes[k] << " records)" << std::endl;
369 std::cout << "Records count: " << rows.size() << std::endl;
370 if (skippedRows)
371 std::cout << "Skipped " << skippedRows << " of " << dataRowNo << " data rows (missing values, reported above)" << std::endl;
372 if (mappedRows) {
373 std::cout << "Mapped " << mappedCells << " missing value(s) in " << mappedRows << " of " << dataRowNo << " data rows to code " << opts.missing_to << " (--csv-missing-to)";
374 // unpaired = the code stays in the data as a missingness indicator; legitimate (a tree
375 // wrapper can split on it) but easy to reach by forgetting, so name the pairing option
376 if (opts.missing_to_unpaired) std::cout << "; the code is left in the data - add --missing-value " << opts.missing_to << " to substitute per-feature training averages instead";
377 std::cout << std::endl;
378 }
379 std::cout << "Features count: " << columns-1 << " (feature indices follow the non-class columns in file order)" << std::endl;
380
381 return true;
382 }
383
385 if (!loaded) throw fst_error("File not loaded");
386 if (classNames.empty()) throw fst_error("Classes not identified");
387 std::vector<std::vector<float> > newRows; newRows.reserve(rows.size());
388 std::vector<int> newIds; newIds.reserve(classIds.size());
389 for (unsigned int c = 0; c < classNames.size(); c++)
390 for (unsigned int i = 0; i < rows.size(); i++)
391 if (classIds[i] == (int)c) { newRows.push_back(std::move(rows[i])); newIds.push_back((int)c); }
392 rows.swap(newRows);
393 classIds.swap(newIds);
394 }
395
396}
397
398#endif
unsigned int skippedRows
number of data rows skipped due to a missing value (see the class description's missing-value paragra...
Definition data_file_CSV.hpp:106
void resolveClassAndHeader(const std::vector< std::string > &toks)
Resolves columns, classColumn and hasHeader from the first line's split tokens toks (also headerNames...
Definition data_file_CSV.hpp:223
unsigned int getClassSize(int clsId)
number of records in class clsId; throws fst_error if not loaded or clsId is out of range (rather tha...
Definition data_file_CSV.hpp:126
bool prepareFile()
Reads and parses the whole CSV/TSV file: BOM-strip + blank-line skip, dialect/class-column/header res...
Definition data_file_CSV.hpp:270
std::vector< int > classIds
each record's class id, parallel to rows
Definition data_file_CSV.hpp:103
std::vector< std::string > classNames
class label text, indexed by class id (assigned in first-appearance order)
Definition data_file_CSV.hpp:104
unsigned int getNoOfClasses()
number of classes (classNames's size); throws fst_error if not loaded
Definition data_file_CSV.hpp:123
std::string _fileName
input .csv/.tsv file name, as passed to the constructor
Definition data_file_CSV.hpp:93
std::vector< unsigned int > classSizes
per-class record counts, indexed by class id, parallel to classNames
Definition data_file_CSV.hpp:105
std::vector< std::string > splitLine(const char *line) const
Definition data_file_CSV.hpp:155
Data_File_CSV_Options opts
dialect/class-column/missing-value options, as passed to the constructor
Definition data_file_CSV.hpp:94
bool parseNumeric(const std::string &tok, float &out) const
Definition data_file_CSV.hpp:180
void resolveDialect(const char *firstLine)
Resolves delim from opts.delimiter, or auto-detects it from firstLine (candidates counted outside dou...
Definition data_file_CSV.hpp:207
unsigned int mappedCells
number of missing feature values replaced by Data_File_CSV_Options::missing_to
Definition data_file_CSV.hpp:107
unsigned int mappedRows
data rows carrying at least one replaced value
Definition data_file_CSV.hpp:108
Data_File_CSV(std::string fileName, const Data_File_CSV_Options &_opts)
Constructs an (unloaded) reader for fileName under options _opts; call prepareFile() to actually read...
Definition data_file_CSV.hpp:112
int classColumn
resolved 0-based file column of the class label, set by resolveClassAndHeader()
Definition data_file_CSV.hpp:98
static std::string trimTok(const std::string &s)
Strips leading/trailing spaces, tabs and CRs from s (a single already-split field token).
Definition data_file_CSV.hpp:146
std::string getClassName(int clsId)
Definition data_file_CSV.hpp:127
bool loaded
whether prepareFile() completed without error; getters that need loaded data throw fst_error while fa...
Definition data_file_CSV.hpp:109
std::vector< std::vector< float > > rows
loaded records; each row holds feature values only, in non-class file-column order
Definition data_file_CSV.hpp:102
bool readLine(FILE *f, std::vector< char > &buff)
Definition data_file_CSV.hpp:135
std::vector< std::string > headerNames
the header row's tokens, empty when hasHeader is false
Definition data_file_CSV.hpp:100
void sortRecordsByClass()
Regroups rows/classIds into class-id order (stable within each class), mirroring Data_File_ARFF::sort...
Definition data_file_CSV.hpp:384
char delim
resolved delimiter character (comma/semicolon/tab), set by resolveDialect()
Definition data_file_CSV.hpp:96
unsigned int getNoOfFeatures()
data dimensionality (columns minus the class column); throws fst_error if not loaded
Definition data_file_CSV.hpp:124
float getFeatureValue(long record, int feature)
the (already-parsed) value of feature feature in record record
Definition data_file_CSV.hpp:131
unsigned int getTotalSize()
number of loaded records (rows's size); throws fst_error if not loaded
Definition data_file_CSV.hpp:125
bool hasHeader
resolved header presence, set by resolveClassAndHeader()
Definition data_file_CSV.hpp:97
bool isMissing(const std::string &tok) const
Whether tok (case-insensitively) is one of the recognized missing-value markers: empty,...
Definition data_file_CSV.hpp:169
unsigned int columns
total file columns (features + class), set by resolveClassAndHeader()
Definition data_file_CSV.hpp:99
FST4 specific error passing class.
Definition error.hpp:61
FST4 specific error passing class.
Global definitions.
The Feature Selection Toolbox 4 library: every library component – data access, criteria and classifi...
Definition data_accessor.hpp:80
CSV loader options.
Definition data_file_CSV.hpp:63
std::string delimiter
field separator: "auto" (detect from the first line), "comma", "semicolon" or "tab"
Definition data_file_CSV.hpp:65
float missing_to
the numeric code that replaces a missing feature value when missing_to_active. The mapping is purely ...
Definition data_file_CSV.hpp:68
bool missing_to_unpaired
set by the caller when no missing-value substitution is configured; the load summary then names the p...
Definition data_file_CSV.hpp:69
std::string header
whether the first line is a header row: "auto" (detect), "yes" or "no"
Definition data_file_CSV.hpp:66
std::string class_column
which column carries the class label: "" = the last column; a decimal integer = that 0-based file col...
Definition data_file_CSV.hpp:64
bool missing_to_active
when true, missing FEATURE tokens are replaced by missing_to and the row is kept instead of skipped; ...
Definition data_file_CSV.hpp:67
Copyright © 2000–present ÚTIA, Institute of Information Theory and Automation of the Czech Academy of Sciences · Contacts · This site sets no cookies and uses no analytics.