Skip to main content

runmat_runtime/builtins/table/object/
analytics.rs

1use 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}