Skip to main content

polars_core/series/ops/
reshape.rs

1use std::borrow::Cow;
2
3use polars_arrow::bitmap::Bitmap;
4use polars_arrow::offset::{Offsets, OffsetsBuffer};
5use polars_compute::gather::sublist::list::array_to_unit_list;
6use polars_error::{PolarsResult, polars_bail, polars_ensure};
7use polars_utils::format_tuple;
8
9use crate::chunked_array::builder::get_list_builder;
10use crate::datatypes::{DataType, ListChunked};
11use crate::prelude::{IntoSeries, Series, *};
12
13impl Series {
14    /// Recurse nested types until we are at the leaf array.
15    pub fn get_leaf_array(&self) -> Series {
16        let s = self;
17        match s.dtype() {
18            #[cfg(feature = "dtype-array")]
19            DataType::Array(dtype, _) => {
20                let ca = s.array().unwrap();
21                let chunks = ca
22                    .downcast_iter()
23                    .map(|arr| arr.values().clone())
24                    .collect::<Vec<_>>();
25                // Safety: guarded by the type system
26                unsafe { Series::from_chunks_and_dtype_unchecked(s.name().clone(), chunks, dtype) }
27                    .get_leaf_array()
28            },
29            DataType::List(dtype) => {
30                let ca = s.list().unwrap();
31                let chunks = ca
32                    .downcast_iter()
33                    .map(|arr| arr.values().clone())
34                    .collect::<Vec<_>>();
35                // Safety: guarded by the type system
36                unsafe { Series::from_chunks_and_dtype_unchecked(s.name().clone(), chunks, dtype) }
37                    .get_leaf_array()
38            },
39            _ => s.clone(),
40        }
41    }
42
43    /// TODO: Move this somewhere else?
44    pub fn list_offsets_and_validities_recursive(
45        &self,
46    ) -> (Vec<OffsetsBuffer<i64>>, Vec<Option<Bitmap>>) {
47        let mut offsets = vec![];
48        let mut validities = vec![];
49
50        let mut s = self.rechunk();
51
52        while let DataType::List(_) = s.dtype() {
53            let ca = s.list().unwrap();
54            offsets.push(ca.offsets().unwrap());
55            validities.push(ca.rechunk_validity());
56            s = ca.get_inner();
57        }
58
59        (offsets, validities)
60    }
61
62    /// Wrap each element of this Series in a single-element list.
63    /// A Series `[1, 2, 3]` becomes `[[1], [2], [3]]`.
64    pub fn to_unit_list(&self) -> ListChunked {
65        let mut ca = ListChunked::from_chunk_iter(
66            self.name().clone(),
67            self.chunks()
68                .iter()
69                .map(|arr| array_to_unit_list(arr.clone())),
70        );
71        // SAFETY: each list wraps an unchanged, valid element of `self`.
72        unsafe { ca.set_inner_dtype(self.dtype().clone()) };
73        ca.set_fast_explode();
74        ca
75    }
76
77    /// Convert the values of this Series to a ListChunked with a length of 1,
78    /// so a Series of `[1, 2, 3]` becomes `[[1, 2, 3]]`.
79    pub fn implode(&self) -> PolarsResult<ListChunked> {
80        let s = self;
81        let s = s.rechunk();
82        let values = s.array_ref(0);
83
84        let offsets = vec![0i64, values.len() as i64];
85        let inner_type = s.dtype();
86
87        let dtype = ListArray::<i64>::default_datatype(values.dtype().clone());
88
89        // SAFETY: offsets are correct.
90        let arr = unsafe {
91            ListArray::new(
92                dtype,
93                Offsets::new_unchecked(offsets).into(),
94                values.clone(),
95                None,
96            )
97        };
98
99        let mut ca = ListChunked::with_chunk(s.name().clone(), arr);
100        unsafe { ca.to_logical(inner_type.clone()) };
101        ca.set_fast_explode();
102        Ok(ca)
103    }
104
105    #[cfg(feature = "dtype-array")]
106    pub fn reshape_array(&self, dimensions: &[ReshapeDimension]) -> PolarsResult<Series> {
107        polars_ensure!(
108            !dimensions.is_empty(),
109            InvalidOperation: "at least one dimension must be specified"
110        );
111
112        // Null rows may store any values: null them, then skip the values of null lists.
113        let mut leaf_array = self.propagate_nulls().unwrap_or_else(|| self.clone());
114        loop {
115            leaf_array = match leaf_array.dtype() {
116                DataType::Array(_, _) => leaf_array.array()?.get_inner(),
117                DataType::List(_) => leaf_array.explode(ExplodeOptions {
118                    empty_as_null: false,
119                    keep_nulls: false,
120                })?,
121                _ => break,
122            };
123        }
124        let leaf_array = leaf_array.rechunk();
125        let size = leaf_array.len();
126
127        let mut total_dim_size = 1;
128        let mut num_infers = 0;
129        for &dim in dimensions {
130            match dim {
131                ReshapeDimension::Infer => num_infers += 1,
132                ReshapeDimension::Specified(dim) => total_dim_size *= dim.get() as usize,
133            }
134        }
135
136        polars_ensure!(num_infers <= 1, InvalidOperation: "can only specify one inferred dimension");
137
138        if size == 0 {
139            polars_ensure!(
140                num_infers > 0 || total_dim_size == 0,
141                InvalidOperation: "cannot reshape empty array into shape without zero dimension: {}",
142                format_tuple!(dimensions),
143            );
144
145            let mut prev_arrow_dtype = leaf_array
146                .dtype()
147                .to_physical()
148                .to_arrow(CompatLevel::newest());
149            let mut prev_dtype = leaf_array.dtype().clone();
150            let mut prev_array = leaf_array.chunks()[0].clone();
151
152            // @NOTE: We need to collect the iterator here because it is lazily processed.
153            let mut current_length = dimensions[0].get_or_infer(0);
154            let len_iter = dimensions[1..]
155                .iter()
156                .map(|d| {
157                    let length = current_length as usize;
158                    current_length *= d.get_or_infer(0);
159                    length
160                })
161                .collect::<Vec<_>>();
162
163            // We pop the outer dimension as that is the height of the series.
164            for (dim, length) in dimensions[1..].iter().zip(len_iter).rev() {
165                // Infer dimension if needed
166                let dim = dim.get_or_infer(0);
167                prev_arrow_dtype = prev_arrow_dtype.to_fixed_size_list(dim as usize, true);
168                prev_dtype = DataType::Array(Box::new(prev_dtype), dim as usize);
169
170                prev_array =
171                    FixedSizeListArray::new(prev_arrow_dtype.clone(), length, prev_array, None)
172                        .boxed();
173            }
174
175            return Ok(unsafe {
176                Series::from_chunks_and_dtype_unchecked(
177                    leaf_array.name().clone(),
178                    vec![prev_array],
179                    &prev_dtype,
180                )
181            });
182        }
183
184        polars_ensure!(
185            total_dim_size > 0,
186            InvalidOperation: "cannot reshape non-empty array into shape containing a zero dimension: {}",
187            format_tuple!(dimensions)
188        );
189
190        polars_ensure!(
191            size.is_multiple_of(total_dim_size),
192            InvalidOperation: "cannot reshape array of size {} into shape {}", size, format_tuple!(dimensions)
193        );
194
195        let leaf_array = leaf_array.rechunk();
196        let mut prev_arrow_dtype = leaf_array
197            .dtype()
198            .to_physical()
199            .to_arrow(CompatLevel::newest());
200        let mut prev_dtype = leaf_array.dtype().clone();
201        let mut prev_array = leaf_array.chunks()[0].clone();
202        let inferred_size = (size / total_dim_size) as u64;
203        let outer_dimension = dimensions[0].get_or_infer(inferred_size);
204
205        // We pop the outer dimension as that is the height of the series.
206        for dim in dimensions[1..].iter().rev() {
207            // Infer dimension if needed
208            let dim = dim.get_or_infer(inferred_size);
209            prev_arrow_dtype = prev_arrow_dtype.to_fixed_size_list(dim as usize, true);
210            prev_dtype = DataType::Array(Box::new(prev_dtype), dim as usize);
211
212            prev_array = FixedSizeListArray::new(
213                prev_arrow_dtype.clone(),
214                prev_array.len() / dim as usize,
215                prev_array,
216                None,
217            )
218            .boxed();
219        }
220
221        polars_ensure!(
222            prev_array.len() as u64 == outer_dimension,
223            InvalidOperation: "cannot reshape array of size {} into shape {}", size, format_tuple!(dimensions)
224        );
225
226        Ok(unsafe {
227            Series::from_chunks_and_dtype_unchecked(
228                leaf_array.name().clone(),
229                vec![prev_array],
230                &prev_dtype,
231            )
232        })
233    }
234
235    pub fn reshape_list(&self, dimensions: &[ReshapeDimension]) -> PolarsResult<Series> {
236        polars_ensure!(
237            !dimensions.is_empty(),
238            InvalidOperation: "at least one dimension must be specified"
239        );
240
241        let s = self;
242        let s = if let DataType::List(_) = s.dtype() {
243            Cow::Owned(s.explode(ExplodeOptions {
244                empty_as_null: false,
245                keep_nulls: true,
246            })?)
247        } else {
248            Cow::Borrowed(s)
249        };
250
251        let s_ref = s.as_ref();
252
253        // let dimensions = dimensions.to_vec();
254
255        match dimensions.len() {
256            1 => {
257                polars_ensure!(
258                    dimensions[0].get().is_none_or( |dim| dim as usize == s_ref.len()),
259                    InvalidOperation: "cannot reshape len {} into shape {:?}", s_ref.len(), dimensions,
260                );
261                Ok(s_ref.clone())
262            },
263            2 => {
264                let rows = dimensions[0];
265                let cols = dimensions[1];
266
267                if s_ref.is_empty() {
268                    if rows.get_or_infer(0) == 0 && cols.get_or_infer(0) <= 1 {
269                        return Ok(s_ref.to_unit_list().into_series());
270                    } else {
271                        polars_bail!(InvalidOperation: "cannot reshape len 0 into shape {}", format_tuple!(dimensions))
272                    }
273                }
274
275                use ReshapeDimension as RD;
276                // Infer dimension.
277
278                let (rows, cols) = match (rows, cols) {
279                    (RD::Infer, RD::Specified(cols)) if cols.get() >= 1 => {
280                        (s_ref.len() as u64 / cols.get(), cols.get())
281                    },
282                    (RD::Specified(rows), RD::Infer) if rows.get() >= 1 => {
283                        (rows.get(), s_ref.len() as u64 / rows.get())
284                    },
285                    (RD::Infer, RD::Infer) => (s_ref.len() as u64, 1u64),
286                    (RD::Specified(rows), RD::Specified(cols)) => (rows.get(), cols.get()),
287                    _ => polars_bail!(InvalidOperation: "reshape of non-zero list into zero list"),
288                };
289
290                // Fast path, we can create a unit list so we only allocate offsets.
291                if rows as usize == s_ref.len() && cols == 1 {
292                    return Ok(s_ref.to_unit_list().into_series());
293                }
294
295                polars_ensure!(
296                    (rows*cols) as usize == s_ref.len() && rows >= 1 && cols >= 1,
297                    InvalidOperation: "cannot reshape len {} into shape {:?}", s_ref.len(), dimensions,
298                );
299
300                let mut builder =
301                    get_list_builder(s_ref.dtype(), s_ref.len(), rows as usize, s.name().clone());
302
303                let mut offset = 0u64;
304                for _ in 0..rows {
305                    let row = s_ref.slice(offset as i64, cols as usize);
306                    builder.append_series(&row).unwrap();
307                    offset += cols;
308                }
309                Ok(builder.finish().into_series())
310            },
311            _ => {
312                polars_bail!(InvalidOperation: "more than two dimensions not supported in reshaping to List.\n\nConsider reshaping to Array type.");
313            },
314        }
315    }
316}
317
318#[cfg(test)]
319mod test {
320    use super::*;
321    use crate::prelude::*;
322
323    #[test]
324    fn test_to_list() -> PolarsResult<()> {
325        let s = Series::new("a".into(), &[1, 2, 3]);
326
327        let mut builder = get_list_builder(s.dtype(), s.len(), 1, s.name().clone());
328        builder.append_series(&s).unwrap();
329        let expected = builder.finish();
330
331        let out = s.implode()?;
332        assert!(expected.into_series().equals(&out.into_series()));
333
334        Ok(())
335    }
336
337    #[test]
338    fn test_reshape() -> PolarsResult<()> {
339        let s = Series::new("a".into(), &[1, 2, 3, 4]);
340
341        for (dims, list_len) in [
342            (&[-1, 1], 4),
343            (&[4, 1], 4),
344            (&[2, 2], 2),
345            (&[-1, 2], 2),
346            (&[2, -1], 2),
347        ] {
348            let dims = dims
349                .iter()
350                .map(|&v| ReshapeDimension::new(v))
351                .collect::<Vec<_>>();
352            let out = s.reshape_list(&dims)?;
353            assert_eq!(out.len(), list_len);
354            assert!(matches!(out.dtype(), DataType::List(_)));
355            assert_eq!(
356                out.explode(ExplodeOptions {
357                    empty_as_null: true,
358                    keep_nulls: true,
359                })?
360                .len(),
361                4
362            );
363        }
364
365        Ok(())
366    }
367}