Skip to content

リストと配列(Lists and Arrays)

Polars は List 列にファーストクラスのサポートを提供します。つまり、各行が同一の要素で構成され、長さが異なるリストです。Polars には Array データタイプもあります。これは NumPy の ndarray オブジェクトに類似しており、行間で長さが同一です。

注意: これは Python の list オブジェクトとは異なります。要素は任意のタイプになります。Polars はこれらを列内で格納できますが、これから説明する特別なリスト操作機能がない一般的な Object データタイプです。

強力な List 操作

以下のデータが異なる天気ステーションから得られたとしましょう。天気ステーションが結果を得ることができない場合、実際の温度ではなくエラーコードが記録されます。

DataFrame

weather = pl.DataFrame(
    {
        "station": ["Station " + str(x) for x in range(1, 6)],
        "temperatures": [
            "20 5 5 E1 7 13 19 9 6 20",
            "18 8 16 11 23 E2 8 E2 E2 E2 90 70 40",
            "19 24 E9 16 6 12 10 22",
            "E2 E0 15 7 8 10 E1 24 17 13 6",
            "14 8 E0 16 22 24 E1",
        ],
    }
)
print(weather)

DataFrame

let stns: Vec<String> = (1..6).map(|i| format!("Station {i}")).collect();
let weather = df!(
        "station"=> &stns,
        "temperatures"=> &[
            "20 5 5 E1 7 13 19 9 6 20",
            "18 8 16 11 23 E2 8 E2 E2 E2 90 70 40",
            "19 24 E9 16 6 12 10 22",
            "E2 E0 15 7 8 10 E1 24 17 13 6",
            "14 8 E0 16 22 24 E1",
        ],
)?;
println!("{}", &weather);

shape: (5, 2)
┌───────────┬─────────────────────────────────┐
│ station   ┆ temperatures                    │
│ ---       ┆ ---                             │
│ str       ┆ str                             │
╞═══════════╪═════════════════════════════════╡
│ Station 1 ┆ 20 5 5 E1 7 13 19 9 6 20        │
│ Station 2 ┆ 18 8 16 11 23 E2 8 E2 E2 E2 90… │
│ Station 3 ┆ 19 24 E9 16 6 12 10 22          │
│ Station 4 ┆ E2 E0 15 7 8 10 E1 24 17 13 6   │
│ Station 5 ┆ 14 8 E0 16 22 24 E1             │
└───────────┴─────────────────────────────────┘

List 列の作成

上記で作成された weather DataFrame では、各ステーションによって捕捉された温度の分析がおそらく必要です。これを行うためには、まず個々の温度測定値を取得する必要があります。これは次のように行います:

str.split

out = weather.with_columns(pl.col("temperatures").str.split(" "))
print(out)

str.split

let out = weather
    .clone()
    .lazy()
    .with_columns([col("temperatures").str().split(lit(" "))])
    .collect()?;
println!("{}", &out);

shape: (5, 2)
┌───────────┬──────────────────────┐
│ station   ┆ temperatures         │
│ ---       ┆ ---                  │
│ str       ┆ list[str]            │
╞═══════════╪══════════════════════╡
│ Station 1 ┆ ["20", "5", … "20"]  │
│ Station 2 ┆ ["18", "8", … "40"]  │
│ Station 3 ┆ ["19", "24", … "22"] │
│ Station 4 ┆ ["E2", "E0", … "6"]  │
│ Station 5 ┆ ["14", "8", … "E1"]  │
└───────────┴──────────────────────┘

この後にできることの一つは、各温度測定をその自身の行に変換することです:

DataFrame.explode

out = weather.with_columns(pl.col("temperatures").str.split(" ")).explode(
    "temperatures"
)
print(out)

DataFrame.explode

let out = weather
    .clone()
    .lazy()
    .with_columns([col("temperatures").str().split(lit(" "))])
    .explode(["temperatures"])
    .collect()?;
println!("{}", &out);

