1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4
5use crate::classification::DataType;
6use crate::pattern::Pattern;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct ColumnProfile {
11 pub name: String,
12 pub data_type: DataType,
13 pub null_count: usize,
14 pub total_count: usize,
15 pub unique_count: Option<usize>,
16 pub stats: ColumnStats,
17 pub patterns: Option<Vec<Pattern>>,
24}
25
26#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28pub struct Quartiles {
29 pub q1: f64,
30 pub q2: f64,
31 pub q3: f64,
32 pub iqr: f64,
33}
34
35#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
37pub struct FrequencyItem {
38 pub value: String,
39 pub count: usize,
40 #[serde(serialize_with = "crate::serde_helpers::round_2")]
41 pub percentage: f64,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct NumericStats {
47 #[serde(serialize_with = "crate::serde_helpers::round_2")]
48 pub min: f64,
49 #[serde(serialize_with = "crate::serde_helpers::round_2")]
50 pub max: f64,
51 #[serde(serialize_with = "crate::serde_helpers::round_4")]
52 pub mean: f64,
53 #[serde(serialize_with = "crate::serde_helpers::round_4")]
54 pub std_dev: f64,
55 #[serde(serialize_with = "crate::serde_helpers::round_4")]
56 pub variance: f64,
57 #[serde(
58 skip_serializing_if = "Option::is_none",
59 serialize_with = "crate::serde_helpers::round_2_opt"
60 )]
61 pub median: Option<f64>,
62 #[serde(
63 skip_serializing_if = "Option::is_none",
64 serialize_with = "crate::serde_helpers::quartiles::serialize"
65 )]
66 pub quartiles: Option<Quartiles>,
67 #[serde(
68 skip_serializing_if = "Option::is_none",
69 serialize_with = "crate::serde_helpers::round_2_opt"
70 )]
71 pub mode: Option<f64>,
72 #[serde(
73 skip_serializing_if = "Option::is_none",
74 serialize_with = "crate::serde_helpers::round_2_opt"
75 )]
76 pub coefficient_of_variation: Option<f64>,
77 #[serde(
78 skip_serializing_if = "Option::is_none",
79 serialize_with = "crate::serde_helpers::round_4_opt"
80 )]
81 pub skewness: Option<f64>,
82 #[serde(
83 skip_serializing_if = "Option::is_none",
84 serialize_with = "crate::serde_helpers::round_4_opt"
85 )]
86 pub kurtosis: Option<f64>,
87 #[serde(skip_serializing_if = "Option::is_none")]
88 pub is_approximate: Option<bool>,
89 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub outlier_count: Option<usize>,
97}
98
99impl NumericStats {
100 pub fn empty() -> Self {
101 Self {
102 min: 0.0,
103 max: 0.0,
104 mean: 0.0,
105 std_dev: 0.0,
106 variance: 0.0,
107 median: None,
108 quartiles: None,
109 mode: None,
110 coefficient_of_variation: None,
111 skewness: None,
112 kurtosis: None,
113 is_approximate: None,
114 outlier_count: None,
115 }
116 }
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct TextStats {
122 pub min_length: usize,
123 pub max_length: usize,
124 #[serde(serialize_with = "crate::serde_helpers::round_2")]
125 pub avg_length: f64,
126 #[serde(skip_serializing_if = "Option::is_none")]
127 pub most_frequent: Option<Vec<FrequencyItem>>,
128 #[serde(skip_serializing_if = "Option::is_none")]
129 pub least_frequent: Option<Vec<FrequencyItem>>,
130}
131
132impl TextStats {
133 pub fn empty() -> Self {
134 Self {
135 min_length: 0,
136 max_length: 0,
137 avg_length: 0.0,
138 most_frequent: None,
139 least_frequent: None,
140 }
141 }
142
143 pub fn from_lengths(min_length: usize, max_length: usize, avg_length: f64) -> Self {
144 Self {
145 min_length: if min_length == usize::MAX {
146 0
147 } else {
148 min_length
149 },
150 max_length,
151 avg_length,
152 most_frequent: None,
153 least_frequent: None,
154 }
155 }
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct DateTimeStats {
161 pub min_datetime: String,
162 pub max_datetime: String,
163 #[serde(serialize_with = "crate::serde_helpers::round_2")]
164 pub duration_days: f64,
165 pub year_distribution: HashMap<i32, usize>,
166 pub month_distribution: HashMap<u32, usize>,
167 pub day_of_week_distribution: HashMap<String, usize>,
168 #[serde(skip_serializing_if = "Option::is_none")]
169 pub hour_distribution: Option<HashMap<u32, usize>>,
170}
171
172impl DateTimeStats {
173 pub fn empty() -> Self {
174 Self {
175 min_datetime: String::new(),
176 max_datetime: String::new(),
177 duration_days: 0.0,
178 year_distribution: HashMap::new(),
179 month_distribution: HashMap::new(),
180 day_of_week_distribution: HashMap::new(),
181 hour_distribution: None,
182 }
183 }
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct BooleanStats {
189 pub true_count: usize,
190 pub false_count: usize,
191 #[serde(serialize_with = "crate::serde_helpers::round_4")]
192 pub true_ratio: f64,
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize)]
197pub enum ColumnStats {
198 Numeric(NumericStats),
199 Text(TextStats),
200 DateTime(DateTimeStats),
201 Boolean(BooleanStats),
202 None,
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn test_column_profile_json_roundtrip() {
211 let profile = ColumnProfile {
212 name: "test_col".to_string(),
213 data_type: DataType::Integer,
214 null_count: 2,
215 total_count: 10,
216 unique_count: Some(8),
217 stats: ColumnStats::Numeric(NumericStats {
218 min: 1.0,
219 max: 100.0,
220 mean: 50.5,
221 std_dev: 28.87,
222 variance: 833.25,
223 median: Some(50.0),
224 quartiles: Some(Quartiles {
225 q1: 25.0,
226 q2: 50.0,
227 q3: 75.0,
228 iqr: 50.0,
229 }),
230 mode: Some(42.0),
231 coefficient_of_variation: Some(57.17),
232 skewness: Some(0.0),
233 kurtosis: Some(-1.2),
234 is_approximate: Some(false),
235 outlier_count: Some(0),
236 }),
237 patterns: Some(vec![]),
238 };
239
240 let json = serde_json::to_string(&profile).unwrap();
241 let deserialized: ColumnProfile = serde_json::from_str(&json).unwrap();
242
243 assert_eq!(deserialized.name, "test_col");
244 assert_eq!(deserialized.data_type, DataType::Integer);
245 assert_eq!(deserialized.total_count, 10);
246 assert_eq!(deserialized.null_count, 2);
247
248 if let ColumnStats::Numeric(n) = &deserialized.stats {
249 assert!((n.min - 1.0).abs() < 0.01);
250 assert!((n.max - 100.0).abs() < 0.01);
251 assert!((n.mean - 50.5).abs() < 0.01);
252 assert!(n.median.is_some());
253 assert!(n.quartiles.is_some());
254 } else {
255 panic!("Expected Numeric stats after roundtrip");
256 }
257 }
258
259 #[test]
260 fn test_text_stats_json_roundtrip() {
261 let profile = ColumnProfile {
262 name: "name".to_string(),
263 data_type: DataType::String,
264 null_count: 0,
265 total_count: 3,
266 unique_count: Some(3),
267 stats: ColumnStats::Text(TextStats {
268 min_length: 3,
269 max_length: 7,
270 avg_length: 5.0,
271 most_frequent: None,
272 least_frequent: None,
273 }),
274 patterns: Some(vec![]),
275 };
276
277 let json = serde_json::to_string(&profile).unwrap();
278 let deserialized: ColumnProfile = serde_json::from_str(&json).unwrap();
279
280 assert_eq!(deserialized.data_type, DataType::String);
281 if let ColumnStats::Text(t) = &deserialized.stats {
282 assert_eq!(t.min_length, 3);
283 assert_eq!(t.max_length, 7);
284 } else {
285 panic!("Expected Text stats after roundtrip");
286 }
287 }
288}