Skip to main content

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");