リストと配列(Lists and Arrays)
Polars は List
列にファーストクラスのサポートを提供します。つまり、各行が同一の要素で構成され、長さが異なるリストです。Polars には Array
データタイプもあります。これは NumPy の ndarray
オブジェクトに類似しており、行間で長さが同一です。
注意: これは Python の list
オブジェクトとは異なります。要素は任意のタイプになります。Polars はこれらを列内で格納できますが、これから説明する特別なリスト操作機能がない一般的な Object
データタイプです。
強力な List
操作
以下のデータが異なる天気ステーションから得られたとしましょう。天気ステーションが結果を得ることができない場合、実際の温度ではなくエラーコードが記録されます。
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)
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
では、各ステーションによって捕捉された温度の分析がおそらく必要です。これを行うためには、まず個々の温度測定値を取得する必要があります。これは次のように行います:
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"] │
└───────────┴──────────────────────┘
この後にできることの一つは、各温度測定をその自身の行に変換することです:
out = weather.with_columns(pl.col("temperatures").str.split(" ")).explode(
"temperatures"
)
print(out)
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
を通じて特定することもできます。それらを実行してみましょう:
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)
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 に関する参照がある場合は、単に arr
を list
に置き換えてください。これは List
データタイプの古いアクセサーでした。arr
は最近導入された Array
データタイプを指します(以下を参照)。
List
内の要素ごとの計算
初期 DataFrame
からエラーの数が最も多いステーションを特定する必要がある場合、次の手順を行います:
- 文字列入力を
List
の文字列値として解析します(既に実行済み)。 - 数字に変換可能な文字列を特定します。
- リスト内の非数値(つまり
null
値)の数を行ごとに特定します。 - この出力を
errors
と名付け、ステーションを簡単に特定できるようにします。
第三ステップには、リストの各要素にキャスティング(または代替として正規表現検索)操作を適用する必要があります。これは pl.element()
コンテキストでそれらを最初に参照してから、適切な Polars 式を呼び出すことによって行うことができます。それを見てみましょう:
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)
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 英字の存在を認識すること)?
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 日間にわたる別の天気データがあるとしましょう:
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)
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 はこれを直接行う特別な関数を提供していませんが、式がとても多用途であるため、自分のパーセンテージランク式を作成することができます。試してみましょう!
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
は最近導入された新しいデータタイプで、現在も機能が進化しています。List
と Array
の主な違いは、後者は行ごとに同じ数の要素を持つことが制限されている点ですが、List
は可変の要素数を持つことができます。それでも、各要素のデータタイプは同じである必要があります。
このように 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)
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] │
└───────────────┴───────────────┘
基本操作が利用可能です:
out = array_df.select(
pl.col("Array_1").arr.min().name.suffix("_min"),
pl.col("Array_2").arr.sum().name.suffix("_sum"),
)
print(out)
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
は現在も積極的に開発されており、このセクションは将来変更される可能性が高いです。