Преобразование ключа JSON в фрейм данных Polars

Мне было интересно, как прочитать файл JSON в DataFrame Polars в Rust по ключу «данные». Однако я считаю, что структуру файла JSON, которая у меня есть, будет трудно достичь.

Вот первая структура файла JSON, содержащая типы данных.

{
  "data": [
    {
      "dataItemName": "TICKER",
      "result": [
        "AAPL",
        "MSFT",
        "TSLA"
      ],
      "dataType": "STRING",
      "error": 0
    },
    {
      "dataItemName": "SALES",
      "result": [ 
        259968,
        143015,
        24578
      ],
      "dataType": "DOUBLE",
      "error": 0
    },
    {
      "dataItemName": "CNAME",
      "result": [
        "Apple Inc.",
        "Microsoft Corporation",
        "Tesla Inc"
      ],
      "dataType": "STRING",
      "error": 0
    },
    {
      "dataItemName": "PRICE",
      "result": [
        115.98,
        214.22,
        430.83
      ],
      "dataType": "DOUBLE",
      "error": 0
    },
    {
      "dataItemName": "ASSETS",
      "result": [
        338516,
        301311,
        34309
      ],
      "dataType": "DOUBLE",
      "error": 0
    }
  ]
}

Вот что я пробовал в Rust.

use polars::prelude::*;


fn main() {
    let json_file = std::fs::File::open("data/test_merged.json").unwrap();
    let df = JsonReader::new(json_file).finish().unwrap();
    println!("{:?}", df);
}

Вот пример вывода Rust, в котором один столбец/строка DataFrame

shape: (1, 1)
┌───────────────────────────────────┐
│ data                              │
│ ---                               │
│ list[struct[63]]                  │
╞═══════════════════════════════════╡
│ [{0.0,0.530558,3.38631,"2023-06-… │
└───────────────────────────────────┘

Существует только 3 типа данных: числа с плавающей запятой и целые числа.

Вот аналогичный вопрос для версии Python. преобразовать json в фрейм данных Polars

Вы уверены, что хотите, чтобы фрейм данных был похож на Python?

Chayim Friedman 25.03.2024 06:53
Почему Python в конце концов умрет
Почему Python в конце концов умрет
Последние 20 лет были действительно хорошими для Python. Он прошел путь от "просто языка сценариев" до основного языка, используемого для написания...
2
1
342
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Во-первых, вы должны отметить, что 1. в вашем коде Python Polars читает не из json, а скорее читает из уже созданного в памяти словаря, созданного из этого json. 2. полученный df почти наверняка не тот, который вам нужен.

Polars поддерживает serde, но у него свой собственный формат, поэтому это не так просто, как просто массировать входящие данные. Самый простой способ, вероятно, — создать структуры, имитирующие структуру, которую ожидает Polars, и реализовать все необходимые переименования полей, затем десериализовать, чтобы переименование произошло, повторную сериализацию с переименованными полями, а затем снова десериализовать в DataFrame. Для приведенного ниже кода требуются ящики polars с включенной функцией serde, serde и serde_json.

use polars::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize, Serialize)]
#[serde(untagged)]
enum Values {
    String(Vec<String>),
    Double(Vec<f64>),
}

#[derive(Debug, Deserialize, Serialize)]
enum DataType {
    #[serde(rename(deserialize = "STRING", serialize = "Utf8"))]
    String,
    #[serde(rename(deserialize = "DOUBLE"))]
    Float64,
}

#[derive(Debug, Deserialize, Serialize)]
struct Column {
    #[serde(rename(deserialize = "dataItemName"))]
    name: String,
    #[serde(rename(deserialize = "result"))]
    values: Values,
    #[serde(rename(deserialize = "dataType"))]
    datatype: DataType,
}

#[derive(Debug, Deserialize, Serialize)]
struct Data {
    #[serde(rename(deserialize = "data"))]
    columns: Vec<Column>,
}

fn main() -> anyhow::Result<()> {
    let data = serde_json::from_str::<Data>(DATA)?;
    let df = serde_json::from_value::<DataFrame>(serde_json::to_value(data)?)?;
    println!("{df:?}");

    Ok(())
}

Результат

shape: (3, 5)
┌────────┬──────────┬───────────────────────┬────────┬──────────┐
│ TICKER ┆ SALES    ┆ CNAME                 ┆ PRICE  ┆ ASSETS   │
│ ---    ┆ ---      ┆ ---                   ┆ ---    ┆ ---      │
│ str    ┆ f64      ┆ str                   ┆ f64    ┆ f64      │
╞════════╪══════════╪═══════════════════════╪════════╪══════════╡
│ AAPL   ┆ 259968.0 ┆ Apple Inc.            ┆ 115.98 ┆ 338516.0 │
│ MSFT   ┆ 143015.0 ┆ Microsoft Corporation ┆ 214.22 ┆ 301311.0 │
│ TSLA   ┆ 24578.0  ┆ Tesla Inc             ┆ 430.83 ┆ 34309.0  │
└────────┴──────────┴───────────────────────┴────────┴──────────┘

