Skip to main content

polars_core/frame/
horizontal.rs

1use polars_error::{PolarsResult, polars_err};
2
3use super::Column;
4use crate::datatypes::AnyValue;
5use crate::frame::DataFrame;
6use crate::frame::validation::validate_columns_slice;
7
8impl DataFrame {
9    /// Add columns horizontally.
10    ///
11    /// # Safety
12    /// The caller must ensure:
13    /// - the length of all [`Column`] is equal to the height of this [`DataFrame`]
14    /// - the columns names are unique
15    ///
16    /// Note: If `self` is empty, `self.height` will always be overridden by the height of the first
17    /// column in `columns`.
18    ///
19    /// Note that on a debug build this will panic on duplicates / height mismatch.
20    pub unsafe fn hstack_mut_unchecked(&mut self, columns: &[Column]) -> &mut Self {
21        if self.shape() == (0, 0)
22            && let Some(c) = columns.first()
23        {
24            unsafe { self.set_height(c.len()) };
25        }
26
27        unsafe { self.columns_mut() }.extend_from_slice(columns);
28
29        if cfg!(debug_assertions) {
30            if let err @ Err(_) = validate_columns_slice(self.height(), self.columns()) {
31                let initial_width = self.width() - columns.len();
32                unsafe { self.columns_mut() }.truncate(initial_width);
33                err.unwrap();
34            }
35        }
36
37        self
38    }
39
40    /// Add multiple [`Column`] to a [`DataFrame`].
41    /// Errors if the resulting DataFrame columns have duplicate names or unequal heights.
42    ///
43    /// Note: If `self` is empty, `self.height` will always be overridden by the height of the first
44    /// column in `columns`.
45    ///
46    /// # Example
47    ///
48    /// ```rust
49    /// # use polars_core::prelude::*;
50    /// fn stack(df: &mut DataFrame, columns: &[Column]) {
51    ///     df.hstack_mut(columns);
52    /// }
53    /// ```
54    pub fn hstack_mut(&mut self, columns: &[Column]) -> PolarsResult<&mut Self> {
55        if self.shape() == (0, 0)
56            && let Some(c) = columns.first()
57        {
58            unsafe { self.set_height(c.len()) };
59        }
60
61        unsafe { self.columns_mut() }.extend_from_slice(columns);
62
63        if let err @ Err(_) = validate_columns_slice(self.height(), self.columns()) {
64            let initial_width = self.width() - columns.len();
65            unsafe { self.columns_mut() }.truncate(initial_width);
66            err?;
67        }
68
69        Ok(self)
70    }
71}
72
73/// Concat [`DataFrame`]s horizontally.
74///
75/// If the lengths don't match and strict is false we pad with nulls, or return a `ShapeError` if strict is true.
76pub fn concat_df_horizontal(
77    dfs: &[DataFrame],
78    check_duplicates: bool,
79    strict: bool,
80    unit_length_as_scalar: bool,
81) -> PolarsResult<DataFrame> {
82    if dfs.is_empty() {
83        return Err(polars_err!(ComputeError: "cannot concat empty dataframes"));
84    }
85    let heights = || dfs.iter().map(|df| df.height());
86    let output_height = if unit_length_as_scalar {
87        heights().filter(|h| *h != 1).max().unwrap_or(1)
88    } else {
89        heights().max().unwrap()
90    };
91
92    let owned_df;
93
94    let mut out_width = 0;
95
96    let all_equal_height = dfs.iter().filter(|df| df.shape() != (0, 0)).all(|df| {
97        out_width += df.width();
98        df.height() == output_height
99    });
100
101    // if not all equal length, extend the DataFrame with nulls
102    let dfs = if !all_equal_height {
103        if strict {
104            return Err(
105                polars_err!(ShapeMismatch: "cannot concat dataframes with different heights in 'strict' mode"),
106            );
107        }
108        out_width = 0;
109
110        owned_df = dfs
111            .iter()
112            .filter(|df| df.shape() != (0, 0))
113            .cloned()
114            .map(|mut df| {
115                out_width += df.width();
116                let h = df.height();
117
118                if h != output_height {
119                    if unit_length_as_scalar && h == 1 {
120                        // SAFETY: We extend each scalar column length to
121                        // `output_height`. Then, we set the height of the resulting dataframe.
122                        unsafe { df.columns_mut() }.iter_mut().for_each(|c| {
123                            let Column::Scalar(s) = c else {
124                                panic!("only supported for scalars");
125                            };
126
127                            *c = Column::Scalar(s.resize(output_height));
128                        });
129                    } else {
130                        let diff = output_height - h;
131
132                        // SAFETY: We extend each column with nulls to the point of being of length
133                        // `output_height`. Then, we set the height of the resulting dataframe.
134                        unsafe { df.columns_mut() }.iter_mut().for_each(|c| {
135                            *c = c.extend_constant(AnyValue::Null, diff).unwrap();
136                        });
137                    }
138                    unsafe {
139                        df.set_height(output_height);
140                    }
141                }
142
143                df
144            })
145            .collect::<Vec<_>>();
146        owned_df.as_slice()
147    } else {
148        dfs
149    };
150
151    let mut acc_cols = Vec::with_capacity(out_width);
152
153    for df in dfs {
154        acc_cols.extend(df.columns().iter().cloned());
155    }
156
157    let df = if check_duplicates {
158        DataFrame::new(output_height, acc_cols)?
159    } else {
160        unsafe { DataFrame::new_unchecked(output_height, acc_cols) }
161    };
162
163    Ok(df)
164}
165
166#[cfg(test)]
167mod tests {
168    use polars_error::PolarsError;
169
170    #[test]
171    fn test_hstack_mut_empty_frame_height_validation() {
172        use crate::frame::DataFrame;
173        use crate::prelude::{Column, DataType};
174        let mut df = DataFrame::empty();
175        let result = df.hstack_mut(&[
176            Column::full_null("a".into(), 1, &DataType::Null),
177            Column::full_null("b".into(), 3, &DataType::Null),
178        ]);
179
180        assert!(
181            matches!(result, Err(PolarsError::ShapeMismatch(_))),
182            "expected shape mismatch error"
183        );
184        // Ensure the DataFrame is not mutated in the error case.
185        assert_eq!(df.width(), 0);
186    }
187
188    #[test]
189    fn test_scalar_broadcast_over_empty_frame() {
190        use crate::prelude::*;
191
192        // Unit-length "scalar" frame broadcast against zero-row frame should yield zero rows
193        let empty = df!["a" => Vec::<i64>::new()].unwrap();
194        let scalar = df!["b" => [1i64]].unwrap();
195        let out = super::concat_df_horizontal(&[empty, scalar], true, false, true).unwrap();
196        assert_eq!(out.height(), 0);
197        assert_eq!(out.width(), 2);
198
199        // Non-empty frame must still broadcast the scalar across all its rows
200        let non_empty = df!["a" => [10i64, 20, 30]].unwrap();
201        let scalar = df!["b" => [1i64]].unwrap();
202        let out = super::concat_df_horizontal(&[non_empty, scalar], true, false, true).unwrap();
203        assert_eq!(out.height(), 3);
204        assert_eq!(
205            out.column("b").unwrap().as_materialized_series(),
206            &Series::new("b".into(), [1i64, 1, 1])
207        );
208    }
209}