shape: (49, 2)
┌───────────┬──────────────┐
│ station   ┆ temperatures │
│ ---       ┆ ---          │
│ str       ┆ str          │
╞═══════════╪══════════════╡
│ Station 1 ┆ 20           │
│ Station 1 ┆ 5            │
│ Station 1 ┆ 5            │
│ Station 1 ┆ E1           │
│ Station 1 ┆ 7            │
│ …         ┆ …            │
│ Station 5 ┆ E0           │
│ Station 5 ┆ 16           │
│ Station 5 ┆ 22           │
│ Station 5 ┆ 24           │
│ Station 5 ┆ E1           │
└───────────┴──────────────┘

しかし、Polars では List 要素を操作するためにこれを行う必要はしばしばありません。

List 列の操作

Polars は List 列に対していくつかの標準操作を提供します。最初の 3 つの測定値が必要な場合、head(3) を行います。最後の 3 つは tail(3) で取得できます。または、slice を使用しても良いです(負のインデックスがサポートされています)。また、観測数を lengths を通じて特定することもできます。それらを実行してみましょう:

Expr.list

out = weather.with_columns(pl.col("temperatures").str.split(" ")).with_columns(
    pl.col("temperatures").list.head(3).alias("top3"),
    pl.col("temperatures").list.slice(-3, 3).alias("bottom_3"),
    pl.col("temperatures").list.len().alias("obs"),
)
print(out)

Expr.list

let out = weather
    .clone()
    .lazy()
    .with_columns([col("temperatures").str().split(lit(" "))])
    .with_columns([
        col("temperatures").list().head(lit(3)).alias("top3"),
        col("temperatures")
            .list()
            .slice(lit(-3), lit(3))
            .alias("bottom_3"),
        col("temperatures").list().len().alias("obs"),
    ])
    .collect()?;
println!("{}", &out);

shape: (5, 5)
┌───────────┬──────────────────────┬────────────────────┬────────────────────┬─────┐
│ station   ┆ temperatures         ┆ top3               ┆ bottom_3           ┆ obs │
│ ---       ┆ ---                  ┆ ---                ┆ ---                ┆ --- │
│ str       ┆ list[str]            ┆ list[str]          ┆ list[str]          ┆ u32 │
╞═══════════╪══════════════════════╪════════════════════╪════════════════════╪═════╡
│ Station 1 ┆ ["20", "5", … "20"]  ┆ ["20", "5", "5"]   ┆ ["9", "6", "20"]   ┆ 10  │
│ Station 2 ┆ ["18", "8", … "40"]  ┆ ["18", "8", "16"]  ┆ ["90", "70", "40"] ┆ 13  │
│ Station 3 ┆ ["19", "24", … "22"] ┆ ["19", "24", "E9"] ┆ ["12", "10", "22"] ┆ 8   │
│ Station 4 ┆ ["E2", "E0", … "6"]  ┆ ["E2", "E0", "15"] ┆ ["17", "13", "6"]  ┆ 11  │
│ Station 5 ┆ ["14", "8", … "E1"]  ┆ ["14", "8", "E0"]  ┆ ["22", "24", "E1"] ┆ 7   │
└───────────┴──────────────────────┴────────────────────┴────────────────────┴─────┘

arr でしたが、今は list です

Stackoverflow や他の情報源で arr API に関する参照がある場合は、単に arrlist に置き換えてください。これは List データタイプの古いアクセサーでした。arr は最近導入された Array データタイプを指します(以下を参照)。

List 内の要素ごとの計算

初期 DataFrame からエラーの数が最も多いステーションを特定する必要がある場合、次の手順を行います:

  1. 文字列入力を List の文字列値として解析します(既に実行済み)。
  2. 数字に変換可能な文字列を特定します。
  3. リスト内の非数値(つまり null 値)の数を行ごとに特定します。
  4. この出力を errors と名付け、ステーションを簡単に特定できるようにします。

