Struct データ型(The Struct datatype)
Polars の Struct は、複数のカラムを扱う際の慣用的な方法です。また、"無料"の操作です。つまり、カラムを Struct に移動してもデータのコピーは行われません!
このセクションでは、米国のいくつかの州での映画の平均評価をキャプチャする DataFrame から始めましょう。
ratings = pl.DataFrame(
    {
        "Movie": ["Cars", "IT", "ET", "Cars", "Up", "IT", "Cars", "ET", "Up", "ET"],
        "Theatre": ["NE", "ME", "IL", "ND", "NE", "SD", "NE", "IL", "IL", "SD"],
        "Avg_Rating": [4.5, 4.4, 4.6, 4.3, 4.8, 4.7, 4.7, 4.9, 4.7, 4.6],
        "Count": [30, 27, 26, 29, 31, 28, 28, 26, 33, 26],
    }
)
print(ratings)
let ratings = df!(
        "Movie"=> &["Cars", "IT", "ET", "Cars", "Up", "IT", "Cars", "ET", "Up", "ET"],
        "Theatre"=> &["NE", "ME", "IL", "ND", "NE", "SD", "NE", "IL", "IL", "SD"],
        "Avg_Rating"=> &[4.5, 4.4, 4.6, 4.3, 4.8, 4.7, 4.7, 4.9, 4.7, 4.6],
        "Count"=> &[30, 27, 26, 29, 31, 28, 28, 26, 33, 26],
)?;
println!("{}", &ratings);
shape: (10, 4)
┌───────┬─────────┬────────────┬───────┐
│ Movie ┆ Theatre ┆ Avg_Rating ┆ Count │
│ ---   ┆ ---     ┆ ---        ┆ ---   │
│ str   ┆ str     ┆ f64        ┆ i64   │
╞═══════╪═════════╪════════════╪═══════╡
│ Cars  ┆ NE      ┆ 4.5        ┆ 30    │
│ IT    ┆ ME      ┆ 4.4        ┆ 27    │
│ ET    ┆ IL      ┆ 4.6        ┆ 26    │
│ Cars  ┆ ND      ┆ 4.3        ┆ 29    │
│ Up    ┆ NE      ┆ 4.8        ┆ 31    │
│ IT    ┆ SD      ┆ 4.7        ┆ 28    │
│ Cars  ┆ NE      ┆ 4.7        ┆ 28    │
│ ET    ┆ IL      ┆ 4.9        ┆ 26    │
│ Up    ┆ IL      ┆ 4.7        ┆ 33    │
│ ET    ┆ SD      ┆ 4.6        ┆ 26    │
└───────┴─────────┴────────────┴───────┘
Struct 型との遭遇
Struct カラムになる一般的な操作は、探索的データ分析でよく使用される value_counts 関数です。州がデータにどれだけ登場するかを調べる方法は次のとおりです。
out = ratings.select(pl.col("Theatre").value_counts(sort=True))
print(out)
  value_counts ·  Available on feature dtype-struct
let out = ratings
    .clone()
    .lazy()
    .select([col("Theatre").value_counts(true, true)])
    .collect()?;
println!("{}", &out);
shape: (5, 1)
┌───────────┐
│ Theatre   │
│ ---       │
│ struct[2] │
╞═══════════╡
│ {"NE",3}  │
│ {"IL",3}  │
│ {"SD",2}  │
│ {"ME",1}  │
│ {"ND",1}  │
└───────────┘
特にこれまでのツールでは見られなかったデータ型から来る予期せぬ出力です。しかし、困っていません。より馴染みのある出力に戻るためには、Struct カラムを構成するカラムに unnest するだけです。
shape: (5, 2)
┌─────────┬───────┐
│ Theatre ┆ count │
│ ---     ┆ ---   │
│ str     ┆ u32   │
╞═════════╪═══════╡
│ NE      ┆ 3     │
│ IL      ┆ 3     │
│ SD      ┆ 2     │
│ ME      ┆ 1     │
│ ND      ┆ 1     │
└─────────┴───────┘
value_counts が Struct を返す理由
Polars のエクスプレッションは常に Fn(Series) -> Series のシグネチャを持ち、エクスプレッションの入出力として複数のカラムを提供するために Struct がデータ型として使われます。言い換えると、すべてのエクスプレッションは Series オブジェクトを返さなければならず、Struct はその要求を満たすのに役立ちます。
dict としての Structs
Polars は Series コンストラクタに送られた dict を Struct として解釈します:
rating_series = pl.Series(
    "ratings",
    [
        {"Movie": "Cars", "Theatre": "NE", "Avg_Rating": 4.5},
        {"Movie": "Toy Story", "Theatre": "ME", "Avg_Rating": 4.9},
    ],
)
print(rating_series)
// Don't think we can make it the same way in rust, but this works
let rating_series = df!(
    "Movie" => &["Cars", "Toy Story"],
    "Theatre" => &["NE", "ME"],
    "Avg_Rating" => &[4.5, 4.9],
)?
.into_struct("ratings")
.into_series();
println!("{}", &rating_series);
shape: (2,)
Series: 'ratings' [struct[3]]
[
    {"Cars","NE",4.5}
    {"Toy Story","ME",4.9}
]
Series オブジェクトの構築
ここでは Series が最初に name、その後に values で構築されていることに注意してください。
後者を最初に提供することは Polars ではアンチパターンとされ、避けるべきです。
Struct の個々の値を抽出する
上記で作成した Series の movie 値だけを取得する必要があるとしましょう。その場合、field メソッドを使用できます:
out = rating_series.struct.field("Movie")
print(out)
let out = rating_series.struct_()?.field_by_name("Movie")?;
println!("{}", &out);
shape: (2,)
Series: 'Movie' [str]
[
    "Cars"
    "Toy Story"
]
Struct の個々のキーをリネームする
Struct カラムの個々の field をリネームする必要がある場合、まず rating_series オブジェクトを DataFrame に変換して変更を簡単に確認できるようにし、その後 rename_fields メソッドを使用します:
out = (
    rating_series.to_frame()
    .select(pl.col("ratings").struct.rename_fields(["Film", "State", "Value"]))
    .unnest("ratings")
)
print(out)
let out = DataFrame::new([rating_series].into())?
    .lazy()
    .select([col("ratings")
        .struct_()
        .rename_fields(["Film".into(), "State".into(), "Value".into()].to_vec())])
    .unnest(["ratings"])
    .collect()?;
