1use std::fmt::Write;
2
3use polars_arrow::array::ValueSize;
4use polars_compute::gather::sublist::list::{index_is_oob, sublist_get};
5use polars_core::chunked_array::builder::get_list_builder;
6#[cfg(feature = "diff")]
7use polars_core::series::ops::NullBehavior;
8use polars_core::utils::{CustomIterTools, try_get_supertype};
9
10use super::*;
11use crate::chunked_array::list::min_max::{list_max_function, list_min_function};
12use crate::chunked_array::list::sum_mean::sum_with_nulls;
13#[cfg(feature = "diff")]
14use crate::prelude::diff;
15use crate::prelude::list::sum_mean::{mean_list_numerical, sum_list_numerical};
16use crate::series::ArgAgg;
17
18pub(super) fn has_inner_nulls(ca: &ListChunked) -> bool {
19 for arr in ca.downcast_iter() {
20 if arr.values().null_count() > 0 {
21 return true;
22 }
23 }
24 false
25}
26
27fn cast_rhs(
28 other: &mut [Column],
29 inner_type: &DataType,
30 dtype: &DataType,
31 length: usize,
32 allow_broadcast: bool,
33) -> PolarsResult<()> {
34 for s in other.iter_mut() {
35 if !matches!(s.dtype(), DataType::List(_)) {
37 *s = s.cast(inner_type)?
38 }
39 if !matches!(s.dtype(), DataType::List(_)) && s.dtype() == inner_type {
40 *s = s
42 .reshape_list(&[ReshapeDimension::Infer, ReshapeDimension::new_dimension(1)])
43 .unwrap();
44 }
45 if s.dtype() != dtype {
46 *s = s.cast(dtype).map_err(|e| {
47 polars_err!(
48 SchemaMismatch:
49 "cannot concat `{}` into a list of `{}`: {}",
50 s.dtype(),
51 dtype,
52 e
53 )
54 })?;
55 }
56
57 if allow_broadcast {
58 s.broadcast_in_place_to(length)?;
60 } else {
61 polars_ensure!(
62 s.len() == length || s.len() == 1,
63 ShapeMismatch: "series length {} does not match expected length of {}",
64 s.len(), length
65 );
66 }
67 }
68 Ok(())
69}
70
71pub trait ListNameSpaceImpl: AsList {
72 fn lst_join(
75 &self,
76 separator: &StringChunked,
77 ignore_nulls: bool,
78 ) -> PolarsResult<StringChunked> {
79 let ca = self.as_list();
80 match ca.inner_dtype() {
81 DataType::String => match separator.len() {
82 1 => match separator.get(0) {
83 Some(separator) => self.join_literal(separator, ignore_nulls),
84 _ => Ok(StringChunked::full_null(ca.name().clone(), ca.len())),
85 },
86 _ => self.join_many(separator, ignore_nulls),
87 },
88 dt => polars_bail!(op = "`lst.join`", got = dt, expected = "String"),
89 }
90 }
91
92 fn join_literal(&self, separator: &str, ignore_nulls: bool) -> PolarsResult<StringChunked> {
93 let ca = self.as_list();
94 let mut buf = String::with_capacity(128);
96 let mut builder = StringChunkedBuilder::new(ca.name().clone(), ca.len());
97
98 ca.for_each_amortized(|opt_s| {
99 let opt_val = opt_s.and_then(|s| {
100 buf.clear();
102 let ca = s.as_ref().str().unwrap();
103
104 if ca.null_count() != 0 && !ignore_nulls {
105 return None;
106 }
107
108 for arr in ca.downcast_iter() {
109 for val in arr.non_null_values_iter() {
110 buf.write_str(val).unwrap();
111 buf.write_str(separator).unwrap();
112 }
113 }
114
115 Some(&buf[..buf.len().saturating_sub(separator.len())])
118 });
119 builder.append_option(opt_val)
120 });
121 Ok(builder.finish())
122 }
123
124 fn join_many(
125 &self,
126 separator: &StringChunked,
127 ignore_nulls: bool,
128 ) -> PolarsResult<StringChunked> {
129 let ca = self.as_list();
130 let mut buf = String::with_capacity(128);
132 let mut builder = StringChunkedBuilder::new(ca.name().clone(), ca.len());
133 {
134 ca.amortized_iter()
135 .zip(separator.iter())
136 .for_each(|(opt_s, opt_sep)| match opt_sep {
137 Some(separator) => {
138 let opt_val = opt_s.and_then(|s| {
139 buf.clear();
141 let ca = s.as_ref().str().unwrap();
142
143 if ca.null_count() != 0 && !ignore_nulls {
144 return None;
145 }
146
147 for arr in ca.downcast_iter() {
148 for val in arr.non_null_values_iter() {
149 buf.write_str(val).unwrap();
150 buf.write_str(separator).unwrap();
151 }
152 }
153
154 Some(&buf[..buf.len().saturating_sub(separator.len())])
157 });
158 builder.append_option(opt_val)
159 },
160 _ => builder.append_null(),
161 })
162 }
163 Ok(builder.finish())
164 }
165
166 fn lst_max(&self) -> PolarsResult<Series> {
167 list_max_function(self.as_list())
168 }
169
170 fn lst_min(&self) -> PolarsResult<Series> {
171 list_min_function(self.as_list())
172 }
173
174 fn lst_sum(&self) -> PolarsResult<Series> {
175 let ca = self.as_list();
176
177 if has_inner_nulls(ca) {
178 return sum_with_nulls(ca, ca.inner_dtype());
179 };
180
181 match ca.inner_dtype() {
182 DataType::Boolean => Ok(count_boolean_bits(ca).into_series()),
183 dt if dt.is_primitive_numeric() => Ok(sum_list_numerical(ca, dt)),
184 dt => sum_with_nulls(ca, dt),
185 }
186 }
187
188 fn lst_mean(&self) -> Series {
189 let ca = self.as_list();
190
191 if has_inner_nulls(ca) {
192 return sum_mean::mean_with_nulls(ca);
193 };
194
195 match ca.inner_dtype() {
196 dt if dt.is_primitive_numeric() => mean_list_numerical(ca, dt),
197 _ => sum_mean::mean_with_nulls(ca),
198 }
199 }
200
201 fn lst_median(&self) -> Series {
202 let ca = self.as_list();
203 dispersion::median_with_nulls(ca)
204 }
205
206 fn lst_std(&self, ddof: u8) -> Series {
207 let ca = self.as_list();
208 dispersion::std_with_nulls(ca, ddof)
209 }
210
211 fn lst_var(&self, ddof: u8) -> PolarsResult<Series> {
212 let ca = self.as_list();
213 dispersion::var_with_nulls(ca, ddof)
214 }
215
216 fn same_type(&self, out: ListChunked) -> ListChunked {
217 let ca = self.as_list();
218 let dtype = ca.dtype();
219 if out.dtype() != dtype {
220 out.cast(ca.dtype()).unwrap().list().unwrap().clone()
221 } else {
222 out
223 }
224 }
225
226 fn lst_sort(&self, options: SortOptions) -> PolarsResult<ListChunked> {
227 let ca = self.as_list();
228 let out = unsafe { ca.try_apply_amortized_same_type(|s| s.as_ref().sort_with(options))? };
230 Ok(self.same_type(out))
231 }
232
233 fn lst_arg_min(&self) -> IdxCa {
234 let ca = self.as_list();
235 ca.apply_amortized_generic(|opt_s| {
236 opt_s.and_then(|s| s.as_ref().arg_min().map(|idx| idx as IdxSize))
237 })
238 }
239
240 fn lst_arg_max(&self) -> IdxCa {
241 let ca = self.as_list();
242 ca.apply_amortized_generic(|opt_s| {
243 opt_s.and_then(|s| s.as_ref().arg_max().map(|idx| idx as IdxSize))
244 })
245 }
246
247 #[cfg(feature = "diff")]
248 fn lst_diff(&self, n: i64, null_behavior: NullBehavior) -> PolarsResult<ListChunked> {
249 let ca = self.as_list();
250 ca.try_apply_amortized(|s| diff(s.as_ref(), n, null_behavior))
251 }
252
253 fn lst_shift(&self, periods: &Column) -> PolarsResult<ListChunked> {
254 let ca = self.as_list();
255 let periods_s = periods.cast(&DataType::Int64)?;
256 let periods = periods_s.i64()?;
257
258 polars_ensure!(
259 ca.len() == periods.len() || ca.len() == 1 || periods.len() == 1,
260 length_mismatch = "list.shift",
261 ca.len(),
262 periods.len()
263 );
264
265 let target_len = periods.len();
266 if ca.len() == 1 && target_len > 1 {
267 let single_list = ca.get_as_series(0);
268 let out = shift_broadcast_list(
269 single_list,
270 periods,
271 target_len,
272 ca.name().clone(),
273 ca.inner_dtype(),
274 );
275 return Ok(self.same_type(out));
276 }
277
278 let out = match periods.len() {
279 1 => {
280 if let Some(periods) = periods.get(0) {
281 unsafe { ca.apply_amortized_same_type(|s| s.as_ref().shift(periods)) }
283 } else {
284 ListChunked::full_null_with_dtype(ca.name().clone(), ca.len(), ca.inner_dtype())
285 }
286 },
287 _ => ca.zip_and_apply_amortized(periods, |opt_s, opt_periods| {
288 match (opt_s, opt_periods) {
289 (Some(s), Some(periods)) => Some(s.as_ref().shift(periods)),
290 _ => None,
291 }
292 }),
293 };
294 Ok(self.same_type(out))
295 }
296
297 fn lst_lengths(&self) -> IdxCa {
298 let ca = self.as_list();
299
300 let ca_validity = ca.rechunk_validity();
301
302 if ca_validity.as_ref().is_some_and(|x| x.set_bits() == 0) {
303 return IdxCa::full_null(ca.name().clone(), ca.len());
304 }
305
306 let mut lengths = Vec::with_capacity(ca.len());
307 ca.downcast_iter().for_each(|arr| {
308 let offsets = arr.offsets().as_slice();
309 let mut last = offsets[0];
310 for o in &offsets[1..] {
311 lengths.push((*o - last) as IdxSize);
312 last = *o;
313 }
314 });
315
316 let arr = IdxArr::from_vec(lengths).with_validity(ca_validity);
317 IdxCa::with_chunk(ca.name().clone(), arr)
318 }
319
320 fn lst_get(&self, idx: i64, null_on_oob: bool) -> PolarsResult<Series> {
325 let ca = self.as_list();
326 if !null_on_oob && ca.downcast_iter().any(|arr| index_is_oob(arr, idx)) {
327 polars_bail!(ComputeError: "get index is out of bounds");
328 }
329
330 let chunks = ca
331 .downcast_iter()
332 .map(|arr| sublist_get(arr, idx))
333 .collect::<Vec<_>>();
334
335 let s = Series::try_from((ca.name().clone(), chunks)).unwrap();
336 unsafe { s.from_physical_unchecked(ca.inner_dtype()) }
338 }
339
340 #[cfg(feature = "list_gather")]
341 fn lst_gather_every(&self, n: &IdxCa, offset: &IdxCa) -> PolarsResult<Series> {
342 let list_ca = self.as_list();
343 let out = match (n.len(), offset.len()) {
344 (1, 1) => match (n.get(0), offset.get(0)) {
345 (Some(n), Some(offset)) => unsafe {
346 list_ca.try_apply_amortized_same_type(|s| {
348 s.as_ref().gather_every(n as usize, offset as usize)
349 })?
350 },
351 _ => ListChunked::full_null_with_dtype(
352 list_ca.name().clone(),
353 list_ca.len(),
354 list_ca.inner_dtype(),
355 ),
356 },
357 (1, len_offset) if len_offset == list_ca.len() => {
358 if let Some(n) = n.get(0) {
359 list_ca.try_zip_and_apply_amortized(offset, |opt_s, opt_offset| {
360 match (opt_s, opt_offset) {
361 (Some(s), Some(offset)) => {
362 Ok(Some(s.as_ref().gather_every(n as usize, offset as usize)?))
363 },
364 _ => Ok(None),
365 }
366 })?
367 } else {
368 ListChunked::full_null_with_dtype(
369 list_ca.name().clone(),
370 list_ca.len(),
371 list_ca.inner_dtype(),
372 )
373 }
374 },
375 (len_n, 1) if len_n == list_ca.len() => {
376 if let Some(offset) = offset.get(0) {
377 list_ca.try_zip_and_apply_amortized(n, |opt_s, opt_n| match (opt_s, opt_n) {
378 (Some(s), Some(n)) => {
379 Ok(Some(s.as_ref().gather_every(n as usize, offset as usize)?))
380 },
381 _ => Ok(None),
382 })?
383 } else {
384 ListChunked::full_null_with_dtype(
385 list_ca.name().clone(),
386 list_ca.len(),
387 list_ca.inner_dtype(),
388 )
389 }
390 },
391 (len_n, len_offset) if len_n == len_offset && len_n == list_ca.len() => list_ca
392 .try_binary_zip_and_apply_amortized(
393 n,
394 offset,
395 |opt_s, opt_n, opt_offset| match (opt_s, opt_n, opt_offset) {
396 (Some(s), Some(n), Some(offset)) => {
397 Ok(Some(s.as_ref().gather_every(n as usize, offset as usize)?))
398 },
399 _ => Ok(None),
400 },
401 )?,
402 _ => {
403 polars_bail!(
404 ComputeError: "The lengths of `n` and `offset` should be 1 or equal to the length of list (got `n`: {}, `offset`: {}, list: {}).",
405 n.len(), offset.len(), list_ca.len()
406 )
407 },
408 };
409 Ok(out.into_series())
410 }
411
412 #[cfg(feature = "list_drop_nulls")]
413 fn lst_drop_nulls(&self) -> ListChunked {
414 let list_ca = self.as_list();
415
416 unsafe { list_ca.apply_amortized_same_type(|s| s.as_ref().drop_nulls()) }
418 }
419
420 #[cfg(feature = "list_sample")]
421 fn lst_sample_n(
422 &self,
423 n: &Series,
424 with_replacement: bool,
425 shuffle: Option<bool>,
426 seed: Option<u64>,
427 ) -> PolarsResult<ListChunked> {
428 let ca = self.as_list();
429
430 let n_s = n.strict_cast(&IDX_DTYPE)?;
431 let n = n_s.idx()?;
432
433 polars_ensure!(
434 ca.len() == n.len() || ca.len() == 1 || n.len() == 1,
435 length_mismatch = "list.sample(n)",
436 ca.len(),
437 n.len()
438 );
439
440 let target_len = n.len();
441 if ca.len() == 1 && target_len > 1 {
442 let single_list = ca.get_as_series(0);
443 let out = sample_n_broadcast_list(
444 single_list,
445 n,
446 with_replacement,
447 shuffle,
448 seed,
449 target_len,
450 ca.name().clone(),
451 ca.inner_dtype(),
452 )?;
453 return Ok(self.same_type(out));
454 }
455
456 let out = match n.len() {
457 1 => {
458 if let Some(n) = n.get(0) {
459 unsafe {
460 ca.try_apply_amortized_same_type(|s| {
462 s.as_ref()
463 .sample_n(n as usize, with_replacement, shuffle, seed)
464 })
465 }
466 } else {
467 Ok(ListChunked::full_null_with_dtype(
468 ca.name().clone(),
469 ca.len(),
470 ca.inner_dtype(),
471 ))
472 }
473 },
474 _ => ca.try_zip_and_apply_amortized(n, |opt_s, opt_n| match (opt_s, opt_n) {
475 (Some(s), Some(n)) => s
476 .as_ref()
477 .sample_n(n as usize, with_replacement, shuffle, seed)
478 .map(Some),
479 _ => Ok(None),
480 }),
481 };
482 out.map(|ok| self.same_type(ok))
483 }
484
485 #[cfg(feature = "list_sample")]
486 fn lst_sample_fraction(
487 &self,
488 fraction: &Series,
489 with_replacement: bool,
490 shuffle: Option<bool>,
491 seed: Option<u64>,
492 ) -> PolarsResult<ListChunked> {
493 let ca = self.as_list();
494
495 let fraction_s = fraction.cast(&DataType::Float64)?;
496 let fraction = fraction_s.f64()?;
497
498 if !with_replacement {
499 for frac in fraction.iter().flatten() {
500 polars_ensure!(
501 (0.0..=1.0).contains(&frac),
502 ComputeError: "fraction must be between 0.0 and 1.0, got: {}", frac
503 )
504 }
505 }
506
507 polars_ensure!(
508 ca.len() == fraction.len() || ca.len() == 1 || fraction.len() == 1,
509 length_mismatch = "list.sample(fraction)",
510 ca.len(),
511 fraction.len()
512 );
513
514 let target_len = fraction.len();
515 if ca.len() == 1 && target_len > 1 {
516 let single_list = ca.get_as_series(0);
517 let out = sample_frac_broadcast_list(
518 single_list,
519 fraction,
520 with_replacement,
521 shuffle,
522 seed,
523 target_len,
524 ca.name().clone(),
525 ca.inner_dtype(),
526 )?;
527 return Ok(self.same_type(out));
528 }
529
530 let out = match fraction.len() {
531 1 => {
532 if let Some(fraction) = fraction.get(0) {
533 unsafe {
534 ca.try_apply_amortized_same_type(|s| {
536 let n = (s.as_ref().len() as f64 * fraction) as usize;
537 s.as_ref().sample_n(n, with_replacement, shuffle, seed)
538 })
539 }
540 } else {
541 Ok(ListChunked::full_null_with_dtype(
542 ca.name().clone(),
543 ca.len(),
544 ca.inner_dtype(),
545 ))
546 }
547 },
548 _ => ca.try_zip_and_apply_amortized(fraction, |opt_s, opt_n| match (opt_s, opt_n) {
549 (Some(s), Some(fraction)) => {
550 let n = (s.as_ref().len() as f64 * fraction) as usize;
551 s.as_ref()
552 .sample_n(n, with_replacement, shuffle, seed)
553 .map(Some)
554 },
555 _ => Ok(None),
556 }),
557 };
558 out.map(|ok| self.same_type(ok))
559 }
560
561 fn lst_concat(&self, other: &[Column]) -> PolarsResult<ListChunked> {
562 let ca = self.as_list();
563 if other.is_empty() {
564 return Ok(ca.clone());
565 }
566
567 let other_len = other.len();
568 let length = ca.len();
569 let mut other = other.to_vec();
570 let mut inner_super_type = ca.inner_dtype().clone();
571
572 for s in &other {
573 match s.dtype() {
574 DataType::List(inner_type) => {
575 inner_super_type = try_get_supertype(&inner_super_type, inner_type)?;
576 },
577 dt => {
578 inner_super_type = try_get_supertype(&inner_super_type, dt)?;
579 },
580 }
581 }
582
583 let dtype = &DataType::List(Box::new(inner_super_type.clone()));
585 let ca = ca.cast(dtype)?;
586 let ca = ca.list().unwrap();
587
588 let out = if other.iter().all(|s| s.len() == 1) && ca.len() != 1 {
591 cast_rhs(&mut other, &inner_super_type, dtype, length, false)?;
592 let to_append = other
593 .iter()
594 .filter_map(|s| {
595 let lst = s.list().unwrap();
596 unsafe {
598 lst.get_as_series(0)
599 .map(|s| s.from_physical_unchecked(&inner_super_type).unwrap())
600 }
601 })
602 .collect::<Vec<_>>();
603
604 if to_append.len() != other_len {
606 return Ok(ListChunked::full_null_with_dtype(
607 ca.name().clone(),
608 length,
609 &inner_super_type,
610 ));
611 }
612
613 let vals_size_other = other
614 .iter()
615 .map(|s| s.list().unwrap().get_values_size())
616 .sum::<usize>();
617
618 let mut builder = get_list_builder(
619 &inner_super_type,
620 ca.get_values_size() + vals_size_other + 1,
621 length,
622 ca.name().clone(),
623 );
624 ca.series_iter().for_each(|opt_s| {
625 let opt_s = opt_s.map(|mut s| {
626 for append in &to_append {
627 s.append(append).unwrap();
628 }
629 match inner_super_type {
630 #[cfg(feature = "dtype-struct")]
632 DataType::Struct(_) => s = s.rechunk(),
633 _ => {},
635 }
636 s
637 });
638 builder.append_opt_series(opt_s.as_ref()).unwrap();
639 });
640 builder.finish()
641 } else {
642 cast_rhs(&mut other, &inner_super_type, dtype, length, true)?;
644
645 let vals_size_other = other
646 .iter()
647 .map(|s| s.list().unwrap().get_values_size())
648 .sum::<usize>();
649 let mut iters = Vec::with_capacity(other_len + 1);
650
651 for s in other.iter_mut() {
652 iters.push(s.list()?.amortized_iter())
653 }
654 let mut first_iter = ca.series_iter();
655 let mut builder = get_list_builder(
656 &inner_super_type,
657 ca.get_values_size() + vals_size_other + 1,
658 length,
659 ca.name().clone(),
660 );
661
662 for _ in 0..ca.len() {
663 let mut acc = match first_iter.next().unwrap() {
664 Some(s) => s,
665 None => {
666 builder.append_null();
667 for it in &mut iters {
669 it.next().unwrap();
670 }
671 continue;
672 },
673 };
674
675 let mut has_nulls = false;
676 for it in &mut iters {
677 match it.next().unwrap() {
678 Some(s) => {
679 if !has_nulls {
680 acc.append(s.as_ref())?;
681 }
682 },
683 None => {
684 has_nulls = true;
685 },
686 }
687 }
688 if has_nulls {
689 builder.append_null();
690 continue;
691 }
692
693 match inner_super_type {
694 #[cfg(feature = "dtype-struct")]
696 DataType::Struct(_) => acc = acc.rechunk(),
697 _ => {},
699 }
700 builder.append_series(&acc).unwrap();
701 }
702 builder.finish()
703 };
704 Ok(out)
705 }
706}
707
708impl ListNameSpaceImpl for ListChunked {}
709
710pub fn slice_broadcast_list(
711 single_list: Option<Series>,
712 offsets: &Int64Chunked,
713 lengths: &Int64Chunked,
714 target_len: usize,
715 name: PlSmallStr,
716 inner_dtype: &DataType,
717) -> ListChunked {
718 debug_assert!(target_len == offsets.len().max(lengths.len()));
719
720 let Some(single_list) = single_list else {
721 return ListChunked::full_null_with_dtype(name, target_len, inner_dtype);
722 };
723
724 let iter = (0..target_len).map(|index| {
725 let opt_offset = offsets.get(if offsets.len() == 1 { 0 } else { index });
726 let opt_length = lengths.get(if lengths.len() == 1 { 0 } else { index });
727 let offset = opt_offset?;
728 let length = opt_length.map_or(usize::MAX, |l| l as usize);
729 Some(single_list.slice(offset, length))
730 });
731
732 let mut out: ListChunked = iter.collect_trusted();
733 out.rename(name);
734 out
735}
736
737fn shift_broadcast_list(
738 single_list: Option<Series>,
739 periods: &Int64Chunked,
740 target_len: usize,
741 name: PlSmallStr,
742 inner_dtype: &DataType,
743) -> ListChunked {
744 debug_assert!(target_len == periods.len());
745
746 let Some(single_list) = single_list else {
747 return ListChunked::full_null_with_dtype(name, target_len, inner_dtype);
748 };
749
750 let iter = (0..target_len).map(|index| {
751 let opt_period = periods.get(index);
752 opt_period.map(|period| single_list.shift(period))
753 });
754
755 let mut out: ListChunked = iter.collect_trusted();
756 out.rename(name);
757 out
758}
759
760#[cfg(feature = "list_sample")]
761#[allow(clippy::too_many_arguments)]
762fn sample_n_broadcast_list(
763 single_list: Option<Series>,
764 n: &IdxCa,
765 with_replacement: bool,
766 shuffle: Option<bool>,
767 seed: Option<u64>,
768 target_len: usize,
769 name: PlSmallStr,
770 inner_dtype: &DataType,
771) -> PolarsResult<ListChunked> {
772 debug_assert!(target_len == n.len());
773
774 let Some(single_list) = single_list else {
775 return Ok(ListChunked::full_null_with_dtype(
776 name,
777 target_len,
778 inner_dtype,
779 ));
780 };
781
782 let mut out: ListChunked = (0..target_len)
783 .map(|index| -> PolarsResult<Option<Series>> {
784 match n.get(index) {
785 Some(n_val) => single_list
786 .sample_n(n_val as usize, with_replacement, shuffle, seed)
787 .map(Some),
788 None => Ok(None),
789 }
790 })
791 .collect::<PolarsResult<_>>()?;
792
793 out.rename(name);
794 Ok(out)
795}
796
797#[cfg(feature = "list_sample")]
798#[allow(clippy::too_many_arguments)]
799fn sample_frac_broadcast_list(
800 single_list: Option<Series>,
801 fraction: &Float64Chunked,
802 with_replacement: bool,
803 shuffle: Option<bool>,
804 seed: Option<u64>,
805 target_len: usize,
806 name: PlSmallStr,
807 inner_dtype: &DataType,
808) -> PolarsResult<ListChunked> {
809 debug_assert!(target_len == fraction.len());
810
811 let Some(single_list) = single_list else {
812 return Ok(ListChunked::full_null_with_dtype(
813 name,
814 target_len,
815 inner_dtype,
816 ));
817 };
818
819 let mut out: ListChunked = (0..target_len)
820 .map(|index| -> PolarsResult<Option<Series>> {
821 match fraction.get(index) {
822 Some(frac_val) => single_list
823 .sample_frac(frac_val, with_replacement, shuffle, seed)
824 .map(Some),
825 None => Ok(None),
826 }
827 })
828 .collect::<PolarsResult<_>>()?;
829
830 out.rename(name);
831 Ok(out)
832}