第三ステップには、リストの各要素にキャスティング(または代替として正規表現検索)操作を適用する必要があります。これは pl.element() コンテキストでそれらを最初に参照してから、適切な Polars 式を呼び出すことによって行うことができます。それを見てみましょう:

Expr.list · element

out = weather.with_columns(
    pl.col("temperatures")
    .str.split(" ")
    .list.eval(pl.element().cast(pl.Int64, strict=False).is_null())
    .list.sum()
    .alias("errors")
)
print(out)

Expr.list · element

let out = weather
    .clone()
    .lazy()
    .with_columns([col("temperatures")
        .str()
        .split(lit(" "))
        .list()
        .eval(col("").cast(DataType::Int64).is_null(), false)
        .list()
        .sum()
        .alias("errors")])
    .collect()?;
println!("{}", &out);

shape: (5, 3)
┌───────────┬─────────────────────────────────┬────────┐
│ station   ┆ temperatures                    ┆ errors │
│ ---       ┆ ---                             ┆ ---    │
│ str       ┆ str                             ┆ u32    │
╞═══════════╪═════════════════════════════════╪════════╡
│ Station 1 ┆ 20 5 5 E1 7 13 19 9 6 20        ┆ 1      │
│ Station 2 ┆ 18 8 16 11 23 E2 8 E2 E2 E2 90… ┆ 4      │
│ Station 3 ┆ 19 24 E9 16 6 12 10 22          ┆ 1      │
│ Station 4 ┆ E2 E0 15 7 8 10 E1 24 17 13 6   ┆ 3      │
│ Station 5 ┆ 14 8 E0 16 22 24 E1             ┆ 2      │
└───────────┴─────────────────────────────────┴────────┘

正規表現ルートを選択した場合はどうでしょうか(つまり、any 英字の存在を認識すること)?

str.contains

out = weather.with_columns(
    pl.col("temperatures")
    .str.split(" ")
    .list.eval(pl.element().str.contains("(?i)[a-z]"))
    .list.sum()
    .alias("errors")
)
print(out)

str.contains · Available on feature regex

let out = weather
    .clone()
    .lazy()
    .with_columns([col("temperatures")
        .str()
        .split(lit(" "))
        .list()
        .eval(col("").str().contains(lit("(?i)[a-z]"), false), false)
        .list()
        .sum()
        .alias("errors")])
    .collect()?;
println!("{}", &out);

shape: (5, 3)
┌───────────┬─────────────────────────────────┬────────┐
│ station   ┆ temperatures                    ┆ errors │
│ ---       ┆ ---                             ┆ ---    │
│ str       ┆ str                             ┆ u32    │
╞═══════════╪═════════════════════════════════╪════════╡
│ Station 1 ┆ 20 5 5 E1 7 13 19 9 6 20        ┆ 1      │
│ Station 2 ┆ 18 8 16 11 23 E2 8 E2 E2 E2 90… ┆ 4      │
│ Station 3 ┆ 19 24 E9 16 6 12 10 22          ┆ 1      │
│ Station 4 ┆ E2 E0 15 7 8 10 E1 24 17 13 6   ┆ 3      │
│ Station 5 ┆ 14 8 E0 16 22 24 E1             ┆ 2      │
└───────────┴─────────────────────────────────┴────────┘

(?i) に慣れていない場合は、Polars の str.contains 関数のドキュメントを見る良いタイミングです!Rust regex クレートは多くの追加の正規表現フラグを提供しており、役立つかもしれません。

行ごとの計算

このコンテキストは行方向での計算に理想的です。

list.eval 式(Rust では list().eval)を使用して、リストの要素に対して 任意の Polars 操作を適用することができます!これらの式は完全に Polars のクエリエンジンで実行され、並列に実行されるので、最適化されます。異なるステーションの 3 日間にわたる別の天気データがあるとしましょう:

DataFrame

