Мне было интересно, как прочитать файл 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
Во-первых, вы должны отметить, что 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.
Невозможно, чтобы DataFrame, возвращаемый Python, был тем, чего на самом деле хочет OP.
Спасибо вам! @BallpointBen Это очень полезно. И Dataframe, возвращаемый в моем примере, взят из Rust, а не из Python. И вы правы: я не хочу, чтобы это был один столбец/строка со списком, передаваемым как одна точка данных.
@TrevorSeibert Как рекомендуют поляры, если вы используете этот ответ, рассмотрите возможность переключения распределителя на что-то вроде jemalloc или mimalloc, это значительно улучшит производительность этого кода (это также значительно улучшает мой первый ответ, но не второй , вероятно, потому, что он не выделяет много).
@ChayimFriedman Спасибо за примеры! У меня также есть последний вопрос, который я добавил выше. Как бы вы предложили создать DF в формате Flattened JSON?
@TrevorSeibert Это отдельный вопрос, и его следует задать как отдельный вопрос SO (не забудьте принять ответ, который вам помог). У вас есть два разных файла JSON?
@ChayimFriedman Да, у меня есть два разных файла JSON, другой из которых представляет собой расширенный формат, но с такими же данными.
@ChayimFriedman Я задам отдельный вопрос SO.
Если производительность важна, то версия @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
Спасибо! Хаим. Я тоже попробую это.
Я удивлен, что круговой обход serde_json::Value
даже конкурентоспособен по сравнению с вашим решением (если считать, что оно в 2 раза быстрее).
почему ты звонишь либо from_vec
, либо new
?
@ecoe to_vec()
быстрее, так как у него нулевое копирование, поэтому его следует отдавать предпочтение, но он недоступен для строк, поскольку внутреннее представление серии строк не эквивалентно Vec<String>
.
@ChayimFriedman, спасибо, я был бы признателен за разъяснение: почему, по вашему мнению, в Python есть удобная вспомогательная функция from_dict
для подобных случаев, а в Rust нет? Разве в Rust не может быть макроса from_struct
и/или from_structs
или какого-нибудь эквивалентного помощника?
@ecoe Я не могу говорить за разработчиков поляров, но такая функция в Rust будет намного сложнее, чем в Python. В Python любой класс можно легко преобразовать в словарь, а построение DataFrame
из списка словарей тривиально. Но в Rust для этого, вероятно, потребуется создание собственного производного объекта для структуры. Однако последняя версия на Rust гораздо более производительна.
Если 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
Вы уверены, что хотите, чтобы фрейм данных был похож на Python?