runmat_runtime/builtins/table/object/
analytics.rs1use super::selectors::parse_variable_selector;
2use super::*;
3
4mod grpstats;
5
6pub(in crate::builtins::table) use grpstats::grpstats_impl;
7
8pub fn sortrows_table(value: Value, rest: &[Value]) -> BuiltinResult<(Value, Tensor)> {
9 let object = into_table_object(value, "sortrows")?;
10 let names = table_variable_names_from_object(&object)?;
11 let sort_spec = SortSpec::parse(rest, &names)?;
12 let height = table_height(&object)?;
13 let variables = table_variables(&object)?;
14 let mut indices: Vec<usize> = (0..height).collect();
15 indices.sort_by(|&a, &b| {
16 for key in &sort_spec.keys {
17 let Some(value) = variables.fields.get(&key.name) else {
18 continue;
19 };
20 let ord = compare_table_cells(value, a, b).unwrap_or(Ordering::Equal);
21 let ord = if key.descending { ord.reverse() } else { ord };
22 if ord != Ordering::Equal {
23 return ord;
24 }
25 }
26 a.cmp(&b)
27 });
28 let mut sorted_columns = Vec::with_capacity(names.len());
29 for name in &names {
30 let value = variables
31 .fields
32 .get(name)
33 .ok_or_else(|| invalid_variable(format!("table: missing variable '{name}'")))?;
34 sorted_columns.push(select_rows(value, &indices)?);
35 }
36 let row_names = selected_row_names(&object, &indices)?;
37 let sorted = table_from_columns_with_properties(names, sorted_columns, row_names)?;
38 let indices_tensor = Tensor::new(
39 indices.iter().map(|idx| *idx as f64 + 1.0).collect(),
40 vec![indices.len(), 1],
41 )
42 .map_err(invalid_variable)?;
43 Ok((sorted, indices_tensor))
44}
45
46pub(in crate::builtins::table) struct SortSpec {
47 keys: Vec<SortKey>,
48}
49
50pub(in crate::builtins::table) struct SortKey {
51 name: String,
52 descending: bool,
53}
54
55impl SortSpec {
56 fn parse(rest: &[Value], names: &[String]) -> BuiltinResult<Self> {
57 let mut keys = if rest.is_empty() {
58 names
59 .iter()
60 .map(|name| SortKey {
61 name: name.clone(),
62 descending: false,
63 })
64 .collect::<Vec<_>>()
65 } else {
66 parse_variable_selector(rest.first(), names)?
67 .into_iter()
68 .map(|name| SortKey {
69 name,
70 descending: false,
71 })
72 .collect()
73 };
74 if let Some(direction) = rest.get(1) {
75 let directions = string_list(direction)?;
76 if directions.len() == 1 {
77 let descending = directions[0].eq_ignore_ascii_case("descend")
78 || directions[0].eq_ignore_ascii_case("desc");
79 for key in &mut keys {
80 key.descending = descending;
81 }
82 } else {
83 for (key, direction) in keys.iter_mut().zip(directions.iter()) {
84 key.descending = direction.eq_ignore_ascii_case("descend")
85 || direction.eq_ignore_ascii_case("desc");
86 }
87 }
88 }
89 Ok(Self { keys })
90 }
91}
92
93pub(in crate::builtins::table) fn compare_table_cells(
94 value: &Value,
95 a: usize,
96 b: usize,
97) -> BuiltinResult<Ordering> {
98 match value {
99 Value::Tensor(tensor) => Ok(tensor
100 .get2(a, 0)
101 .map_err(invalid_index)?
102 .partial_cmp(&tensor.get2(b, 0).map_err(invalid_index)?)
103 .unwrap_or(Ordering::Greater)),
104 Value::StringArray(array) => {
105 let av = array.data.get(a).cloned().unwrap_or_default();
106 let bv = array.data.get(b).cloned().unwrap_or_default();
107 Ok(av.cmp(&bv))
108 }
109 Value::LogicalArray(array) => {
110 let av = *array.data.get(a).unwrap_or(&0);
111 let bv = *array.data.get(b).unwrap_or(&0);
112 Ok(av.cmp(&bv))
113 }
114 Value::Object(obj) if obj.is_class("datetime") => {
115 let tensor = crate::builtins::datetime::serials_from_datetime_value(value)?;
116 Ok(tensor
117 .data
118 .get(a)
119 .copied()
120 .unwrap_or(f64::NAN)
121 .partial_cmp(&tensor.data.get(b).copied().unwrap_or(f64::NAN))
122 .unwrap_or(Ordering::Greater))
123 }
124 other => Ok(cell_key_string(other, a).cmp(&cell_key_string(other, b))),
125 }
126}
127
128#[derive(Clone, Debug)]
129pub(in crate::builtins::table) enum GroupAtom {
130 Number(f64),
131 Text(String),
132 Logical(bool),
133 Missing,
134}
135
136impl GroupAtom {
137 fn rank(&self) -> u8 {
138 match self {
139 Self::Missing => 0,
140 Self::Logical(_) => 1,
141 Self::Number(_) => 2,
142 Self::Text(_) => 3,
143 }
144 }
145}
146
147impl PartialEq for GroupAtom {
148 fn eq(&self, other: &Self) -> bool {
149 self.cmp(other) == Ordering::Equal
150 }
151}
152
153impl Eq for GroupAtom {}
154
155impl PartialOrd for GroupAtom {
156 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
157 Some(self.cmp(other))
158 }
159}
160
161impl Ord for GroupAtom {
162 fn cmp(&self, other: &Self) -> Ordering {
163 let rank = self.rank().cmp(&other.rank());
164 if rank != Ordering::Equal {
165 return rank;
166 }
167 match (self, other) {
168 (Self::Missing, Self::Missing) => Ordering::Equal,
169 (Self::Logical(a), Self::Logical(b)) => a.cmp(b),
170 (Self::Number(a), Self::Number(b)) => a.total_cmp(b),
171 (Self::Text(a), Self::Text(b)) => a.cmp(b),
172 _ => Ordering::Equal,
173 }
174 }
175}
176
177pub(in crate::builtins::table) fn cell_group_atom(value: &Value, row: usize) -> GroupAtom {
178 match value {
179 Value::Tensor(tensor) => tensor
180 .get2(row, 0)
181 .map(GroupAtom::Number)
182 .unwrap_or(GroupAtom::Missing),
183 Value::StringArray(array) => array
184 .data
185 .get(row)
186 .cloned()
187 .map(GroupAtom::Text)
188 .unwrap_or(GroupAtom::Missing),
189 Value::LogicalArray(array) => array
190 .data
191 .get(row)
192 .map(|value| GroupAtom::Logical(*value != 0))
193 .unwrap_or(GroupAtom::Missing),
194 Value::Object(obj) if obj.is_class("datetime") => {
195 crate::builtins::datetime::serials_from_datetime_value(value)
196 .ok()
197 .and_then(|tensor| tensor.data.get(row).copied())
198 .map(GroupAtom::Number)
199 .unwrap_or(GroupAtom::Missing)
200 }
201 other => GroupAtom::Text(cell_key_string(other, row)),
202 }
203}
204
205pub(in crate::builtins::table) fn pivot_impl(
206 table: Value,
207 rowvars: Value,
208 colvars: Value,
209 datavar: Value,
210 method: &str,
211) -> BuiltinResult<Value> {
212 let object = into_table_object(table, "pivot")?;
213 let names = table_variable_names_from_object(&object)?;
214 let row_names = parse_variable_selector_for_object(Some(&rowvars), &object, &names)?;
215 let col_names = parse_variable_selector_for_object(Some(&colvars), &object, &names)?;
216 let data_names = parse_variable_selector_for_object(Some(&datavar), &object, &names)?;
217 if row_names.is_empty() || col_names.is_empty() || data_names.is_empty() {
218 return Err(invalid_argument(
219 "pivot: rowvars, colvars, and datavar must select at least one variable",
220 ));
221 }
222 if data_names.len() != 1 {
223 return Err(invalid_argument(
224 "pivot: exactly one data variable is currently supported",
225 ));
226 }
227 let data_name = &data_names[0];
228 let variables = table_variables(&object)?;
229 let data_value = variables
230 .fields
231 .get(data_name)
232 .ok_or_else(|| invalid_variable(format!("pivot: missing data variable '{data_name}'")))?;
233 if !matches!(data_value, Value::Tensor(tensor) if tensor.cols() == 1) {
234 return Err(invalid_variable(
235 "pivot: data variable must be a numeric column vector",
236 ));
237 }
238
239 let height = table_height(&object)?;
240 let mut row_order = Vec::<Vec<GroupAtom>>::new();
241 let mut row_first_index = BTreeMap::<Vec<GroupAtom>, usize>::new();
242 let mut col_order = Vec::<Vec<GroupAtom>>::new();
243 let mut col_seen = BTreeMap::<Vec<GroupAtom>, ()>::new();
244 let mut buckets = BTreeMap::<(Vec<GroupAtom>, Vec<GroupAtom>), Vec<usize>>::new();
245 for row in 0..height {
246 let row_key = group_key_for_row(&variables, &row_names, row);
247 let col_key = group_key_for_row(&variables, &col_names, row);
248 if !row_first_index.contains_key(&row_key) {
249 row_first_index.insert(row_key.clone(), row);
250 row_order.push(row_key.clone());
251 }
252 if !col_seen.contains_key(&col_key) {
253 col_seen.insert(col_key.clone(), ());
254 col_order.push(col_key.clone());
255 }
256 buckets.entry((row_key, col_key)).or_default().push(row);
257 }
258
259 let mut out_names = row_names.clone();
260 let mut out_columns = Vec::with_capacity(row_names.len() + col_order.len());
261 for name in &row_names {
262 let value = variables
263 .fields
264 .get(name)
265 .ok_or_else(|| invalid_variable(format!("pivot: missing row variable '{name}'")))?;
266 let rows = row_order
267 .iter()
268 .filter_map(|key| row_first_index.get(key).copied())
269 .collect::<Vec<_>>();
270 out_columns.push(select_rows(value, &rows)?);
271 }
272 for col_key in &col_order {
273 let mut values = Vec::with_capacity(row_order.len());
274 for row_key in &row_order {
275 let summary_rows = buckets
276 .get(&(row_key.clone(), col_key.clone()))
277 .cloned()
278 .unwrap_or_default();
279 if summary_rows.is_empty() {
280 values.push(f64::NAN);
281 } else {
282 values.push(
283 summarize_groups(data_value, std::iter::once(&summary_rows), method)?
284 .into_iter()
285 .next()
286 .unwrap_or(f64::NAN),
287 );
288 }
289 }
290 out_names.push(format!(
291 "{}_{}",
292 make_valid_variable_name(&group_key_label(col_key), out_names.len() + 1),
293 data_name
294 ));
295 out_columns.push(Value::Tensor(
296 Tensor::new(values, vec![row_order.len(), 1]).map_err(invalid_variable)?,
297 ));
298 }
299 let out_names = make_unique_variable_names(out_names);
300 table_from_columns(out_names, out_columns)
301}
302
303pub(in crate::builtins::table) fn group_key_for_row(
304 variables: &StructValue,
305 names: &[String],
306 row: usize,
307) -> Vec<GroupAtom> {
308 names
309 .iter()
310 .map(|name| {
311 variables
312 .fields
313 .get(name)
314 .map(|value| cell_group_atom(value, row))
315 .unwrap_or(GroupAtom::Missing)
316 })
317 .collect()
318}
319
320pub(in crate::builtins::table) fn group_key_label(key: &[GroupAtom]) -> String {
321 if key.is_empty() {
322 return "missing".to_string();
323 }
324 key.iter()
325 .map(group_atom_label)
326 .collect::<Vec<_>>()
327 .join("_")
328}
329
330pub(in crate::builtins::table) fn group_atom_label(atom: &GroupAtom) -> String {
331 match atom {
332 GroupAtom::Number(value) => format_key_number(*value),
333 GroupAtom::Text(text) => text.clone(),
334 GroupAtom::Logical(flag) => flag.to_string(),
335 GroupAtom::Missing => "missing".to_string(),
336 }
337}
338
339pub(in crate::builtins::table) fn groupsummary_impl(
340 table: Value,
341 groupvars: Value,
342 method: Value,
343 rest: Vec<Value>,
344) -> BuiltinResult<Value> {
345 let object = into_table_object(table, "groupsummary")?;
346 let names = table_variable_names_from_object(&object)?;
347 let group_names = parse_variable_selector_for_object(Some(&groupvars), &object, &names)?;
348 let methods = string_list(&method)?;
349 if methods.is_empty() {
350 return Err(invalid_argument(
351 "groupsummary: method list must not be empty",
352 ));
353 }
354 let data_names = if let Some(value) = rest.first() {
355 parse_variable_selector_for_object(Some(value), &object, &names)?
356 } else {
357 names
358 .iter()
359 .filter(|name| !group_names.contains(name))
360 .filter(|name| {
361 table_variables(&object)
362 .ok()
363 .and_then(|vars| vars.fields.get(*name).cloned())
364 .map(|value| matches!(value, Value::Tensor(_)))
365 .unwrap_or(false)
366 })
367 .cloned()
368 .collect()
369 };
370 let variables = table_variables(&object)?;
371 let height = table_height(&object)?;
372 let mut groups: BTreeMap<Vec<GroupAtom>, Vec<usize>> = BTreeMap::new();
373 for row in 0..height {
374 let key = group_names
375 .iter()
376 .map(|name| {
377 variables
378 .fields
379 .get(name)
380 .map(|value| cell_group_atom(value, row))
381 .unwrap_or(GroupAtom::Missing)
382 })
383 .collect::<Vec<_>>();
384 groups.entry(key).or_default().push(row);
385 }
386 let group_rows = groups
387 .values()
388 .filter_map(|rows| rows.first().copied())
389 .collect::<Vec<_>>();
390 let mut out_names = Vec::new();
391 let mut out_columns = Vec::new();
392 for name in &group_names {
393 let value = variables.fields.get(name).ok_or_else(|| {
394 invalid_variable(format!("groupsummary: missing group variable '{name}'"))
395 })?;
396 out_names.push(name.clone());
397 out_columns.push(select_rows(value, &group_rows)?);
398 }
399 out_names.push("GroupCount".to_string());
400 out_columns.push(Value::Tensor(
401 Tensor::new(
402 groups.values().map(|rows| rows.len() as f64).collect(),
403 vec![groups.len(), 1],
404 )
405 .map_err(invalid_variable)?,
406 ));
407 for method in &methods {
408 for name in &data_names {
409 let value = variables.fields.get(name).ok_or_else(|| {
410 invalid_variable(format!("groupsummary: missing data variable '{name}'"))
411 })?;
412 let values = summarize_groups(value, groups.values(), method)?;
413 out_names.push(format!("{}_{}", method.to_ascii_lowercase(), name));
414 out_columns.push(Value::Tensor(
415 Tensor::new(values, vec![groups.len(), 1]).map_err(invalid_variable)?,
416 ));
417 }
418 }
419 table_from_columns(out_names, out_columns)
420}
421
422pub(in crate::builtins::table) fn summarize_groups<'a>(
423 value: &Value,
424 groups: impl Iterator<Item = &'a Vec<usize>>,
425 method: &str,
426) -> BuiltinResult<Vec<f64>> {
427 let tensor = match value {
428 Value::Tensor(tensor) if tensor.cols() == 1 => tensor,
429 _ => {
430 return Err(invalid_variable(
431 "groupsummary: summary data variables must be numeric column vectors",
432 ))
433 }
434 };
435 groups
436 .map(|rows| {
437 let mut values = rows
438 .iter()
439 .map(|row| tensor.get2(*row, 0).map_err(invalid_index))
440 .collect::<BuiltinResult<Vec<_>>>()?;
441 values.retain(|value| !value.is_nan());
442 let result = match method.to_ascii_lowercase().as_str() {
443 "mean" => {
444 if values.is_empty() {
445 f64::NAN
446 } else {
447 values.iter().sum::<f64>() / values.len() as f64
448 }
449 }
450 "sum" => values.iter().sum(),
451 "min" => values.into_iter().fold(f64::INFINITY, f64::min),
452 "max" => values.into_iter().fold(f64::NEG_INFINITY, f64::max),
453 "median" => {
454 if values.is_empty() {
455 f64::NAN
456 } else {
457 values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
458 let mid = values.len() / 2;
459 if values.len() % 2 == 0 {
460 (values[mid - 1] + values[mid]) / 2.0
461 } else {
462 values[mid]
463 }
464 }
465 }
466 "count" | "numel" => values.len() as f64,
467 other => {
468 return Err(invalid_argument(format!(
469 "groupsummary: unsupported method '{other}'"
470 )))
471 }
472 };
473 Ok(result)
474 })
475 .collect()
476}
477
478pub(in crate::builtins::table) fn cell_key_string(value: &Value, row: usize) -> String {
479 match value {
480 Value::Tensor(tensor) => tensor
481 .get2(row, 0)
482 .map(format_key_number)
483 .unwrap_or_default(),
484 Value::StringArray(array) => array.data.get(row).cloned().unwrap_or_default(),
485 Value::LogicalArray(array) => array
486 .data
487 .get(row)
488 .map(|value| value.to_string())
489 .unwrap_or_default(),
490 Value::Object(obj) if obj.is_class("datetime") => {
491 crate::builtins::datetime::serials_from_datetime_value(value)
492 .ok()
493 .and_then(|tensor| tensor.data.get(row).copied())
494 .map(format_key_number)
495 .unwrap_or_default()
496 }
497 Value::Object(obj) if obj.is_class("duration") => {
498 crate::builtins::duration::duration_tensor_from_duration_value(value)
499 .ok()
500 .and_then(|tensor| tensor.data.get(row).copied())
501 .map(format_key_number)
502 .unwrap_or_default()
503 }
504 Value::Object(obj) if obj.is_class(CATEGORICAL_CLASS) => {
505 categorical_label_at(obj, row).unwrap_or_default()
506 }
507 Value::Cell(cell) => cell
508 .get(row, 0)
509 .map(|item| cell_to_text(&item))
510 .unwrap_or_default(),
511 other => format!("{other}"),
512 }
513}