pub mod bin_mapper;
pub(crate) mod builder;
pub use bin_mapper::BinMapper;
use serde::{Deserialize, Serialize};
pub const MISSING_BIN: u16 = 0;
pub trait Bin: Copy + PartialEq + PartialOrd + Send + Sync + 'static {
const MISSING: Self;
fn as_usize(self) -> usize;
fn from_u16(v: u16) -> Self;
}
impl Bin for u8 {
const MISSING: Self = 0;
#[inline(always)]
fn as_usize(self) -> usize {
self as usize
}
#[inline(always)]
fn from_u16(v: u16) -> Self {
v as u8
}
}
impl Bin for u16 {
const MISSING: Self = 0;
#[inline(always)]
fn as_usize(self) -> usize {
self as usize
}
#[inline(always)]
fn from_u16(v: u16) -> Self {
v
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BinWidth {
U8,
U16,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum BinData {
U8(Vec<Vec<u8>>),
U16(Vec<Vec<u16>>),
}
impl BinData {
pub fn width(&self) -> BinWidth {
match self {
BinData::U8(_) => BinWidth::U8,
BinData::U16(_) => BinWidth::U16,
}
}
pub fn n_features(&self) -> usize {
match self {
BinData::U8(v) => v.len(),
BinData::U16(v) => v.len(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Dataset {
pub(crate) n_rows: usize,
pub(crate) n_features: usize,
pub(crate) bin_data: BinData,
pub(crate) bin_mappers: Vec<BinMapper>,
pub(crate) labels: Vec<f32>,
}
impl Dataset {
pub fn n_rows(&self) -> usize {
self.n_rows
}
pub fn n_features(&self) -> usize {
self.n_features
}
pub fn labels(&self) -> &[f32] {
&self.labels
}
pub fn bin_width(&self) -> BinWidth {
self.bin_data.width()
}
pub fn feature_column_u8(&self, feat: usize) -> &[u8] {
match &self.bin_data {
BinData::U8(v) => &v[feat],
BinData::U16(_) => panic!("feature_column_u8 called on a U16 dataset"),
}
}
pub fn feature_column_u16(&self, feat: usize) -> &[u16] {
match &self.bin_data {
BinData::U16(v) => &v[feat],
BinData::U8(_) => panic!("feature_column_u16 called on a U8 dataset"),
}
}
#[inline]
pub fn feature_bin(&self, feat: usize, row: usize) -> u16 {
match &self.bin_data {
BinData::U8(v) => v[feat][row] as u16,
BinData::U16(v) => v[feat][row],
}
}
pub fn bin_mapper(&self, feat: usize) -> &BinMapper {
&self.bin_mappers[feat]
}
pub fn bin_mappers(&self) -> &[BinMapper] {
&self.bin_mappers
}
pub(crate) fn augment_unknown(&mut self, fraction: f64, maskable: &[bool], rng: &mut impl rand::Rng) -> Vec<usize> {
let cat_feats: Vec<usize> = (0..self.n_features).filter(|&f| maskable.get(f).copied().unwrap_or(false)).collect();
if fraction <= 0.0 || cat_feats.is_empty() {
return Vec::new();
}
let base_rows = self.n_rows;
let mut plan: Vec<(usize, usize)> = Vec::new();
for row in 0..base_rows {
if rng.r#gen::<f64>() < fraction {
let m = cat_feats[rng.gen_range(0..cat_feats.len())];
plan.push((row, m));
}
}
if plan.is_empty() {
return Vec::new();
}
match &mut self.bin_data {
BinData::U8(cols) => append_masked(cols, &plan, 0u8),
BinData::U16(cols) => append_masked(cols, &plan, 0u16),
}
for &(base, _) in &plan {
self.labels.push(self.labels[base]);
}
self.n_rows += plan.len();
plan.into_iter().map(|(base, _)| base).collect()
}
}
fn append_masked<B: Bin>(cols: &mut [Vec<B>], plan: &[(usize, usize)], missing: B) {
for (feat, col) in cols.iter_mut().enumerate() {
col.reserve(plan.len());
for &(base, mask) in plan {
let v = if feat == mask { missing } else { col[base] };
col.push(v);
}
}
}
macro_rules! with_columns {
($ds:expr, $feats:expr, |$cols:ident| $body:block) => {
match $ds.bin_width() {
$crate::dataset::BinWidth::U8 => {
let $cols: Vec<&[u8]> =
$feats.iter().map(|&f| $ds.feature_column_u8(f)).collect();
$body
}
$crate::dataset::BinWidth::U16 => {
let $cols: Vec<&[u16]> =
$feats.iter().map(|&f| $ds.feature_column_u16(f)).collect();
$body
}
}
};
}
macro_rules! with_column {
($ds:expr, $feat:expr, |$col:ident| $body:block) => {
match $ds.bin_width() {
$crate::dataset::BinWidth::U8 => {
let $col: &[u8] = $ds.feature_column_u8($feat);
$body
}
$crate::dataset::BinWidth::U16 => {
let $col: &[u16] = $ds.feature_column_u16($feat);
$body
}
}
};
}
pub(crate) use with_column;
pub(crate) use with_columns;
#[cfg(test)]
mod augment_tests {
use super::MISSING_BIN;
use super::builder::build_dataset;
use rand::SeedableRng;
use rand_chacha::ChaCha8Rng;
#[test]
fn augment_unknown_masks_one_categorical_per_synthetic_row() {
let cols = vec![vec![10.0, 20.0, 30.0, 10.0], vec![1.0, 2.0, 3.0, 4.0]];
let labels = vec![1.0f32, 0.0, 1.0, 0.0];
let mut ds = build_dataset(cols, 16, 16, &[true, false], 1, &labels).unwrap();
let base_rows = ds.n_rows();
let mut rng = ChaCha8Rng::seed_from_u64(7);
let base_idx = ds.augment_unknown(1.0, &[true, false], &mut rng);
assert_eq!(base_idx.len(), base_rows);
assert_eq!(ds.n_rows(), base_rows * 2);
for (i, &base) in base_idx.iter().enumerate() {
let row = base_rows + i;
assert_eq!(ds.labels()[row], ds.labels()[base]);
assert_eq!(ds.feature_bin(0, row), MISSING_BIN);
assert_eq!(ds.feature_bin(1, row), ds.feature_bin(1, base));
}
}
#[test]
fn augment_unknown_noop_without_categoricals() {
let cols = vec![vec![1.0, 2.0, 3.0]];
let labels = vec![1.0f32, 0.0, 1.0];
let mut ds = build_dataset(cols, 16, 16, &[false], 1, &labels).unwrap();
let mut rng = ChaCha8Rng::seed_from_u64(1);
assert!(ds.augment_unknown(1.0, &[false], &mut rng).is_empty());
assert_eq!(ds.n_rows(), 3);
}
}