weather_by_day = pl.DataFrame(
    {
        "station": ["Station " + str(x) for x in range(1, 11)],
        "day_1": [17, 11, 8, 22, 9, 21, 20, 8, 8, 17],
        "day_2": [15, 11, 10, 8, 7, 14, 18, 21, 15, 13],
        "day_3": [16, 15, 24, 24, 8, 23, 19, 23, 16, 10],
    }
)
print(weather_by_day)

DataFrame

let stns: Vec<String> = (1..11).map(|i| format!("Station {i}")).collect();
let weather_by_day = df!(
        "station" => &stns,
        "day_1" => &[17, 11, 8, 22, 9, 21, 20, 8, 8, 17],
        "day_2" => &[15, 11, 10, 8, 7, 14, 18, 21, 15, 13],
        "day_3" => &[16, 15, 24, 24, 8, 23, 19, 23, 16, 10],
)?;
println!("{}", &weather_by_day);

shape: (10, 4)
┌────────────┬───────┬───────┬───────┐
│ station    ┆ day_1 ┆ day_2 ┆ day_3 │
│ ---        ┆ ---   ┆ ---   ┆ ---   │
│ str        ┆ i64   ┆ i64   ┆ i64   │
╞════════════╪═══════╪═══════╪═══════╡
│ Station 1  ┆ 17    ┆ 15    ┆ 16    │
│ Station 2  ┆ 11    ┆ 11    ┆ 15    │
│ Station 3  ┆ 8     ┆ 10    ┆ 24    │
│ Station 4  ┆ 22    ┆ 8     ┆ 24    │
│ Station 5  ┆ 9     ┆ 7     ┆ 8     │
│ Station 6  ┆ 21    ┆ 14    ┆ 23    │
│ Station 7  ┆ 20    ┆ 18    ┆ 19    │
│ Station 8  ┆ 8     ┆ 21    ┆ 23    │
│ Station 9  ┆ 8     ┆ 15    ┆ 16    │
│ Station 10 ┆ 17    ┆ 13    ┆ 10    │
└────────────┴───────┴───────┴───────┘

面白いことをしてみましょう。各ステーションで測定された温度の日ごとのパーセンテージランクを計算します。Pandas では rank 値のパーセンテージを計算することができます。Polars はこれを直接行う特別な関数を提供していませんが、式がとても多用途であるため、自分のパーセンテージランク式を作成することができます。試してみましょう!

list.eval

rank_pct = (pl.element().rank(descending=True) / pl.col("*").count()).round(2)

out = weather_by_day.with_columns(
    # create the list of homogeneous data
    pl.concat_list(pl.all().exclude("station")).alias("all_temps")
).select(
    # select all columns except the intermediate list
    pl.all().exclude("all_temps"),
    # compute the rank by calling `list.eval`
    pl.col("all_temps").list.eval(rank_pct, parallel=True).alias("temps_rank"),
)

print(out)

list.eval · Available on feature list_eval

let rank_pct = (col("")
    .rank(
        RankOptions {
            method: RankMethod::Average,
            descending: true,
        },
        None,
    )
    .cast(DataType::Float32)
    / col("*").count().cast(DataType::Float32))
.round(2);

let out = weather_by_day
    .clone()
    .lazy()
    .with_columns(
        // create the list of homogeneous data
        [concat_list([all().exclude(["station"])])?.alias("all_temps")],
    )
    .select(
        // select all columns except the intermediate list
        [
            all().exclude(["all_temps"]),
            // compute the rank by calling `list.eval`
            col("all_temps")
                .list()
                .eval(rank_pct, true)
                .alias("temps_rank"),
        ],
    )
    .collect()?;

println!("{}", &out);