println!("{}", &out);
shape: (2, 3)
┌───────────┬───────┬───────┐
│ Film      ┆ State ┆ Value │
│ ---       ┆ ---   ┆ ---   │
│ str       ┆ str   ┆ f64   │
╞═══════════╪═══════╪═══════╡
│ Cars      ┆ NE    ┆ 4.5   │
│ Toy Story ┆ ME    ┆ 4.9   │
└───────────┴───────┴───────┘
Struct カラムの実用的な使用例
重複行の特定
ratings データに戻ります。Movie と Theatre レベルで重複がある場合を特定したいとします。ここで Struct データ型が光ります:
out = ratings.filter(pl.struct("Movie", "Theatre").is_duplicated())
print(out)
  is_duplicated ·  Struct ·  Available on feature dtype-struct
let out = ratings
    .clone()
    .lazy()
    // .filter(as_struct(&[col("Movie"), col("Theatre")]).is_duplicated())
    // Error: .is_duplicated() not available if you try that
    // https://github.com/pola-rs/polars/issues/3803
    .filter(len().over([col("Movie"), col("Theatre")]).gt(lit(1)))
    .collect()?;
println!("{}", &out);
shape: (4, 4)
┌───────┬─────────┬────────────┬───────┐
│ Movie ┆ Theatre ┆ Avg_Rating ┆ Count │
│ ---   ┆ ---     ┆ ---        ┆ ---   │
│ str   ┆ str     ┆ f64        ┆ i64   │
╞═══════╪═════════╪════════════╪═══════╡
│ Cars  ┆ NE      ┆ 4.5        ┆ 30    │
│ ET    ┆ IL      ┆ 4.6        ┆ 26    │
│ Cars  ┆ NE      ┆ 4.7        ┆ 28    │
│ ET    ┆ IL      ┆ 4.9        ┆ 26    │
└───────┴─────────┴────────────┴───────┘
このレベルでも is_unique を使用してユニークなケースを特定できます!
複数カラムのランキング
重複があることが分かっている場合、どのランクを優先させるかを決めたいとします。評価の Count を Avg_Rating 自体よりも重要とし、タイブレークにのみ使用します。次のように行えます:
out = ratings.with_columns(
    pl.struct("Count", "Avg_Rating")
    .rank("dense", descending=True)
    .over("Movie", "Theatre")
    .alias("Rank")
).filter(pl.struct("Movie", "Theatre").is_duplicated())
print(out)
  is_duplicated ·  Struct ·  Available on feature dtype-struct
let out = ratings
    .clone()
    .lazy()
    .with_columns([as_struct(vec![col("Count"), col("Avg_Rating")])
        .rank(
            RankOptions {
                method: RankMethod::Dense,
                descending: false,
            },
            None,
        )
        .over([col("Movie"), col("Theatre")])
        .alias("Rank")])
    // .filter(as_struct(&[col("Movie"), col("Theatre")]).is_duplicated())
    // Error: .is_duplicated() not available if you try that
    // https://github.com/pola-rs/polars/issues/3803
    .filter(len().over([col("Movie"), col("Theatre")]).gt(lit(1)))
    .collect()?;
println!("{}", &out);
shape: (4, 5)
┌───────┬─────────┬────────────┬───────┬──────┐
│ Movie ┆ Theatre ┆ Avg_Rating ┆ Count ┆ Rank │
│ ---   ┆ ---     ┆ ---        ┆ ---   ┆ ---  │
│ str   ┆ str     ┆ f64        ┆ i64   ┆ u32  │
╞═══════╪═════════╪════════════╪═══════╪══════╡
│ Cars  ┆ NE      ┆ 4.5        ┆ 30    ┆ 1    │
│ ET    ┆ IL      ┆ 4.6        ┆ 26    ┆ 2    │
│ Cars  ┆ NE      ┆ 4.7        ┆ 28    ┆ 2    │
│ ET    ┆ IL      ┆ 4.9        ┆ 26    ┆ 1    │
└───────┴─────────┴────────────┴───────┴──────┘
Polars でとてもエレガントに実現できるかなり複雑な要求のセットです!
複数カラム適用の使用
これは前のセクションの ユーザー定義関数 で議論されました。