pandrs 0.4.1

A high-performance DataFrame library for Rust, providing pandas-like API with advanced features including SIMD optimization, parallel processing, and distributed computing capabilities
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
//! Module providing DataFrame conversion functionality
//!
//! # NULL-preservation policy
//!
//! `crate::dataframe::DataFrame` stores every column as a plain, non-nullable
//! `crate::series::Series<T>` — there is no `Option<T>`/bitmask at that layer.
//! `crate::optimized` columns (`Int64Column`, `Float64Column`, `BooleanColumn`,
//! `StringColumn`), on the other hand, carry an explicit null bitmask via
//! `with_nulls`. Converting between the two therefore needs an explicit,
//! documented convention for representing "no value" on whichever side cannot
//! natively express it. Both directions below are chosen to be the
//! least-lossy option the `Series<T>` type system allows, and never silently
//! substitute a numeric/string default (0 / "" / false) for a value that was
//! never actually 0 / "" / false in the source.
//!
//! ## `from_standard_dataframe` (standard -> optimized)
//!
//! * `Series<String>` — the historical behavior of inferring a native type
//!   from string content is preserved (matching how this crate's CSV reader
//!   treats freshly-parsed string fields): if every *non-empty* value in the
//!   column parses as `i64` / `f64` / a recognized boolean word
//!   (`true`/`false`/`1`/`0`), the column is built as that native type. In
//!   every case an empty string (`""`) is treated as NULL rather than being
//!   coerced into `0` / `0.0` / `false` (the historical bug this module
//!   fixes) -- and a column that is *entirely* empty strings is never
//!   misclassified as numeric just because `""` vacuously "parses" as
//!   anything; it stays a fully-NULL `Series<String>`.
//! * `Series<f32>` / `Series<f64>` — `NaN` is treated as NULL (the standard
//!   floating-point "no value" sentinel used everywhere else in this crate).
//! * `Series<bool>` and every integer `Series<iN>`/`Series<uN>` — these types
//!   have no in-band sentinel for "missing", so they convert 1:1 with no
//!   NULLs. `u64` values that do not fit in `i64` (the only signed 64-bit
//!   integer column type this crate has) are rejected with an explicit
//!   error rather than silently wrapped/truncated.
//! * Any other element type is rejected with `Error::NotImplemented` naming
//!   the offending column, rather than the column being silently dropped
//!   (the historical bug this module fixes: a `DataFrame` made only of
//!   typed numeric/boolean columns used to convert to an OptimizedDataFrame
//!   with zero columns).
//!
//! ## `to_standard_dataframe` (optimized -> standard)
//!
//! * `Float64Column` NULLs become `f64::NAN` — lossless given the crate-wide
//!   NaN-as-NA convention above.
//! * `StringColumn` NULLs become `""` — matches the convention above.
//! * `BooleanColumn` with **any** NULL is promoted to `Series<String>`
//!   (`"true"` / `"false"` / `""`), because `Series<bool>` cannot represent
//!   NULL at all. A NULL-free `BooleanColumn` still converts to a native
//!   `Series<bool>`, unchanged.
//! * `Int64Column` with **any** NULL is promoted to `Series<f64>` with
//!   `f64::NAN` at the NULL positions — the same upcast pandas itself
//!   performs for an integer column containing NA. This keeps the result
//!   numeric (so downstream numeric consumers such as
//!   `DataFrame::get_column_numeric_values` keep working) at the cost of
//!   exact-integer precision above 2^53, which is the least-lossy option
//!   available; promoting to `Series<String>` instead was rejected because
//!   it would make the resulting Rust type depend on whether the data
//!   happens to contain a NULL, which downstream numeric code cannot
//!   possibly guard against. A NULL-free `Int64Column` still converts to a
//!   native `Series<i64>`, unchanged.

use crate::column::{BooleanColumn, Column, ColumnTrait, Float64Column, Int64Column, StringColumn};
use crate::error::{Error, Result};
use crate::index::DataFrameIndex;
use crate::optimized::dataframe::OptimizedDataFrame;
use crate::optimized::split_dataframe::core::OptimizedDataFrame as SplitDataFrame;