Конечно, DATA — это предоставленная вами строка,

const DATA: &str = r#"
{
  "data": [
    {
      "dataItemName": "TICKER",
      "result": [
        "AAPL",
        "MSFT",
        "TSLA"
      ],
      "dataType": "STRING",
      "error": 0
    },
    {
      "dataItemName": "SALES",
      "result": [
        259968,
        143015,
        24578
      ],
      "dataType": "DOUBLE",
      "error": 0
    },
    {
      "dataItemName": "CNAME",
      "result": [
        "Apple Inc.",
        "Microsoft Corporation",
        "Tesla Inc"
      ],
      "dataType": "STRING",
      "error": 0
    },
    {
      "dataItemName": "PRICE",
      "result": [
        115.98,
        214.22,
        430.83
      ],
      "dataType": "DOUBLE",
      "error": 0
    },
    {
      "dataItemName": "ASSETS",
      "result": [
        338516,
        301311,
        34309
      ],
      "dataType": "DOUBLE",
      "error": 0
    }
  ]
}
"#;

Этот фрейм данных не совпадает с возвращаемым кодом Python.

Chayim Friedman 25.03.2024 06:53

Невозможно, чтобы DataFrame, возвращаемый Python, был тем, чего на самом деле хочет OP.

BallpointBen 25.03.2024 15:01

Спасибо вам! @BallpointBen Это очень полезно. И Dataframe, возвращаемый в моем примере, взят из Rust, а не из Python. И вы правы: я не хочу, чтобы это был один столбец/строка со списком, передаваемым как одна точка данных.

Trevor Seibert 25.03.2024 15:33

@TrevorSeibert Как рекомендуют поляры, если вы используете этот ответ, рассмотрите возможность переключения распределителя на что-то вроде jemalloc или mimalloc, это значительно улучшит производительность этого кода (это также значительно улучшает мой первый ответ, но не второй , вероятно, потому, что он не выделяет много).

Chayim Friedman 25.03.2024 21:23

@ChayimFriedman Спасибо за примеры! У меня также есть последний вопрос, который я добавил выше. Как бы вы предложили создать DF в формате Flattened JSON?

Trevor Seibert 25.03.2024 21:29

@TrevorSeibert Это отдельный вопрос, и его следует задать как отдельный вопрос SO (не забудьте принять ответ, который вам помог). У вас есть два разных файла JSON?

Chayim Friedman 25.03.2024 22:15

@ChayimFriedman Да, у меня есть два разных файла JSON, другой из которых представляет собой расширенный формат, но с такими же данными.

Trevor Seibert 25.03.2024 22:22

@ChayimFriedman Я задам отдельный вопрос SO.

Trevor Seibert 25.03.2024 22:46
Ответ принят как подходящий

Если производительность важна, то версия @BallpointBen не самая быстрая из возможных; вот более производительная версия:

pub fn convert(json: &str) -> Result<DataFrame, Box<dyn Error>> {
    use serde::Deserialize;

    #[derive(Debug, Deserialize)]
    #[serde(untagged)]
    enum Values {
        String(Vec<String>),
        Double(Vec<f64>),
    }

    #[derive(Debug, Deserialize)]
    #[serde(rename_all = "UPPERCASE")]
    enum DataType {
        String,
        Double,
    }

    #[derive(Debug, Deserialize)]
    #[serde(rename_all = "camelCase")]
    struct Column {
        data_item_name: String,
        result: Values,
        data_type: DataType,
    }

    #[derive(Debug, Deserialize)]
    struct Data {
        data: Vec<Column>,
    }

    let data = serde_json::from_str::<Data>(json)?;
    let df = data
        .data
        .into_iter()
        .map(|column| match column.data_type {
            DataType::String => {
                let Values::String(values) = column.result else {
                    return Err("column type mismatch");
                };
                Ok(Series::new(&column.data_item_name, values))
            }
            DataType::Double => {
                let Values::Double(values) = column.result else {
                    return Err("column type mismatch");
                };
                Ok(Series::from_vec(&column.data_item_name, values))
            }
        })
        .collect::<Result<DataFrame, _>>()?;

    Ok(df)
}

Тест с 1000 случайными входами:

BallpointBen            time:   [338.41 µs 340.05 µs 341.85 µs]
Found 2 outliers among 100 measurements (2.00%)
  2 (2.00%) high mild

Mine                    time:   [195.82 µs 196.79 µs 197.95 µs]
Found 11 outliers among 100 measurements (11.00%)
  8 (8.00%) high mild
  3 (3.00%) high severe

Спасибо! Хаим. Я тоже попробую это.

Trevor Seibert 25.03.2024 15:44

Я удивлен, что круговой обход serde_json::Value даже конкурентоспособен по сравнению с вашим решением (если считать, что оно в 2 раза быстрее).

BallpointBen 26.03.2024 05:30

почему ты звонишь либо from_vec, либо new?