shape: (10, 5)
┌────────────┬───────┬───────┬───────┬────────────────────┐
│ station    ┆ day_1 ┆ day_2 ┆ day_3 ┆ temps_rank         │
│ ---        ┆ ---   ┆ ---   ┆ ---   ┆ ---                │
│ str        ┆ i64   ┆ i64   ┆ i64   ┆ list[f64]          │
╞════════════╪═══════╪═══════╪═══════╪════════════════════╡
│ Station 1  ┆ 17    ┆ 15    ┆ 16    ┆ [0.33, 1.0, 0.67]  │
│ Station 2  ┆ 11    ┆ 11    ┆ 15    ┆ [0.83, 0.83, 0.33] │
│ Station 3  ┆ 8     ┆ 10    ┆ 24    ┆ [1.0, 0.67, 0.33]  │
│ Station 4  ┆ 22    ┆ 8     ┆ 24    ┆ [0.67, 1.0, 0.33]  │
│ Station 5  ┆ 9     ┆ 7     ┆ 8     ┆ [0.33, 1.0, 0.67]  │
│ Station 6  ┆ 21    ┆ 14    ┆ 23    ┆ [0.67, 1.0, 0.33]  │
│ Station 7  ┆ 20    ┆ 18    ┆ 19    ┆ [0.33, 1.0, 0.67]  │
│ Station 8  ┆ 8     ┆ 21    ┆ 23    ┆ [1.0, 0.67, 0.33]  │
│ Station 9  ┆ 8     ┆ 15    ┆ 16    ┆ [1.0, 0.67, 0.33]  │
│ Station 10 ┆ 17    ┆ 13    ┆ 10    ┆ [0.33, 0.67, 1.0]  │
└────────────┴───────┴───────┴───────┴────────────────────┘

Polars Array

Array は最近導入された新しいデータタイプで、現在も機能が進化しています。ListArray の主な違いは、後者は行ごとに同じ数の要素を持つことが制限されている点ですが、List は可変の要素数を持つことができます。それでも、各要素のデータタイプは同じである必要があります。

このように Array 列を定義することができます:

Array

array_df = pl.DataFrame(
    [
        pl.Series("Array_1", [[1, 3], [2, 5]]),
        pl.Series("Array_2", [[1, 7, 3], [8, 1, 0]]),
    ],
    schema={
        "Array_1": pl.Array(pl.Int64, 2),
        "Array_2": pl.Array(pl.Int64, 3),
    },
)
print(array_df)

Array

let mut col1: ListPrimitiveChunkedBuilder<Int32Type> =
    ListPrimitiveChunkedBuilder::new("Array_1", 8, 8, DataType::Int32);
col1.append_slice(&[1, 3]);
col1.append_slice(&[2, 5]);
let mut col2: ListPrimitiveChunkedBuilder<Int32Type> =
    ListPrimitiveChunkedBuilder::new("Array_2", 8, 8, DataType::Int32);
col2.append_slice(&[1, 7, 3]);
col2.append_slice(&[8, 1, 0]);
let array_df = DataFrame::new([col1.finish(), col2.finish()].into())?;

println!("{}", &array_df);

shape: (2, 2)
┌───────────────┬───────────────┐
│ Array_1       ┆ Array_2       │
│ ---           ┆ ---           │
│ array[i64, 2] ┆ array[i64, 3] │
╞═══════════════╪═══════════════╡
│ [1, 3]        ┆ [1, 7, 3]     │
│ [2, 5]        ┆ [8, 1, 0]     │
└───────────────┴───────────────┘

基本操作が利用可能です:

Series.arr

out = array_df.select(
    pl.col("Array_1").arr.min().name.suffix("_min"),
    pl.col("Array_2").arr.sum().name.suffix("_sum"),
)
print(out)

Series.arr

let out = array_df
    .clone()
    .lazy()
    .select([
        col("Array_1").list().min().name().suffix("_min"),
        col("Array_2").list().sum().name().suffix("_sum"),
    ])
    .collect()?;
println!("{}", &out);

shape: (2, 2)
┌─────────────┬─────────────┐
│ Array_1_min ┆ Array_2_sum │
│ ---         ┆ ---         │
│ i64         ┆ i64         │
╞═════════════╪═════════════╡
│ 1           ┆ 11          │
│ 2           ┆ 9           │
└─────────────┴─────────────┘

Polars Array は現在も積極的に開発されており、このセクションは将来変更される可能性が高いです。