/// Build an `optimized::Column` from the named column of a standard
/// `DataFrame`, dispatching over every `Series<T>` element type this crate
/// commonly stores. See the module-level doc comment for the NULL policy.
fn build_column_from_series(df: &crate::dataframe::DataFrame, col_name: &str) -> Result<Column> {
    // Series<String>: infer a native type from the string content, exactly
    // as the CSV reader does (see module doc comment). "" is always NULL and
    // is excluded from the "does every value fit this type?" checks below,
    // so an all-blank column is never misclassified as numeric.
    if let Ok(col) = df.get_column::<String>(col_name) {
        let values: Vec<String> = col.values().to_vec();
        let non_empty_values: Vec<&String> = values.iter().filter(|s| !s.is_empty()).collect();

        if non_empty_values.is_empty() {
            let nulls = vec![true; values.len()];
            return Ok(Column::String(StringColumn::with_nulls(values, nulls)));
        }

        // Integer type
        let all_ints = non_empty_values.iter().all(|&s| s.parse::<i64>().is_ok());
        if all_ints {
            let mut int_values = Vec::with_capacity(values.len());
            let mut nulls = Vec::with_capacity(values.len());
            for s in &values {
                if s.is_empty() {
                    int_values.push(0);
                    nulls.push(true);
                } else {
                    let parsed = s.parse::<i64>().map_err(|e| {
                        Error::Cast(format!(
                            "Column '{}': failed to parse '{}' as i64: {}",
                            col_name, s, e
                        ))
                    })?;
                    int_values.push(parsed);
                    nulls.push(false);
                }
            }
            return Ok(Column::Int64(Int64Column::with_nulls(int_values, nulls)));
        }

        // Floating point type
        let all_floats = non_empty_values.iter().all(|&s| s.parse::<f64>().is_ok());
        if all_floats {
            let mut float_values = Vec::with_capacity(values.len());
            let mut nulls = Vec::with_capacity(values.len());
            for s in &values {
                if s.is_empty() {
                    float_values.push(0.0);
                    nulls.push(true);
                } else {
                    let parsed = s.parse::<f64>().map_err(|e| {
                        Error::Cast(format!(
                            "Column '{}': failed to parse '{}' as f64: {}",
                            col_name, s, e
                        ))
                    })?;
                    float_values.push(parsed);
                    nulls.push(false);
                }
            }
            return Ok(Column::Float64(Float64Column::with_nulls(
                float_values,
                nulls,
            )));
        }

        // Boolean type (case-insensitive true/false/1/0, matching the
        // original narrower word set this function has always used --
        // unlike the CSV reader's broader yes/no/t/f set).
        let all_bools = non_empty_values.iter().all(|&s| {
            let lower = s.to_lowercase();
            lower == "true" || lower == "false" || lower == "1" || lower == "0"
        });
        if all_bools {
            let mut bool_values = Vec::with_capacity(values.len());
            let mut nulls = Vec::with_capacity(values.len());
            for s in &values {
                if s.is_empty() {
                    bool_values.push(false);
                    nulls.push(true);
                } else {
                    let lower = s.to_lowercase();
                    bool_values.push(lower == "true" || lower == "1");
                    nulls.push(false);
                }
            }
            return Ok(Column::Boolean(BooleanColumn::with_nulls(
                bool_values,
                nulls,
            )));
        }

        // Default is string type; "" is still NULL.
        let nulls: Vec<bool> = values.iter().map(|s| s.is_empty()).collect();
        return Ok(Column::String(StringColumn::with_nulls(values, nulls)));
    }

    // Series<f64> / Series<f32>: NaN is treated as NULL.
    if let Ok(col) = df.get_column::<f64>(col_name) {
        let mut values = Vec::with_capacity(col.len());
        let mut nulls = Vec::with_capacity(col.len());
        for &v in col.values() {
            let is_null = v.is_nan();
            nulls.push(is_null);
            values.push(if is_null { 0.0 } else { v });
        }
        return Ok(Column::Float64(Float64Column::with_nulls(values, nulls)));
    }
    if let Ok(col) = df.get_column::<f32>(col_name) {
        let mut values = Vec::with_capacity(col.len());
        let mut nulls = Vec::with_capacity(col.len());
        for &v in col.values() {
            let is_null = v.is_nan();
            nulls.push(is_null);
            values.push(if is_null { 0.0 } else { v as f64 });
        }
        return Ok(Column::Float64(Float64Column::with_nulls(values, nulls)));
    }

    // bool: Series<bool> has no NULL sentinel, so this always converts 1:1.
    if let Ok(col) = df.get_column::<bool>(col_name) {
        return Ok(Column::Boolean(BooleanColumn::new(col.values().to_vec())));
    }

    // Signed/unsigned integers: no NULL sentinel, always convert 1:1 (widened
    // to i64, the only integer column type OptimizedDataFrame has).
    if let Ok(col) = df.get_column::<i64>(col_name) {
        return Ok(Column::Int64(Int64Column::new(col.values().to_vec())));
    }
    if let Ok(col) = df.get_column::<i32>(col_name) {
        let values: Vec<i64> = col.values().iter().map(|&v| v as i64).collect();
        return Ok(Column::Int64(Int64Column::new(values)));
    }
    if let Ok(col) = df.get_column::<i16>(col_name) {
        let values: Vec<i64> = col.values().iter().map(|&v| v as i64).collect();
        return Ok(Column::Int64(Int64Column::new(values)));
    }
    if let Ok(col) = df.get_column::<i8>(col_name) {
        let values: Vec<i64> = col.values().iter().map(|&v| v as i64).collect();
        return Ok(Column::Int64(Int64Column::new(values)));
    }
    if let Ok(col) = df.get_column::<u32>(col_name) {
        let values: Vec<i64> = col.values().iter().map(|&v| v as i64).collect();
        return Ok(Column::Int64(Int64Column::new(values)));
    }
    if let Ok(col) = df.get_column::<u16>(col_name) {
        let values: Vec<i64> = col.values().iter().map(|&v| v as i64).collect();
        return Ok(Column::Int64(Int64Column::new(values)));
    }
    if let Ok(col) = df.get_column::<u8>(col_name) {
        let values: Vec<i64> = col.values().iter().map(|&v| v as i64).collect();
        return Ok(Column::Int64(Int64Column::new(values)));
    }
    if let Ok(col) = df.get_column::<u64>(col_name) {
        let mut values = Vec::with_capacity(col.len());
        for &v in col.values() {
            let converted = i64::try_from(v).map_err(|_| {
                Error::Cast(format!(
                    "Column '{}' contains a u64 value {} that does not fit in i64; \
                     the OptimizedDataFrame bridge has no unsigned 64-bit column type",
                    col_name, v
                ))
            })?;
            values.push(converted);
        }
        return Ok(Column::Int64(Int64Column::new(values)));
    }

    Err(Error::NotImplemented(format!(
        "Column '{}' has an element type that the DataFrame -> OptimizedDataFrame bridge does \
         not support (supported element types: String, bool, 8/16/32/64-bit signed or \
         unsigned integers, and 32/64-bit floats)",
        col_name
    )))
}

