dataset_ml/dataset/banknote_authentication.rs
1//! Banknote Authentication dataset.
2//!
3//! The dataset holds features extracted from images of genuine and forged
4//! banknote-like specimens. Researchers digitized the images with an
5//! industrial camera normally used for print inspection. They then used a
6//! Wavelet Transform tool to derive four continuous statistics from each
7//! image. The task is to predict the class of a specimen from those four
8//! features.
9//!
10//! **Columns (5):**
11//!
12//! | Name | Type | Description |
13//! |------------|-----------|--------------------------------------------|
14//! | `variance` | `Numeric` | variance of the Wavelet-Transformed image |
15//! | `skewness` | `Numeric` | skewness of the Wavelet-Transformed image |
16//! | `curtosis` | `Numeric` | curtosis of the Wavelet-Transformed image |
17//! | `entropy` | `Numeric` | entropy of the image |
18//! | `class` | `Integer` | raw class code, `0` or `1` |
19//!
20//! `curtosis` keeps the source's spelling. UCI names the attribute that way.
21//!
22//! The source designates the four statistics as the inputs
23//! ([`BanknoteAuthentication::FEATURE_NAMES`](crate::BanknoteAuthentication::FEATURE_NAMES)) and `class` as the label
24//! ([`BanknoteAuthentication::TARGET`](crate::BanknoteAuthentication::TARGET)).
25//!
26//! **Samples:** 1372 total (762 of class `0`, 610 of class `1`)
27//! **Application:** Binary classification / banknote authentication
28//!
29//! **Missing values:** none.
30//!
31//! **Source:** UCI Machine Learning Repository
32//! <https://doi.org/10.24432/C55P57>
33
34use crate::DOWNLOAD_RETRIES;
35use crate::table::{Column, ColumnData, Table};
36use crate::traits::impl_ml_dataset;
37use dataset_core::{Dataset, DatasetError, acquire_dataset, download_to_with_retries, unzip};
38use ndarray::Array1;
39use std::fs::File;
40
41use csv::ReaderBuilder;
42
43/// The URL for the Banknote Authentication dataset.
44///
45/// This is the UCI static package. It is a ZIP archive that contains a single
46/// file, `data_banknote_authentication.txt`.
47///
48/// # Citation
49///
50/// V. Lohweg. "Banknote Authentication," UCI Machine Learning Repository,
51/// \[Online\]. Available: <https://doi.org/10.24432/C55P57>
52const BANKNOTE_AUTHENTICATION_DATA_URL: &str =
53 "https://archive.ics.uci.edu/static/public/267/banknote+authentication.zip";
54
55/// The filename used for the downloaded ZIP archive inside the temp directory.
56const BANKNOTE_AUTHENTICATION_ZIP_FILENAME: &str = "banknote_authentication.zip";
57
58/// The name of the only file inside the archive, holding all 1372 records.
59const BANKNOTE_AUTHENTICATION_SOURCE_FILENAME: &str = "data_banknote_authentication.txt";
60
61/// The name of the final cached Banknote Authentication dataset file.
62const BANKNOTE_AUTHENTICATION_FILENAME: &str = "banknote_authentication.csv";
63
64/// The SHA256 hash of the Banknote Authentication dataset file
65/// (`data_banknote_authentication.txt`).
66const BANKNOTE_AUTHENTICATION_SHA256: &str =
67 "d0539aaed2139ba7a587b3e34fb345ce503ff7d5d33dbf9912d8e195ce425cb9";
68
69/// The name of the dataset.
70const BANKNOTE_AUTHENTICATION_DATASET_NAME: &str = "banknote_authentication";
71
72/// Number of samples.
73const N_SAMPLES: usize = 1372;
74
75/// The number of numeric features per sample.
76const N_FEATURES: usize = 4;
77
78/// The number of columns per CSV record (4 features + 1 label).
79const N_COLUMNS: usize = N_FEATURES + 1;
80
81/// A struct that represents the Banknote Authentication dataset with lazy
82/// loading.
83///
84/// The dataset loads only when you call a data accessor method. After the first
85/// load, the dataset caches the data for later accesses.
86///
87/// # About Dataset
88///
89/// Researchers extracted the data from images of genuine and forged
90/// banknote-like specimens. They digitized the images with an industrial
91/// camera normally used for print inspection. This camera produced 400×400
92/// pixel grayscale images at a resolution of about 660 dpi. Researchers then
93/// used a Wavelet Transform tool to extract four continuous statistics from
94/// each image. These statistics are the variance, skewness, curtosis, and
95/// entropy of the transformed image. They cover 1372 specimens.
96///
97/// # Columns
98///
99/// | Name | Type | Description |
100/// |------------|-----------|--------------------------------------------|
101/// | `variance` | `Numeric` | variance of the Wavelet-Transformed image |
102/// | `skewness` | `Numeric` | skewness of the Wavelet-Transformed image |
103/// | `curtosis` | `Numeric` | curtosis of the Wavelet-Transformed image |
104/// | `entropy` | `Numeric` | entropy of the image |
105/// | `class` | `Integer` | raw class code, `0` or `1` |
106///
107/// `curtosis` keeps the source's spelling. UCI names the attribute that way.
108///
109/// The source designates the four statistics as the inputs
110/// ([`BanknoteAuthentication::FEATURE_NAMES`]) and `class` as the label
111/// ([`BanknoteAuthentication::TARGET`]).
112///
113/// UCI does not document which `class` code marks a genuine note and which one
114/// marks a forged note. The loader keeps the code verbatim.
115///
116/// Missing values: none.
117///
118/// See more information at
119/// <https://archive.ics.uci.edu/dataset/267/banknote+authentication>.
120///
121/// # Citation
122///
123/// V. Lohweg. "Banknote Authentication," UCI Machine Learning Repository,
124/// \[Online\]. Available: <https://doi.org/10.24432/C55P57>
125///
126/// # Thread Safety
127///
128/// This struct implements `Send` and `Sync` automatically, because all fields
129/// implement them. This makes the struct safe to share across threads. The
130/// internal [`Dataset`] makes lazy initialization thread-safe.
131///
132/// # Example
133/// ```no_run
134/// use dataset_ml::BanknoteAuthentication;
135///
136/// // the loader creates the directory if it does not exist
137/// let download_dir = "./banknote_authentication";
138///
139/// let mut dataset = BanknoteAuthentication::new(download_dir);
140/// let table = dataset.data().unwrap();
141///
142/// assert_eq!(table.n_samples(), 1372);
143/// assert_eq!(table.n_columns(), 5);
144///
145/// // Ask for the feature matrix when you want it.
146/// let features = table.numeric_matrix(&BanknoteAuthentication::FEATURE_NAMES).unwrap();
147/// assert_eq!(features.shape(), &[1372, 4]);
148///
149/// // Reach one column by name.
150/// let class = table.column(BanknoteAuthentication::TARGET).unwrap().as_integer().unwrap();
151/// assert_eq!(class.len(), 1372);
152///
153/// // `get_data_mut()` edits the table in place. This needs no clone and no
154/// // reload. The change stays cached.
155/// if let Some(table) = dataset.get_data_mut() {
156/// if let Some(column) = table.column_mut("variance") {
157/// if let dataset_ml::ColumnData::Numeric(values) = column.data_mut() {
158/// values[0] = 0.5;
159/// }
160/// }
161/// }
162/// assert!(dataset.get_data().is_some());
163///
164/// // `take_data()` moves the owned table out with no clone. This leaves the
165/// // instance reusable.
166/// let owned = dataset.take_data().unwrap();
167/// assert_eq!(owned.n_samples(), 1372);
168///
169/// // `into_data()` also returns the owned table with no clone, but it consumes
170/// // the instance.
171/// let owned = dataset.into_data().unwrap();
172/// assert_eq!(owned.n_samples(), 1372);
173/// ```
174#[derive(Debug)]
175pub struct BanknoteAuthentication {
176 dataset: Dataset<Table, DatasetError>,
177}
178
179impl BanknoteAuthentication {
180 /// The columns the source designates as the model inputs, in source order.
181 pub const FEATURE_NAMES: [&'static str; N_FEATURES] =
182 ["variance", "skewness", "curtosis", "entropy"];
183
184 /// The column the source designates as the label.
185 pub const TARGET: &'static str = "class";
186
187 /// Create a new BanknoteAuthentication instance without loading data.
188 ///
189 /// The dataset loads lazily, on your first call to a data accessor method.
190 /// This is a lightweight operation that only stores the storage directory.
191 ///
192 /// # Parameters
193 ///
194 /// - `storage_dir` - The directory that stores the dataset.
195 ///
196 /// # Returns
197 ///
198 /// - `Self` - a `BanknoteAuthentication` instance ready for lazy loading.
199 pub fn new(storage_dir: &str) -> Self {
200 BanknoteAuthentication {
201 dataset: Dataset::new(storage_dir, Self::load_data),
202 }
203 }
204
205 /// Get and parse the Banknote Authentication dataset.
206 fn load_data(dir: &str) -> Result<Table, DatasetError> {
207 let file_path = acquire_dataset(
208 dir,
209 BANKNOTE_AUTHENTICATION_FILENAME,
210 BANKNOTE_AUTHENTICATION_DATASET_NAME,
211 Some(BANKNOTE_AUTHENTICATION_SHA256),
212 |temp_path| {
213 download_to_with_retries(
214 BANKNOTE_AUTHENTICATION_DATA_URL,
215 temp_path,
216 Some(BANKNOTE_AUTHENTICATION_ZIP_FILENAME),
217 DOWNLOAD_RETRIES,
218 )?;
219 unzip(
220 &temp_path.join(BANKNOTE_AUTHENTICATION_ZIP_FILENAME),
221 temp_path,
222 )?;
223 Ok(temp_path.join(BANKNOTE_AUTHENTICATION_SOURCE_FILENAME))
224 },
225 )?;
226
227 // The source is plain comma-separated with no header: every line is a
228 // record of 4 numeric features followed by the class code.
229 let file = File::open(&file_path)?;
230 let mut rdr = ReaderBuilder::new().has_headers(false).from_reader(file);
231
232 let mut features: Vec<Vec<f64>> = (0..N_FEATURES)
233 .map(|_| Vec::with_capacity(N_SAMPLES))
234 .collect();
235 let mut labels: Vec<i64> = Vec::with_capacity(N_SAMPLES);
236
237 for (idx, result) in rdr.records().enumerate() {
238 let record = result.map_err(|e| {
239 DatasetError::csv_read_error(BANKNOTE_AUTHENTICATION_DATASET_NAME, e)
240 })?;
241 let line_num = idx + 1; // headerless file, lines are 1-indexed
242
243 // Skip blank lines defensively (e.g. a trailing newline).
244 if record.iter().all(|f| f.is_empty()) {
245 continue;
246 }
247
248 if record.len() != N_COLUMNS {
249 return Err(DatasetError::invalid_column_count(
250 BANKNOTE_AUTHENTICATION_DATASET_NAME,
251 N_COLUMNS,
252 record.len(),
253 line_num,
254 ));
255 }
256
257 // 4 numeric features.
258 for (col, name) in Self::FEATURE_NAMES.iter().enumerate() {
259 let value: f64 = record[col].trim().parse().map_err(|e| {
260 DatasetError::parse_failed(
261 BANKNOTE_AUTHENTICATION_DATASET_NAME,
262 name,
263 line_num,
264 e,
265 )
266 })?;
267 features[col].push(value);
268 }
269
270 // Label, kept as the raw `0`/`1` code the source records.
271 let raw_label = record[N_FEATURES].trim();
272 let label: u8 = raw_label.parse().map_err(|e| {
273 DatasetError::parse_failed(
274 BANKNOTE_AUTHENTICATION_DATASET_NAME,
275 "class",
276 line_num,
277 e,
278 )
279 })?;
280 if label > 1 {
281 return Err(DatasetError::invalid_value(
282 BANKNOTE_AUTHENTICATION_DATASET_NAME,
283 "class",
284 raw_label,
285 line_num,
286 ));
287 }
288 labels.push(i64::from(label));
289 }
290
291 let mut columns: Vec<Column> = Vec::with_capacity(N_COLUMNS);
292 for (name, values) in Self::FEATURE_NAMES.into_iter().zip(features) {
293 columns.push(Column::new(
294 name,
295 ColumnData::Numeric(Array1::from_vec(values)),
296 ));
297 }
298 columns.push(Column::new(
299 Self::TARGET,
300 ColumnData::Integer(Array1::from_vec(labels)),
301 ));
302
303 Table::new(BANKNOTE_AUTHENTICATION_DATASET_NAME, columns)
304 }
305
306 /// Get a reference to the parsed table.
307 ///
308 /// This method triggers lazy loading on the first call. Later calls return
309 /// the cached data.
310 ///
311 /// # Returns
312 ///
313 /// - `&Table` - reference to the cached table of 1372 samples and 5 columns.
314 ///
315 /// # Errors
316 ///
317 /// Returns `DatasetError` if:
318 /// - Download fails due to network issues
319 /// - File extraction or I/O operations fail
320 /// - Data format is invalid (wrong number of columns, unparseable values, or invalid labels)
321 pub fn data(&self) -> Result<&Table, DatasetError> {
322 self.dataset.load()
323 }
324
325 /// Get a reference to the parsed table **without** triggering loading.
326 ///
327 /// Unlike [`BanknoteAuthentication::data`], this method never runs the
328 /// loader. If the data has not loaded yet, it returns `None` instead of
329 /// downloading and parsing it.
330 ///
331 /// # Returns
332 ///
333 /// - `Some(&Table)` - reference to the cached table, if loaded.
334 /// - `None` - if the dataset has not loaded yet.
335 pub fn get_data(&self) -> Option<&Table> {
336 self.dataset.get()
337 }
338
339 /// Get a mutable reference to the parsed table for **in-place** editing.
340 ///
341 /// This needs no clone, and it does not remove the data from the cache. The
342 /// changes stay in the cache. Later calls to
343 /// [`BanknoteAuthentication::data`] or [`BanknoteAuthentication::get_data`]
344 /// see them.
345 ///
346 /// Like [`BanknoteAuthentication::get_data`], this does **not** trigger
347 /// loading.
348 ///
349 /// # Returns
350 ///
351 /// - `Some(&mut Table)` - mutable reference to the cached table, if loaded.
352 /// - `None` - if the dataset has not loaded yet.
353 pub fn get_data_mut(&mut self) -> Option<&mut Table> {
354 self.dataset.get_mut()
355 }
356
357 /// Consume the dataset and return the **owned** table.
358 ///
359 /// This **consumes** `self`. If you want owned data but need to keep using
360 /// the instance, use [`BanknoteAuthentication::take_data`] instead.
361 ///
362 /// # Returns
363 ///
364 /// - `Table` - the owned table of 1372 samples and 5 columns.
365 ///
366 /// # Errors
367 ///
368 /// Returns `DatasetError` if loading fails (network, file I/O, or parsing).
369 pub fn into_data(self) -> Result<Table, DatasetError> {
370 self.dataset.load()?;
371 Ok(self
372 .dataset
373 .into_inner()
374 .expect("data is present after a successful load"))
375 }
376
377 /// Take the **owned** table out of the dataset. This leaves the instance
378 /// reusable.
379 ///
380 /// This resets the instance to its unloaded state. The next accessor call
381 /// loads the dataset again.
382 ///
383 /// # Returns
384 ///
385 /// - `Table` - the owned table of 1372 samples and 5 columns.
386 ///
387 /// # Errors
388 ///
389 /// Returns `DatasetError` if loading fails (network, file I/O, or parsing).
390 pub fn take_data(&mut self) -> Result<Table, DatasetError> {
391 self.dataset.load()?;
392 Ok(self
393 .dataset
394 .take()
395 .expect("data is present after a successful load"))
396 }
397}
398
399impl_ml_dataset!(BanknoteAuthentication, "banknote_authentication");