Skip to main content

napparent_tabular/
activation.rs

1//! Pluggable activations for KG pair edges and effect columns.
2//!
3//! **KG pair activation** maps accumulated `(sum, count)` stats in `vals_map` to scalar edge
4//! weights in `vals_map_avg`. The default [`KgPairActivation::LogFrequencyWeightedMean`] applies
5//! `(sum / count) * log10(count)` when count > 1, down-weighting sparse pair cells to reduce
6//! outlier bias in the knowledge-graph structure.
7//!
8//! **Effect activation** maps per-row combined column signal to `{col}_effect` features. The
9//! default [`EffectActivation::GlobalMeanContrast`] subtracts the global mean outcome.
10//!
11//! Future variants (Bayesian shrinkage, robust contrast, etc.) extend the enums without changing
12//! the HashMap-based KG layout.
13
14use crate::preprocess::BinDepth;
15
16/// Raw accumulated stats for one canonical unordered value-pair key in `vals_map`.
17#[derive(Clone, Copy, Debug, PartialEq)]
18pub struct PairStats {
19    pub sum: f32,
20    pub count: f32,
21}
22
23/// Context passed to effect activations per row.
24#[derive(Clone, Copy, Debug, PartialEq)]
25pub struct EffectContext {
26    pub global_mean_outcome: f32,
27}
28
29/// How a KG edge weight is computed from accumulated pair counts.
30#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
31pub enum KgPairActivation {
32    /// `(sum / count) * log10(count)` when count > 1, else 0.
33    #[default]
34    LogFrequencyWeightedMean,
35}
36
37impl KgPairActivation {
38    pub fn activate(&self, stats: PairStats) -> f32 {
39        match self {
40            KgPairActivation::LogFrequencyWeightedMean => log_frequency_weighted_mean(stats),
41        }
42    }
43}
44
45fn log_frequency_weighted_mean(stats: PairStats) -> f32 {
46    if stats.count <= 1.0 {
47        0.0
48    } else {
49        (stats.sum / stats.count) * stats.count.log10()
50    }
51}
52
53/// How combined column signal becomes an effect feature value.
54#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
55pub enum EffectActivation {
56    /// `combined - global_mean_outcome`
57    #[default]
58    GlobalMeanContrast,
59}
60
61impl EffectActivation {
62    pub fn activate(&self, combined: f32, ctx: &EffectContext) -> f32 {
63        match self {
64            EffectActivation::GlobalMeanContrast => combined - ctx.global_mean_outcome,
65        }
66    }
67}
68
69/// Activation settings for both pipeline stages.
70#[derive(Clone, Debug, Default, PartialEq, Eq)]
71pub struct ActivationConfig {
72    pub kg_pair: KgPairActivation,
73    pub effect: EffectActivation,
74}
75
76/// Optional fail-fast caps for large or wide transforms (all `None` = no limits).
77#[derive(Clone, Debug, Default, PartialEq, Eq)]
78pub struct TransformLimits {
79    pub max_rows: Option<usize>,
80    pub max_active_columns: Option<usize>,
81    pub max_col_pairs: Option<usize>,
82    pub max_vals_map_keys: Option<usize>,
83}
84
85/// Full configuration for [`crate::pipeline::transform_record_batches`].
86#[derive(Clone, Debug, PartialEq, Eq)]
87pub struct TransformConfig {
88    pub bin_depth: BinDepth,
89    pub activation: ActivationConfig,
90    /// When true, show progress on stderr (in-place bar on a TTY, line logs when piped).
91    pub verbose: bool,
92    pub limits: TransformLimits,
93}
94
95impl TransformConfig {
96    pub fn new(bin_depth: BinDepth) -> Self {
97        Self {
98            bin_depth,
99            activation: ActivationConfig::default(),
100            verbose: false,
101            limits: TransformLimits::default(),
102        }
103    }
104
105    pub fn with_activation(mut self, activation: ActivationConfig) -> Self {
106        self.activation = activation;
107        self
108    }
109
110    pub fn with_verbose(mut self, verbose: bool) -> Self {
111        self.verbose = verbose;
112        self
113    }
114
115    pub fn with_limits(mut self, limits: TransformLimits) -> Self {
116        self.limits = limits;
117        self
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn log_frequency_weighted_mean_cases() {
127        let act = KgPairActivation::LogFrequencyWeightedMean;
128        assert_eq!(
129            act.activate(PairStats {
130                sum: 10.0,
131                count: 1.0
132            }),
133            0.0
134        );
135        assert_eq!(
136            act.activate(PairStats {
137                sum: 5.0,
138                count: 0.5
139            }),
140            0.0
141        );
142        let v = act.activate(PairStats {
143            sum: 50.0,
144            count: 10.0,
145        });
146        assert!((v - 5.0_f32).abs() < 1e-5);
147    }
148
149    #[test]
150    fn global_mean_contrast() {
151        let act = EffectActivation::GlobalMeanContrast;
152        let ctx = EffectContext {
153            global_mean_outcome: 1.0,
154        };
155        assert!((act.activate(3.0, &ctx) - 2.0).abs() < 1e-6);
156    }
157}