/// Create an OptimizedDataFrame from a standard DataFrame
pub(crate) fn from_standard_dataframe(
    df: &crate::dataframe::DataFrame,
) -> Result<OptimizedDataFrame> {
    // Create a new SplitDataFrame (using internal implementation)
    let mut split_df = SplitDataFrame::new();

    for col_name in df.column_names() {
        let column = build_column_from_series(df, &col_name)?;
        split_df.add_column(col_name.clone(), column)?;
    }

    // Get and set the index.
    //
    // `DataFrame::get_index()` never returns `None`: when no index was ever
    // explicitly set on the source frame it synthesizes a placeholder
    // `DataFrameIndex::Simple(Index::default())`, which has length 0
    // regardless of the frame's actual row count. Handing that straight to
    // `set_index_from_simple_index` on a populated `split_df` used to fail
    // every such conversion with a length-mismatch error (0 != row_count) --
    // i.e. every `DataFrame` built without a follow-up `set_index()` call,
    // which is the common case. Detect that placeholder and synthesize a
    // real default index on the target instead of propagating the "empty"
    // length onto a non-empty frame.
    let df_index = df.get_index();

    match df_index {
        DataFrameIndex::Simple(simple_index)
            if simple_index.len() == 0 && split_df.row_count() > 0 =>
        {
            split_df.set_default_index()?;
        }
        DataFrameIndex::Simple(simple_index) => {
            // Direct copy for Simple Index
            split_df.set_index_from_simple_index(simple_index.clone())?;
        }
        DataFrameIndex::Multi(multi_index) => {
            // Convert multi-index to split DataFrame
            split_df.set_index(DataFrameIndex::Multi(multi_index.clone()))?;
        }
    }

    // Convert SplitDataFrame to OptimizedDataFrame
    let mut opt_df = OptimizedDataFrame::new();

    // Copy column data (using public API)
    for name in split_df.column_names() {
        if let Ok(column_view) = split_df.column(name) {
            let column = column_view.column().clone();
            opt_df.add_column(name.clone(), column)?;
        }
    }

    // Set the index (Simple or Multi -- `set_index_directly` handles both
    // uniformly, so a MultiIndex on the source DataFrame is preserved here
    // instead of being silently dropped).
    if let Some(split_index) = split_df.get_index() {
        opt_df.set_index_directly(split_index.clone())?;
    }

    Ok(opt_df)
}

