Skip to main content

datasynth_core/distributions/
behavioral_drift.rs

1//! Behavioral drift models for realistic entity behavior evolution.
2//!
3//! Provides comprehensive behavioral drift modeling including:
4//! - Vendor behavioral drift (payment terms, quality, pricing)
5//! - Customer behavioral drift (payment patterns, order patterns)
6//! - Employee behavioral drift (approval patterns, error patterns)
7//! - Collective behavioral drift (year-end intensity, automation adoption)
8
9use chrono::NaiveDate;
10use serde::{Deserialize, Serialize};
11
12/// Context for behavioral drift calculations.
13#[derive(Debug, Clone, Default)]
14pub struct DriftContext {
15    /// Economic cycle factor (1.0 = neutral, <1.0 = downturn, >1.0 = growth).
16    pub economic_cycle_factor: f64,
17    /// Whether currently in a recession.
18    pub is_recession: bool,
19    /// Current inflation rate.
20    pub inflation_rate: f64,
21    /// Market sentiment.
22    pub market_sentiment: MarketSentiment,
23    /// Period number (0-indexed).
24    pub period: u32,
25    /// Total periods in simulation.
26    pub total_periods: u32,
27}
28
29impl DriftContext {
30    /// Create a neutral context.
31    pub fn neutral() -> Self {
32        Self {
33            economic_cycle_factor: 1.0,
34            is_recession: false,
35            inflation_rate: 0.02,
36            market_sentiment: MarketSentiment::Neutral,
37            period: 0,
38            total_periods: 12,
39        }
40    }
41
42    /// Get years elapsed (fractional).
43    pub fn years_elapsed(&self) -> f64 {
44        self.period as f64 / 12.0
45    }
46}
47
48/// Market sentiment indicator.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
50#[serde(rename_all = "snake_case")]
51pub enum MarketSentiment {
52    /// Very pessimistic market.
53    VeryPessimistic,
54    /// Pessimistic market.
55    Pessimistic,
56    /// Neutral market.
57    #[default]
58    Neutral,
59    /// Optimistic market.
60    Optimistic,
61    /// Very optimistic market.
62    VeryOptimistic,
63}
64
65impl MarketSentiment {
66    /// Get the sentiment factor (0.6 to 1.4).
67    pub fn factor(&self) -> f64 {
68        match self {
69            Self::VeryPessimistic => 0.6,
70            Self::Pessimistic => 0.8,
71            Self::Neutral => 1.0,
72            Self::Optimistic => 1.2,
73            Self::VeryOptimistic => 1.4,
74        }
75    }
76}
77
78/// Trait for behavioral drift modeling.
79pub trait BehavioralDrift {
80    /// Get the behavioral state at a given date.
81    fn behavioral_state_at(&self, date: NaiveDate, context: &DriftContext) -> BehavioralState;
82
83    /// Evolve the behavior to the current date.
84    fn evolve(&mut self, current_date: NaiveDate, context: &DriftContext);
85}
86
87/// Behavioral state snapshot.
88#[derive(Debug, Clone, Default)]
89pub struct BehavioralState {
90    /// Payment behavior adjustment (days delta).
91    pub payment_days_delta: f64,
92    /// Order pattern adjustment factor.
93    pub order_factor: f64,
94    /// Error rate adjustment factor.
95    pub error_factor: f64,
96    /// Processing time adjustment factor.
97    pub processing_time_factor: f64,
98    /// Quality adjustment factor.
99    pub quality_factor: f64,
100    /// Price sensitivity factor.
101    pub price_sensitivity: f64,
102}
103
104impl BehavioralState {
105    /// Create a neutral state.
106    pub fn neutral() -> Self {
107        Self {
108            payment_days_delta: 0.0,
109            order_factor: 1.0,
110            error_factor: 1.0,
111            processing_time_factor: 1.0,
112            quality_factor: 1.0,
113            price_sensitivity: 1.0,
114        }
115    }
116}
117
118// =============================================================================
119// Vendor Behavioral Drift
120// =============================================================================
121
122/// Vendor behavioral drift configuration.
123#[derive(Debug, Clone, Default, Serialize, Deserialize)]
124pub struct VendorBehavioralDrift {
125    /// Payment terms drift configuration.
126    #[serde(default)]
127    pub payment_terms_drift: PaymentTermsDrift,
128    /// Vendor quality drift configuration.
129    #[serde(default)]
130    pub quality_drift: VendorQualityDrift,
131    /// Pricing behavior drift configuration.
132    #[serde(default)]
133    pub pricing_drift: PricingBehaviorDrift,
134}
135
136impl VendorBehavioralDrift {
137    /// Calculate combined behavioral state.
138    pub fn state_at(&self, context: &DriftContext) -> BehavioralState {
139        let years = context.years_elapsed();
140
141        // Payment terms extension
142        let payment_days = self.payment_terms_drift.extension_rate_per_year
143            * years
144            * (1.0
145                + self.payment_terms_drift.economic_sensitivity
146                    * (context.economic_cycle_factor - 1.0));
147
148        // Quality drift
149        let quality_factor = if years < 1.0 {
150            // New vendor improvement
151            1.0 + self.quality_drift.new_vendor_improvement_rate * years
152        } else {
153            // Complacency after first year
154            1.0 + self.quality_drift.new_vendor_improvement_rate
155                - self.quality_drift.complacency_decline_rate * (years - 1.0)
156        };
157
158        // Price sensitivity to inflation
159        let price_sensitivity =
160            1.0 + self.pricing_drift.inflation_pass_through * context.inflation_rate * years;
161
162        BehavioralState {
163            payment_days_delta: payment_days,
164            quality_factor: quality_factor.clamp(0.7, 1.3),
165            price_sensitivity,
166            ..BehavioralState::neutral()
167        }
168    }
169}
170
171/// Payment terms drift configuration.
172#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct PaymentTermsDrift {
174    /// Average days extension per year.
175    #[serde(default = "default_extension_rate")]
176    pub extension_rate_per_year: f64,
177    /// Economic sensitivity (how much economic conditions affect terms).
178    #[serde(default = "default_economic_sensitivity")]
179    pub economic_sensitivity: f64,
180}
181
182fn default_extension_rate() -> f64 {
183    2.5
184}
185
186fn default_economic_sensitivity() -> f64 {
187    1.0
188}
189
190impl Default for PaymentTermsDrift {
191    fn default() -> Self {
192        Self {
193            extension_rate_per_year: 2.5,
194            economic_sensitivity: 1.0,
195        }
196    }
197}
198
199/// Vendor quality drift configuration.
200#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct VendorQualityDrift {
202    /// Quality improvement rate for new vendors (per year).
203    #[serde(default = "default_improvement_rate")]
204    pub new_vendor_improvement_rate: f64,
205    /// Quality decline rate due to complacency (per year after first year).
206    #[serde(default = "default_decline_rate")]
207    pub complacency_decline_rate: f64,
208}
209
210fn default_improvement_rate() -> f64 {
211    0.02
212}
213
214fn default_decline_rate() -> f64 {
215    0.01
216}
217
218impl Default for VendorQualityDrift {
219    fn default() -> Self {
220        Self {
221            new_vendor_improvement_rate: 0.02,
222            complacency_decline_rate: 0.01,
223        }
224    }
225}
226
227/// Pricing behavior drift configuration.
228#[derive(Debug, Clone, Serialize, Deserialize)]
229pub struct PricingBehaviorDrift {
230    /// How much inflation is passed through (0.0 to 1.0).
231    #[serde(default = "default_pass_through")]
232    pub inflation_pass_through: f64,
233    /// Price volatility factor.
234    #[serde(default = "default_volatility")]
235    pub price_volatility: f64,
236}
237
238fn default_pass_through() -> f64 {
239    0.80
240}
241
242fn default_volatility() -> f64 {
243    0.10
244}
245
246impl Default for PricingBehaviorDrift {
247    fn default() -> Self {
248        Self {
249            inflation_pass_through: 0.80,
250            price_volatility: 0.10,
251        }
252    }
253}
254
255// =============================================================================
256// Customer Behavioral Drift
257// =============================================================================
258
259/// Customer behavioral drift configuration.
260#[derive(Debug, Clone, Default, Serialize, Deserialize)]
261pub struct CustomerBehavioralDrift {
262    /// Customer payment drift configuration.
263    #[serde(default)]
264    pub payment_drift: CustomerPaymentDrift,
265    /// Order pattern drift configuration.
266    #[serde(default)]
267    pub order_drift: OrderPatternDrift,
268}
269
270impl CustomerBehavioralDrift {
271    /// Calculate combined behavioral state.
272    pub fn state_at(&self, context: &DriftContext) -> BehavioralState {
273        // Payment delays during downturns
274        let payment_days = if context.is_recession || context.economic_cycle_factor < 0.9 {
275            let severity = 1.0 - context.economic_cycle_factor;
276            self.payment_drift.downturn_days_extension.0 as f64
277                + (self.payment_drift.downturn_days_extension.1 as f64
278                    - self.payment_drift.downturn_days_extension.0 as f64)
279                    * severity
280        } else {
281            0.0
282        };
283
284        // Order pattern shift (digital adoption)
285        let years = context.years_elapsed();
286        let order_factor = 1.0 + self.order_drift.digital_shift_rate * years;
287
288        BehavioralState {
289            payment_days_delta: payment_days,
290            order_factor,
291            ..BehavioralState::neutral()
292        }
293    }
294}
295
296/// Customer payment drift configuration.
297#[derive(Debug, Clone, Serialize, Deserialize)]
298pub struct CustomerPaymentDrift {
299    /// Days extension range during economic downturn (min, max).
300    #[serde(default = "default_downturn_extension")]
301    pub downturn_days_extension: (u32, u32),
302    /// Bad debt rate increase during downturn.
303    #[serde(default = "default_bad_debt_increase")]
304    pub downturn_bad_debt_increase: f64,
305}
306
307fn default_downturn_extension() -> (u32, u32) {
308    (5, 15)
309}
310
311fn default_bad_debt_increase() -> f64 {
312    0.02
313}
314
315impl Default for CustomerPaymentDrift {
316    fn default() -> Self {
317        Self {
318            downturn_days_extension: (5, 15),
319            downturn_bad_debt_increase: 0.02,
320        }
321    }
322}
323
324/// Order pattern drift configuration.
325#[derive(Debug, Clone, Serialize, Deserialize)]
326pub struct OrderPatternDrift {
327    /// Rate of shift to digital channels (per year).
328    #[serde(default = "default_digital_shift")]
329    pub digital_shift_rate: f64,
330    /// Order consolidation rate (fewer, larger orders).
331    #[serde(default = "default_consolidation")]
332    pub order_consolidation_rate: f64,
333}
334
335fn default_digital_shift() -> f64 {
336    0.05
337}
338
339fn default_consolidation() -> f64 {
340    0.02
341}
342
343impl Default for OrderPatternDrift {
344    fn default() -> Self {
345        Self {
346            digital_shift_rate: 0.05,
347            order_consolidation_rate: 0.02,
348        }
349    }
350}
351
352// =============================================================================
353// Employee Behavioral Drift
354// =============================================================================
355
356/// Employee behavioral drift configuration.
357#[derive(Debug, Clone, Default, Serialize, Deserialize)]
358pub struct EmployeeBehavioralDrift {
359    /// Approval pattern drift configuration.
360    #[serde(default)]
361    pub approval_drift: ApprovalPatternDrift,
362    /// Error pattern drift configuration.
363    #[serde(default)]
364    pub error_drift: ErrorPatternDrift,
365}
366
367impl EmployeeBehavioralDrift {
368    /// Calculate combined behavioral state.
369    pub fn state_at(&self, context: &DriftContext, is_period_end: bool) -> BehavioralState {
370        let years = context.years_elapsed();
371
372        // EOM intensity increase
373        let eom_factor = if is_period_end {
374            1.0 + self.approval_drift.eom_intensity_increase_per_year * years
375        } else {
376            1.0
377        };
378
379        // Learning curve effect on errors
380        // Error factor > 1.0 means more errors; 1.0 means baseline
381        let months = context.period as f64;
382        let error_factor = if months < self.error_drift.learning_curve_months as f64 {
383            // New employee learning curve: starts at (1 + new_employee_error_rate) and decreases to 1.0
384            let progress = months / self.error_drift.learning_curve_months as f64;
385            1.0 + self.error_drift.new_employee_error_rate * (1.0 - progress)
386        } else {
387            // Fatigue factor after learning: gradually increases from 1.0
388            let fatigue_years = (months - self.error_drift.learning_curve_months as f64) / 12.0;
389            1.0 + self.error_drift.fatigue_error_increase * fatigue_years
390        };
391
392        BehavioralState {
393            processing_time_factor: eom_factor,
394            error_factor: error_factor.clamp(0.5, 2.0),
395            ..BehavioralState::neutral()
396        }
397    }
398}
399
400/// Approval pattern drift configuration.
401#[derive(Debug, Clone, Serialize, Deserialize)]
402pub struct ApprovalPatternDrift {
403    /// EOM intensity increase per year.
404    #[serde(default = "default_eom_intensity")]
405    pub eom_intensity_increase_per_year: f64,
406    /// Volume threshold for rubber-stamp behavior.
407    #[serde(default = "default_rubber_stamp")]
408    pub rubber_stamp_volume_threshold: u32,
409}
410
411fn default_eom_intensity() -> f64 {
412    0.05
413}
414
415fn default_rubber_stamp() -> u32 {
416    50
417}
418
419impl Default for ApprovalPatternDrift {
420    fn default() -> Self {
421        Self {
422            eom_intensity_increase_per_year: 0.05,
423            rubber_stamp_volume_threshold: 50,
424        }
425    }
426}
427
428/// Error pattern drift configuration.
429#[derive(Debug, Clone, Serialize, Deserialize)]
430pub struct ErrorPatternDrift {
431    /// Initial error rate for new employees.
432    #[serde(default = "default_new_error_rate")]
433    pub new_employee_error_rate: f64,
434    /// Learning curve duration in months.
435    #[serde(default = "default_learning_months")]
436    pub learning_curve_months: u32,
437    /// Error rate increase due to fatigue (per year after learning).
438    #[serde(default = "default_fatigue_increase")]
439    pub fatigue_error_increase: f64,
440}
441
442fn default_new_error_rate() -> f64 {
443    0.08
444}
445
446fn default_learning_months() -> u32 {
447    6
448}
449
450fn default_fatigue_increase() -> f64 {
451    0.01
452}
453
454impl Default for ErrorPatternDrift {
455    fn default() -> Self {
456        Self {
457            new_employee_error_rate: 0.08,
458            learning_curve_months: 6,
459            fatigue_error_increase: 0.01,
460        }
461    }
462}
463
464// =============================================================================
465// Collective Behavioral Drift
466// =============================================================================
467
468/// Collective behavioral drift across the organization.
469#[derive(Debug, Clone, Default, Serialize, Deserialize)]
470pub struct CollectiveBehavioralDrift {
471    /// Year-end intensity drift.
472    #[serde(default)]
473    pub year_end_intensity: YearEndIntensityDrift,
474    /// Automation adoption drift.
475    #[serde(default)]
476    pub automation_adoption: AutomationAdoptionDrift,
477    /// Remote work impact drift.
478    #[serde(default)]
479    pub remote_work_impact: RemoteWorkDrift,
480}
481
482impl CollectiveBehavioralDrift {
483    /// Calculate collective state.
484    pub fn state_at(&self, context: &DriftContext, month: u32) -> CollectiveState {
485        let years = context.years_elapsed();
486
487        // Year-end intensity
488        let is_year_end = month == 11 || month == 0; // December or January
489        let year_end_factor = if is_year_end {
490            1.0 + self.year_end_intensity.intensity_increase_per_year * years
491        } else {
492            1.0
493        };
494
495        // Automation adoption (S-curve)
496        let automation_rate = if self.automation_adoption.s_curve_enabled {
497            let midpoint_years = self.automation_adoption.adoption_midpoint_months as f64 / 12.0;
498            let steepness = self.automation_adoption.steepness;
499            1.0 / (1.0 + (-steepness * (years - midpoint_years)).exp())
500        } else {
501            0.0
502        };
503
504        // Remote work posting time flattening
505        let posting_time_variance = if self.remote_work_impact.enabled {
506            1.0 - self.remote_work_impact.posting_time_flattening * years.min(2.0)
507        } else {
508            1.0
509        };
510
511        CollectiveState {
512            year_end_intensity_factor: year_end_factor,
513            automation_rate,
514            posting_time_variance_factor: posting_time_variance.max(0.5),
515        }
516    }
517}
518
519/// Collective behavioral state.
520#[derive(Debug, Clone, Default)]
521pub struct CollectiveState {
522    /// Year-end intensity factor.
523    pub year_end_intensity_factor: f64,
524    /// Automation adoption rate (0.0 to 1.0).
525    pub automation_rate: f64,
526    /// Posting time variance factor.
527    pub posting_time_variance_factor: f64,
528}
529
530/// Year-end intensity drift configuration.
531#[derive(Debug, Clone, Serialize, Deserialize)]
532pub struct YearEndIntensityDrift {
533    /// Intensity increase per year.
534    #[serde(default = "default_intensity_increase")]
535    pub intensity_increase_per_year: f64,
536}
537
538fn default_intensity_increase() -> f64 {
539    0.05
540}
541
542impl Default for YearEndIntensityDrift {
543    fn default() -> Self {
544        Self {
545            intensity_increase_per_year: 0.05,
546        }
547    }
548}
549
550/// Automation adoption drift configuration.
551#[derive(Debug, Clone, Serialize, Deserialize)]
552pub struct AutomationAdoptionDrift {
553    /// Enable S-curve adoption model.
554    #[serde(default)]
555    pub s_curve_enabled: bool,
556    /// Adoption midpoint in months.
557    #[serde(default = "default_midpoint")]
558    pub adoption_midpoint_months: u32,
559    /// Steepness of adoption curve.
560    #[serde(default = "default_steepness")]
561    pub steepness: f64,
562}
563
564fn default_midpoint() -> u32 {
565    24
566}
567
568fn default_steepness() -> f64 {
569    0.15
570}
571
572impl Default for AutomationAdoptionDrift {
573    fn default() -> Self {
574        Self {
575            s_curve_enabled: false,
576            adoption_midpoint_months: 24,
577            steepness: 0.15,
578        }
579    }
580}
581
582/// Remote work impact drift configuration.
583#[derive(Debug, Clone, Serialize, Deserialize)]
584pub struct RemoteWorkDrift {
585    /// Enable remote work impact.
586    #[serde(default)]
587    pub enabled: bool,
588    /// Posting time flattening factor (reduction in time-of-day variance).
589    #[serde(default = "default_flattening")]
590    pub posting_time_flattening: f64,
591}
592
593fn default_flattening() -> f64 {
594    0.3
595}
596
597impl Default for RemoteWorkDrift {
598    fn default() -> Self {
599        Self {
600            enabled: false,
601            posting_time_flattening: 0.3,
602        }
603    }
604}
605
606// =============================================================================
607// Behavioral Drift Controller
608// =============================================================================
609
610/// Main controller for all behavioral drift.
611#[derive(Debug, Clone, Serialize, Deserialize, Default)]
612pub struct BehavioralDriftConfig {
613    /// Enable behavioral drift.
614    #[serde(default)]
615    pub enabled: bool,
616    /// Vendor behavioral drift.
617    #[serde(default)]
618    pub vendor_behavior: VendorBehavioralDrift,
619    /// Customer behavioral drift.
620    #[serde(default)]
621    pub customer_behavior: CustomerBehavioralDrift,
622    /// Employee behavioral drift.
623    #[serde(default)]
624    pub employee_behavior: EmployeeBehavioralDrift,
625    /// Collective behavioral drift.
626    #[serde(default)]
627    pub collective: CollectiveBehavioralDrift,
628}
629
630impl BehavioralDriftConfig {
631    /// Compute all behavioral effects for a given context.
632    pub fn compute_effects(
633        &self,
634        context: &DriftContext,
635        month: u32,
636        is_period_end: bool,
637    ) -> BehavioralEffects {
638        if !self.enabled {
639            return BehavioralEffects::neutral();
640        }
641
642        BehavioralEffects {
643            vendor: self.vendor_behavior.state_at(context),
644            customer: self.customer_behavior.state_at(context),
645            employee: self.employee_behavior.state_at(context, is_period_end),
646            collective: self.collective.state_at(context, month),
647        }
648    }
649}
650
651/// Combined behavioral effects.
652#[derive(Debug, Clone, Default)]
653pub struct BehavioralEffects {
654    /// Vendor behavioral state.
655    pub vendor: BehavioralState,
656    /// Customer behavioral state.
657    pub customer: BehavioralState,
658    /// Employee behavioral state.
659    pub employee: BehavioralState,
660    /// Collective behavioral state.
661    pub collective: CollectiveState,
662}
663
664impl BehavioralEffects {
665    /// Create neutral effects.
666    pub fn neutral() -> Self {
667        Self {
668            vendor: BehavioralState::neutral(),
669            customer: BehavioralState::neutral(),
670            employee: BehavioralState::neutral(),
671            collective: CollectiveState::default(),
672        }
673    }
674}
675
676#[cfg(test)]
677mod tests {
678    use super::*;
679
680    #[test]
681    fn test_vendor_behavioral_drift() {
682        let drift = VendorBehavioralDrift::default();
683        let context = DriftContext {
684            period: 24, // 2 years
685            total_periods: 36,
686            ..DriftContext::neutral()
687        };
688
689        let state = drift.state_at(&context);
690        // Payment terms should have increased
691        assert!(state.payment_days_delta > 0.0);
692        // Quality factor should have decreased due to complacency
693        assert!(state.quality_factor < 1.02);
694    }
695
696    #[test]
697    fn test_customer_downturn_drift() {
698        let drift = CustomerBehavioralDrift::default();
699        let context = DriftContext {
700            is_recession: true,
701            economic_cycle_factor: 0.8,
702            ..DriftContext::neutral()
703        };
704
705        let state = drift.state_at(&context);
706        // Payment delays during downturn
707        assert!(state.payment_days_delta > 0.0);
708    }
709
710    #[test]
711    fn test_employee_learning_curve() {
712        let drift = EmployeeBehavioralDrift::default();
713
714        // New employee (month 1)
715        let context_new = DriftContext {
716            period: 1,
717            ..DriftContext::neutral()
718        };
719        let state_new = drift.state_at(&context_new, false);
720        assert!(state_new.error_factor > 1.0); // Higher errors initially
721
722        // Experienced employee (month 12)
723        let context_exp = DriftContext {
724            period: 12,
725            ..DriftContext::neutral()
726        };
727        let state_exp = drift.state_at(&context_exp, false);
728        assert!(state_exp.error_factor < state_new.error_factor); // Lower errors
729    }
730
731    #[test]
732    fn test_automation_s_curve() {
733        let drift = CollectiveBehavioralDrift {
734            automation_adoption: AutomationAdoptionDrift {
735                s_curve_enabled: true,
736                adoption_midpoint_months: 24,
737                steepness: 0.15,
738            },
739            ..Default::default()
740        };
741
742        // Early (month 6)
743        let context_early = DriftContext {
744            period: 6,
745            ..DriftContext::neutral()
746        };
747        let state_early = drift.state_at(&context_early, 6);
748
749        // Midpoint (month 24)
750        let context_mid = DriftContext {
751            period: 24,
752            ..DriftContext::neutral()
753        };
754        let state_mid = drift.state_at(&context_mid, 0);
755
756        // Late (month 48)
757        let context_late = DriftContext {
758            period: 48,
759            ..DriftContext::neutral()
760        };
761        let state_late = drift.state_at(&context_late, 0);
762
763        // S-curve: slow start, fast middle, slow end
764        assert!(state_early.automation_rate < 0.5);
765        assert!((state_mid.automation_rate - 0.5).abs() < 0.2);
766        assert!(state_late.automation_rate > 0.5);
767    }
768}