polars_core/chunked_array/logical/
categorical.rs1use std::hash::BuildHasher;
2use std::marker::PhantomData;
3
4use num_traits::Zero;
5use polars_arrow::array::DictionaryKey;
6use polars_arrow::bitmap::{Bitmap, BitmapBuilder, MutableBitmap};
7use polars_compute::cast::cast_unchecked;
8use polars_utils::hashing::{_boost_hash_combine, folded_multiply};
9
10use crate::chunked_array::cast::CastOptions;
11use crate::chunked_array::flags::StatisticsFlags;
12use crate::chunked_array::iterator::PolarsIterator;
13use crate::chunked_array::ops::ChunkFullNull;
14use crate::hashing::get_null_hash_value;
15use crate::prelude::*;
16use crate::series::IsSorted;
17use crate::utils::handle_casting_failures;
18
19pub type CategoricalChunked<T> = Logical<T, <T as PolarsCategoricalType>::PolarsPhysical>;
20pub type Categorical8Chunked = CategoricalChunked<Categorical8Type>;
21pub type Categorical16Chunked = CategoricalChunked<Categorical16Type>;
22pub type Categorical32Chunked = CategoricalChunked<Categorical32Type>;
23
24pub trait CategoricalPhysicalDtypeExt {
25 fn dtype(&self) -> DataType;
26}
27
28impl CategoricalPhysicalDtypeExt for CategoricalPhysical {
29 fn dtype(&self) -> DataType {
30 match self {
31 Self::U8 => DataType::UInt8,
32 Self::U16 => DataType::UInt16,
33 Self::U32 => DataType::UInt32,
34 }
35 }
36}
37
38impl<T: PolarsCategoricalType> CategoricalChunked<T> {
39 pub fn is_enum(&self) -> bool {
40 matches!(self.dtype(), DataType::Enum(_, _))
41 }
42
43 pub(crate) fn get_flags(&self) -> StatisticsFlags {
44 let mut flags = self.phys.get_flags();
47 if self.uses_lexical_ordering() {
48 flags.set_sorted(IsSorted::Not);
49 }
50 flags
51 }
52
53 pub(crate) fn set_flags(&mut self, mut flags: StatisticsFlags) {
55 if self.uses_lexical_ordering() {
57 flags.set_sorted(IsSorted::Not)
58 }
59 self.physical_mut().set_flags(flags)
60 }
61
62 pub fn uses_lexical_ordering(&self) -> bool {
65 !self.is_enum()
66 }
67
68 pub fn full_null_with_dtype(name: PlSmallStr, length: usize, dtype: DataType) -> Self {
69 let phys =
70 ChunkedArray::<<T as PolarsCategoricalType>::PolarsPhysical>::full_null(name, length);
71 unsafe { Self::from_cats_and_dtype_unchecked(phys, dtype) }
72 }
73
74 pub fn from_cats_and_dtype(
78 mut cat_ids: ChunkedArray<T::PolarsPhysical>,
79 dtype: DataType,
80 ) -> Self {
81 let (DataType::Enum(_, mapping) | DataType::Categorical(_, mapping)) = &dtype else {
82 panic!("from_cats_and_dtype called on non-categorical type")
83 };
84 assert!(dtype.cat_physical().ok() == Some(T::physical()));
85
86 unsafe {
87 let mut invariants_violated = false;
88 let mut validity = BitmapBuilder::new();
89 for arr in cat_ids.downcast_iter_mut() {
90 validity.reserve(arr.len());
91 if arr.has_nulls() {
92 for opt_cat_id in arr.iter() {
93 if let Some(cat_id) = opt_cat_id {
94 validity.push_unchecked(mapping.cat_to_str(cat_id.as_cat()).is_some());
95 } else {
96 validity.push_unchecked(false);
97 }
98 }
99 } else {
100 for cat_id in arr.values_iter() {
101 validity.push_unchecked(mapping.cat_to_str(cat_id.as_cat()).is_some());
102 }
103 }
104
105 if arr.null_count() != validity.unset_bits() {
106 invariants_violated = true;
107 arr.set_validity(core::mem::take(&mut validity).into_opt_validity());
108 } else {
109 validity.clear();
110 }
111 }
112
113 if invariants_violated {
114 cat_ids.set_flags(StatisticsFlags::empty());
115 cat_ids.compute_len();
116 }
117 }
118
119 Self {
120 phys: cat_ids,
121 dtype,
122 _phantom: PhantomData,
123 }
124 }
125
126 pub unsafe fn from_cats_and_dtype_unchecked(
131 cat_ids: ChunkedArray<T::PolarsPhysical>,
132 dtype: DataType,
133 ) -> Self {
134 debug_assert!(dtype.cat_physical().ok() == Some(T::physical()));
135
136 Self {
137 phys: cat_ids,
138 dtype,
139 _phantom: PhantomData,
140 }
141 }
142
143 pub fn get_mapping(&self) -> &Arc<CategoricalMapping> {
145 let (DataType::Categorical(_, mapping) | DataType::Enum(_, mapping)) = self.dtype() else {
146 unreachable!()
147 };
148 mapping
149 }
150
151 pub fn iter_str(&self) -> impl PolarsIterator<Item = Option<&str>> {
153 let mapping = self.get_mapping();
154 self.phys
155 .iter()
156 .map(|cat| unsafe { Some(mapping.cat_to_str_unchecked(cat?.as_cat())) })
157 }
158
159 pub fn from_str_iter<'a, I: IntoIterator<Item = Option<&'a str>>>(
163 name: PlSmallStr,
164 dtype: DataType,
165 strings: I,
166 ) -> PolarsResult<Self> {
167 let strings = strings.into_iter();
168
169 let hint = strings.size_hint().0;
170 let mut cat_ids = Vec::with_capacity(hint);
171 let mut validity = BitmapBuilder::with_capacity(hint);
172
173 match &dtype {
174 DataType::Categorical(cats, mapping) => {
175 assert!(cats.physical() == T::physical());
176 for opt_s in strings {
177 cat_ids.push(if let Some(s) = opt_s {
178 T::Native::from_cat(mapping.insert_cat(s)?)
179 } else {
180 T::Native::zero()
181 });
182 validity.push(opt_s.is_some());
183 }
184 },
185 DataType::Enum(fcats, mapping) => {
186 assert!(fcats.physical() == T::physical());
187 for opt_s in strings {
188 cat_ids.push(if let Some(cat) = opt_s.and_then(|s| mapping.get_cat(s)) {
189 validity.push(true);
190 T::Native::from_cat(cat)
191 } else {
192 validity.push(false);
193 T::Native::zero()
194 });
195 }
196 },
197 _ => panic!("from_strings_and_dtype_strict called on non-categorical type"),
198 }
199
200 let arr = <T::PolarsPhysical as PolarsDataType>::Array::from_vec(cat_ids)
201 .with_validity(validity.into_opt_validity());
202 let phys = ChunkedArray::<T::PolarsPhysical>::with_chunk(name, arr);
203 Ok(unsafe { Self::from_cats_and_dtype_unchecked(phys, dtype) })
204 }
205
206 pub fn from_str_dictionary<K: DictionaryKey>(
211 name: PlSmallStr,
212 dtype: DataType,
213 arr: &DictionaryArray<K>,
214 ) -> PolarsResult<Self> {
215 let (DataType::Enum(_, mapping) | DataType::Categorical(_, mapping)) = &dtype else {
216 panic!("from_str_dictionary called on non-categorical type")
217 };
218 assert!(dtype.cat_physical().ok() == Some(T::physical()));
219 let is_enum = dtype.is_enum();
220
221 let values = cast_unchecked(&**arr.values(), &ArrowDataType::Utf8View)?;
222 let values = values.as_any().downcast_ref::<Utf8ViewArray>().unwrap();
223 let keys = arr.keys();
224
225 let per_row = || {
227 let strings = keys.iter().map(|k| values.get((*k?).try_into().ok()?));
228 Self::from_str_iter(name.clone(), dtype.clone(), strings)
229 };
230
231 if values.len() > keys.len() {
233 return per_row();
234 }
235
236 let mut seen = MutableBitmap::from_len_zeroed(values.len());
239 let mut value_cats = vec![T::Native::zero(); values.len()];
240 let mut value_is_valid: Option<MutableBitmap> = None;
242 let mut lookup = |k: usize| -> PolarsResult<()> {
243 let opt_cat = match values.get(k) {
244 Some(s) if is_enum => mapping.get_cat(s).map(T::Native::from_cat),
245 Some(s) => Some(T::Native::from_cat(mapping.insert_cat(s)?)),
246 None => None,
247 };
248 match opt_cat {
249 Some(cat) => value_cats[k] = cat,
250 None => {
251 value_is_valid
252 .get_or_insert_with(|| MutableBitmap::from_len_set(values.len()))
253 .set(k, false);
254 },
255 }
256 Ok(())
257 };
258 for k in keys.iter().flatten() {
259 let Ok(k) = (*k).try_into() else {
260 return per_row();
261 };
262 if k >= values.len() {
263 return per_row();
264 }
265 if !unsafe { seen.get_unchecked(k) } {
267 unsafe { seen.set_unchecked(k, true) };
268 lookup(k)?;
269 }
270 }
271
272 let cat_ids: Vec<T::Native> = if keys.null_count() == 0 {
274 keys.values()
275 .iter()
276 .map(|k| unsafe { *value_cats.get_unchecked(k.as_usize()) })
277 .collect_trusted()
278 } else {
279 arr.keys_iter()
280 .map(|k| {
281 k.map_or(T::Native::zero(), |k| unsafe {
282 *value_cats.get_unchecked(k)
283 })
284 })
285 .collect_trusted()
286 };
287 let validity = if let Some(value_is_valid) = value_is_valid {
288 let validity: Bitmap = arr
289 .keys_iter()
290 .map(|k| k.is_some_and(|k| unsafe { value_is_valid.get_unchecked(k) }))
291 .collect_trusted();
292 Some(validity)
293 } else {
294 keys.validity().cloned()
295 };
296
297 let arr =
298 <T::PolarsPhysical as PolarsDataType>::Array::from_vec(cat_ids).with_validity(validity);
299 let phys = ChunkedArray::<T::PolarsPhysical>::with_chunk(name, arr);
300 Ok(unsafe { Self::from_cats_and_dtype_unchecked(phys, dtype) })
301 }
302
303 pub fn to_arrow(&self, compat_level: CompatLevel) -> DictionaryArray<T::Native> {
304 let keys = self.physical().rechunk();
305 let keys = keys.downcast_as_array();
306 let values = self
307 .get_mapping()
308 .to_arrow(compat_level.uses_binview_types());
309 let values_dtype = Box::new(values.dtype().clone());
310 let dtype = ArrowDataType::Dictionary(
311 <T::Native as DictionaryKey>::KEY_TYPE,
312 values_dtype,
313 self.is_enum(),
314 );
315 unsafe { DictionaryArray::try_new_unchecked(dtype, keys.clone(), values).unwrap() }
316 }
317}
318
319impl<T: PolarsCategoricalType> LogicalType for CategoricalChunked<T> {
320 fn dtype(&self) -> &DataType {
321 &self.dtype
322 }
323
324 fn get_any_value(&self, i: usize) -> PolarsResult<AnyValue<'_>> {
325 polars_ensure!(i < self.len(), oob = i, self.len());
326 Ok(unsafe { self.get_any_value_unchecked(i) })
327 }
328
329 unsafe fn get_any_value_unchecked(&self, i: usize) -> AnyValue<'_> {
330 match self.phys.get_unchecked(i) {
331 Some(i) => match &self.dtype {
332 DataType::Enum(_, mapping) => AnyValue::Enum(i.as_cat(), mapping),
333 DataType::Categorical(_, mapping) => AnyValue::Categorical(i.as_cat(), mapping),
334 _ => unreachable!(),
335 },
336 None => AnyValue::Null,
337 }
338 }
339
340 fn cast_with_options(&self, dtype: &DataType, options: CastOptions) -> PolarsResult<Series> {
341 if &self.dtype == dtype {
342 return Ok(self.clone().into_series());
343 }
344
345 match dtype {
346 DataType::String => {
347 let mapping = self.get_mapping();
348
349 let mut builder = StringChunkedBuilder::new(self.phys.name().clone(), self.len());
352 let to_str = |cat_id: CatSize| unsafe { mapping.cat_to_str_unchecked(cat_id) };
353 if !self.phys.has_nulls() {
354 for cat_id in self.phys.into_no_null_iter() {
355 builder.append_value(to_str(cat_id.as_cat()));
356 }
357 } else {
358 for opt_cat_id in self.phys.iter() {
359 let opt_cat_id: Option<_> = opt_cat_id;
360 builder.append_option(opt_cat_id.map(|c| to_str(c.as_cat())));
361 }
362 }
363
364 let ca = builder.finish();
365 Ok(ca.into_series())
366 },
367
368 DataType::Enum(fcats, _mapping) => {
369 let ret = with_match_categorical_physical_type!(fcats.physical(), |$C| {
371 CategoricalChunked::<$C>::from_str_iter(
372 self.name().clone(),
373 dtype.clone(),
374 self.iter_str()
375 )?.into_series()
376 });
377
378 if options.is_strict() && self.null_count() != ret.null_count() {
379 handle_casting_failures(&self.clone().into_series(), &ret)?;
380 }
381
382 Ok(ret)
383 },
384
385 DataType::Categorical(cats, _mapping) => {
386 Ok(
388 with_match_categorical_physical_type!(cats.physical(), |$C| {
389 CategoricalChunked::<$C>::from_str_iter(
390 self.name().clone(),
391 dtype.clone(),
392 self.iter_str()
393 )?.into_series()
394 }),
395 )
396 },
397
398 dt if dt.is_integer() => {
399 polars_bail!(
400 ComputeError: "cannot cast categorical types to {dtype:?}.\n\
401 Instead of `.cast({dtype:?})`, use `.cat.physical()`.",
402 );
403 },
404
405 _ => polars_bail!(ComputeError: "cannot cast categorical types to {dtype:?}"),
406 }
407 }
408}
409
410impl<T: PolarsCategoricalType> VecHash for CategoricalChunked<T>
411where
412 ChunkedArray<<T as PolarsCategoricalType>::PolarsPhysical>: VecHash,
413{
414 fn vec_hash(
415 &self,
416 random_state: PlSeedableRandomStateQuality,
417 buf: &mut Vec<u64>,
418 ) -> PolarsResult<()> {
419 if self.is_enum() {
420 self.phys.vec_hash(random_state, buf)
421 } else {
422 buf.clear();
423 buf.reserve(self.phys.len());
424 let mult = random_state.hash_one(0);
425 let null = get_null_hash_value(&random_state);
426
427 let mapping = self.get_mapping();
428 for opt_cat in self.phys.iter() {
429 if let Some(cat) = opt_cat {
430 let base_h = unsafe { mapping.cat_to_hash_unchecked(cat.as_cat()) };
431 buf.push(folded_multiply(base_h, mult));
432 } else {
433 buf.push(null);
434 }
435 }
436 Ok(())
437 }
438 }
439
440 fn vec_hash_combine(
441 &self,
442 random_state: PlSeedableRandomStateQuality,
443 hashes: &mut [u64],
444 ) -> PolarsResult<()> {
445 if self.is_enum() {
446 self.phys.vec_hash_combine(random_state, hashes)
447 } else {
448 let mult = random_state.hash_one(0);
449 let null = get_null_hash_value(&random_state);
450
451 let mapping = self.get_mapping();
452 assert!(self.phys.len() == hashes.len());
453 for (opt_cat, h) in self.phys.iter().zip(hashes.iter_mut()) {
454 let our_h = if let Some(cat) = opt_cat {
455 let base_h = unsafe { mapping.cat_to_hash_unchecked(cat.as_cat()) };
456 folded_multiply(base_h, mult)
457 } else {
458 null
459 };
460 *h = _boost_hash_combine(our_h, *h);
461 }
462 Ok(())
463 }
464 }
465}