/// Convert OptimizedDataFrame to a standard DataFrame
pub(crate) fn to_standard_dataframe(
    df: &OptimizedDataFrame,
) -> Result<crate::dataframe::DataFrame> {
    // Use internal SplitDataFrame
    let mut split_df = SplitDataFrame::new();

    // Convert column data
    for col_name in df.column_names() {
        let col_view = df.column(col_name)?;
        let col = col_view.column();

        // Add columns to SplitDataFrame
        split_df.add_column(col_name.clone(), col.clone())?;
    }

    // Set the index if it exists
    if let Some(df_index) = df.get_index() {
        // Use appropriate methods instead of directly setting internal fields
        if let DataFrameIndex::Simple(simple_index) = df_index {
            split_df.set_index_from_simple_index(simple_index.clone())?;
        } else if let DataFrameIndex::Multi(multi_index) = df_index {
            // Multi-index support
            split_df.set_index(DataFrameIndex::Multi(multi_index.clone()))?;
        }
    }

    // Convert to standard DataFrame
    let mut std_df = crate::dataframe::DataFrame::new();

    // Process each column. See the module-level doc comment for the NULL
    // policy applied to each column type below.
    for col_name in split_df.column_names() {
        let col_view = split_df.column(col_name)?;
        let col = col_view.column();

        match col {
            Column::Int64(int_col) => {
                let mut opts: Vec<Option<i64>> = Vec::with_capacity(int_col.len());
                for i in 0..int_col.len() {
                    opts.push(int_col.get(i)?);
                }
                if opts.iter().any(Option::is_none) {
                    // NULL present: Series<i64> cannot represent it. Upcast to
                    // Series<f64> with NaN at the NULL positions.
                    let values: Vec<f64> = opts
                        .into_iter()
                        .map(|v| v.map(|x| x as f64).unwrap_or(f64::NAN))
                        .collect();
                    let series = crate::series::Series::new(values, Some(col_name.clone()))?;
                    std_df.add_column(col_name.clone(), series)?;
                } else {
                    let values: Vec<i64> = opts.into_iter().map(|v| v.unwrap_or(0)).collect();
                    let series = crate::series::Series::new(values, Some(col_name.clone()))?;
                    std_df.add_column(col_name.clone(), series)?;
                }
            }
            Column::Float64(float_col) => {
                // Create Series<f64> directly so get_column::<f64>() downcast succeeds.
                // NULL -> NaN (see module doc comment).
                let mut values = Vec::with_capacity(float_col.len());
                for i in 0..float_col.len() {
                    values.push(float_col.get(i)?.unwrap_or(f64::NAN));
                }
                let series = crate::series::Series::new(values, Some(col_name.clone()))?;
                std_df.add_column(col_name.clone(), series)?;
            }
            Column::String(str_col) => {
                // Create Series<String> directly so get_column::<String>() downcast succeeds.
                // NULL -> "" (see module doc comment).
                let mut values = Vec::with_capacity(str_col.len());
                for i in 0..str_col.len() {
                    values.push(str_col.get(i)?.map(|s| s.to_string()).unwrap_or_default());
                }
                let series = crate::series::Series::new(values, Some(col_name.clone()))?;
                std_df.add_column(col_name.clone(), series)?;
            }
            Column::Boolean(bool_col) => {
                let mut opts: Vec<Option<bool>> = Vec::with_capacity(bool_col.len());
                for i in 0..bool_col.len() {
                    opts.push(bool_col.get(i)?);
                }
                if opts.iter().any(Option::is_none) {
                    // NULL present: Series<bool> cannot represent it. Promote to
                    // Series<String> ("true" / "false" / "").
                    let values: Vec<String> = opts
                        .into_iter()
                        .map(|v| match v {
                            Some(true) => "true".to_string(),
                            Some(false) => "false".to_string(),
                            None => String::new(),
                        })
                        .collect();
                    let series = crate::series::Series::new(values, Some(col_name.clone()))?;
                    std_df.add_column(col_name.clone(), series)?;
                } else {
                    let values: Vec<bool> = opts.into_iter().map(|v| v.unwrap_or(false)).collect();
                    let series = crate::series::Series::new(values, Some(col_name.clone()))?;
                    std_df.add_column(col_name.clone(), series)?;
                }
            }
        }
    }

    // Set the index
    if let Some(split_index) = split_df.get_index() {
        match split_index {
            DataFrameIndex::Simple(simple_index) => {
                // Set as string-based Simple Index
                std_df.set_index(simple_index.clone())?;
            }
            DataFrameIndex::Multi(multi_index) => {
                // For multi-index
                std_df.set_multi_index(multi_index.clone())?;
            }
        }
    }

    Ok(std_df)
}

/// Public function to convert a standard DataFrame to an optimized OptimizedDataFrame
pub fn optimize_dataframe(df: &crate::dataframe::DataFrame) -> Result<OptimizedDataFrame> {
    from_standard_dataframe(df)
}

/// Public function to convert an OptimizedDataFrame to a standard DataFrame
pub fn standard_dataframe(df: &OptimizedDataFrame) -> Result<crate::dataframe::DataFrame> {
    to_standard_dataframe(df)
}