ecoe 07.07.2024 01:42

@ecoe to_vec() быстрее, так как у него нулевое копирование, поэтому его следует отдавать предпочтение, но он недоступен для строк, поскольку внутреннее представление серии строк не эквивалентно Vec<String>.

Chayim Friedman 07.07.2024 01:48

@ChayimFriedman, спасибо, я был бы признателен за разъяснение: почему, по вашему мнению, в Python есть удобная вспомогательная функция from_dict для подобных случаев, а в Rust нет? Разве в Rust не может быть макроса from_struct и/или from_structs или какого-нибудь эквивалентного помощника?

ecoe 07.07.2024 15:55

@ecoe Я не могу говорить за разработчиков поляров, но такая функция в Rust будет намного сложнее, чем в Python. В Python любой класс можно легко преобразовать в словарь, а построение DataFrame из списка словарей тривиально. Но в Rust для этого, вероятно, потребуется создание собственного производного объекта для структуры. Однако последняя версия на Rust гораздо более производительна.

Chayim Friedman 07.07.2024 16:06

Если JSON является самоописающимся, т. е. в нем нет двух типов данных, представленных одним и тем же типом JSON (например, даты и строки, представленные в виде строк) — другими словами, поле dataType избыточно, то самый быстрый способ — это десериализовать непосредственно в Series:

pub fn directly_into_series(json: &str) -> Result<DataFrame, Box<dyn Error>> {
    use serde::de::{DeserializeSeed, Deserializer, Error, SeqAccess, Visitor};
    use serde::Deserialize;

    #[derive(Debug, Deserialize)]
    #[serde(rename_all = "camelCase")]
    struct Column {
        data_item_name: String,
        #[serde(deserialize_with = "deserialize_values")]
        result: Series,
    }

    fn deserialize_values<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Series, D::Error> {
        struct Builders {
            strings: StringChunkedBuilder,
            floats: PrimitiveChunkedBuilder<Float64Type>,
            has_strings: bool,
            has_floats: bool,
        }
        impl Default for Builders {
            fn default() -> Self {
                Self {
                    strings: StringChunkedBuilder::new("", 0),
                    floats: PrimitiveChunkedBuilder::new("", 0),
                    has_strings: false,
                    has_floats: false,
                }
            }
        }

        struct ElementDeserializer<'a>(&'a mut Builders);

        impl<'de, 'a> DeserializeSeed<'de> for ElementDeserializer<'a> {
            type Value = ();

            fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
            where
                D: Deserializer<'de>,
            {
                deserializer.deserialize_any(self)
            }
        }

        impl<'de, 'a> Visitor<'de> for ElementDeserializer<'a> {
            type Value = ();

            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
                write!(formatter, "expected a float or string")
            }

            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
            where
                E: Error,
            {
                self.0.strings.append_value(v);
                self.0.has_strings = true;
                Ok(())
            }

            fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
            where
                E: Error,
            {
                self.0.floats.append_value(v);
                self.0.has_floats = true;
                Ok(())
            }
        }

        struct SeqVisitor;

        impl<'de> Visitor<'de> for SeqVisitor {
            type Value = Series;

            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
                write!(formatter, "expected a sequence of floats or strings")
            }

            fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
            where
                A: SeqAccess<'de>,
            {
                let mut builders = Builders::default();
                while let Some(()) = seq.next_element_seed(ElementDeserializer(&mut builders))? {}

                match (builders.has_strings, builders.has_floats) {
                    (false, false) | (true, false) => Ok(builders.strings.finish().into_series()),
                    (false, true) => Ok(builders.floats.finish().into_series()),
                    (true, true) => Err(A::Error::custom("sequence with both floats and strings")),
                }
            }
        }

        deserializer.deserialize_seq(SeqVisitor)
    }

    #[derive(Debug, Deserialize)]
    struct Data {
        data: Vec<Column>,
    }

    let data = serde_json::from_str::<Data>(json)?;
    let df = data
        .data
        .into_iter()
        .map(|mut column| {
            column.result.rename(&column.data_item_name);
            column.result
        })
        .collect::<DataFrame>();

    Ok(df)
}

Тест со случайными 10 000 записей:

BallpointBen            time:   [3.2342 ms 3.2510 ms 3.2692 ms]
Found 2 outliers among 100 measurements (2.00%)
  2 (2.00%) high mild

Benchmarking Mine (other answer): Warming up for 3.0000 s
Warning: Unable to complete 100 samples in 5.0s. You may wish to increase target time to 9.5s, enable flat sampling, or reduce sample count to 50.
Mine (other answer)     time:   [1.8601 ms 1.8670 ms 1.8745 ms]
Found 6 outliers among 100 measurements (6.00%)
  2 (2.00%) high mild
  4 (4.00%) high severe

Deserialize directly into `Series`
                        time:   [739.58 µs 741.60 µs 743.79 µs]
Found 1 outliers among 100 measurements (1.00%)
  1 (1.00%) high severe

Другие вопросы по теме