データフレーム#

データフレーム(DataFrame)は Polars の中心的なデータ構造で、行と列からなる2次元の表を表します。各列はひとつのデータ型を持ち、列ごとに型が異なっていてもかまいません。

「列単位で保持し、列単位で型を管理する」という設計が Polars の速さの源であり、使い方の作法もそこから決まります。列に対する操作を中心に書き、行単位で値を取り出すのは避けるのが基本です。この章では、まずデータフレームの作り方、続いて列・行・単一値の取り出し方を説明します。

import polars as pl
from helper.jupyter import row

データフレームの作成#

ここでは、Python の他のオブジェクトからデータフレームを作る方法を説明します。データを手で書くことはほとんどありませんが、テスト用の小さな例は必ず作ることになります。作り方では、列名をどこから取るかと、型をどう推論するかの2つを決めます。dict[list] なら列名は辞書のキーになります。list[list] では列名が既定で column_0、column_1 といった仮の名前になるので、変えたいときは schema を渡します。型は Polars が値から推論します。整数と None だけの列は Int64 のままですが、整数と文字列が同じ列に混じるとエラーになります。strict=False を渡せば通りますが、そのとき列全体が String になります。

リストと辞書の組み合わせ#

次のプログラムは、Python の異なるデータ構造(辞書のリスト、リストの辞書、リストのリスト)から DataFrame を作ります。

  1. 辞書のリスト(list[dict])— 辞書のキーが列名になり、リスト内の各辞書が1行分のデータになります。

  2. リストの辞書(dict[list])— 辞書のキーが列名になり、各リストの要素がその列のデータになります。

  3. リストのリスト(list[list])— schema 引数で列名を指定します。

    • orient='row' なら、データの方向が行単位であることを指定します。内側のリスト1つが1行分のデータになります。

    • orient='col' なら、データの方向が列単位であることを指定します。内側のリスト1つが1列分のデータになります。この例では内側のリストのデータ型がそろっていないため、strict=False で自動型変換を有効にします。

# list[dict]
dict_in_list = [
    {"name": "Alice", "age": 30},
    {"name": "Bob", "age": 25},
    {"name": "Charlie", "age": 35}
]
df1 = pl.DataFrame(dict_in_list)

# dict[list]
list_in_dict = {
    "name": ["Alice", "Bob", "Charlie"],
    "age": [30, 25, 35],
}
df2 = pl.DataFrame(list_in_dict)

# list[list]
list_in_list = [
    ["Alice", 30],
    ["Bob", 25],
    ["Charlie", 35]
]
columns = ["name", "age"]  # カラム名を指定
df3 = pl.DataFrame(list_in_list, schema=columns, orient='row')
df4 = pl.DataFrame(list_in_list, schema=['p1', 'p2', 'p3'], orient='col', strict=False)
row(df1, df2, df3, df4)
shape: (3, 2)
nameage
stri64
"Alice"30
"Bob"25
"Charlie"35
shape: (3, 2)
nameage
stri64
"Alice"30
"Bob"25
"Charlie"35
shape: (3, 2)
nameage
stri64
"Alice"30
"Bob"25
"Charlie"35
shape: (2, 3)
p1p2p3
strstrstr
"Alice""Bob""Charlie"
"30""25""35"

data は辞書で、値は次の2種類です。

  • "point" キーの値は辞書のリスト(list[dict])で、各辞書には x と y の2つのキーが含まれています。

  • "weight" キーの値は整数のリスト(list[int])です。

データフレームに変換すると、外側の辞書のキーが列名になり、point 列の要素は Struct 型(構造体)に変換されます。

data = {
    "point": [{"x": 1, "y": 2}, {"x": 3, "y": 4}, {"x": 5, "y": 6}],
    "weight": [5, 4, 8],
}
pl.DataFrame(data)
shape: (3, 2)
pointweight
struct[2]i64
{1,2}5
{3,4}4
{5,6}8

NumPy の配列#

NumPy の配列は、リストと同じように扱えます。

  • dict[list] と dict[1次元配列] は同じ

  • list[list] と 2次元配列は同じ

import numpy as np
array_in_dict = {
    "x": np.array([1, 3, 5]),
    "y": np.array([2, 4, 6]),
}

df1 = pl.DataFrame(array_in_dict)

array_2d = np.array([[1, 2], [3, 4], [5, 6]])
df2 = pl.DataFrame(array_2d, schema=['x', 'y'], orient='row')
df3 = pl.DataFrame(array_2d, schema=['p1', 'p2', 'p3'], orient='col')
row(df1, df2, df3)
shape: (3, 2)
xy
i64i64
12
34
56
shape: (3, 2)
xy
i64i64
12
34
56
shape: (2, 3)
p1p2p3
i64i64i64
135
246

1次元の構造化配列をデータフレームに変換すると、配列の各フィールドがデータフレームの各列になります。

arr = np.array([
    (1, 30),
    (2, 25),
    (3, 35)], dtype=[('x', 'i2'), ('y', 'i2')])

pl.DataFrame(arr)
shape: (3, 2)
xy
i16i16
130
225
335

Series を含むデータ#

pl.Series を扱うときは、list[Series] や dict[Series] をデータフレームに変換することがよくあります。どちらの場合も各 Series がデータフレームの列になりますが、列名の扱いが異なります。

  • list[Series]: 列名は Series の名前がそのまま使われます。

  • dict[Series]: 列名は辞書のキーが使われます。

sx = pl.Series('x', [1, 2, 3])
sy = pl.Series('y', [4, 5, 6])

df1 = pl.DataFrame([sx, sy])
df2 = pl.DataFrame({'A':sx, 'B':sy})
row(df1, df2)
shape: (3, 2)
xy
i64i64
14
25
36
shape: (3, 2)
AB
i64i64
14
25
36

pl.from_*()関数#

from_ で始まる関数は、さまざまなデータをデータフレームに変換します。これらを使うと、意図しないデータ変換が起こりにくくなり、コードの堅牢性を高められます。

  • pl.from_dict() — dict[list] から変換

  • pl.from_dicts() — list[dict] から変換

  • pl.from_numpy() — NumPy の配列から変換

  • pl.from_records() — list[list] から変換

  • pl.from_pandas() — Pandas の DataFrame から変換

  • pl.from_arrow() — pyarrow の Array または Table から変換

データフレームの属性#

df = pl.DataFrame(
    {
        "a": [3, 3, 3, 4],
        "b": [4.0, 12, 6, 7],
        "g": ['A', 'B', 'A', 'B']
    }
)

shape 属性でデータフレームの形状(高さ、幅)を取得できます。height と width 属性で高さと幅をそれぞれ取得でき、len() 関数でも高さを取得できます。

print(f"{df.shape = }")
print(f"{df.height = }")
print(f"{df.width = }")
print(f"{len(df) = }")
df.shape = (4, 3)
df.height = 4
df.width = 3
len(df) = 4

columns 属性で列名のリストを取得できます。

df.columns
['a', 'b', 'g']

schema 属性で、列名とデータ型の対応を表す辞書を取得できます。

df.schema
Schema([('a', Int64), ('b', Float64), ('g', String)])
df.schema['g']
String

dtypes 属性で、各列のデータ型のリストを取得できます。

df.dtypes
[Int64, Float64, String]

flags 属性で各列のソート状態を取得できます。つまり、Polars がその列を「すでに並んでいる」と認識しているかどうかが分かります。

このフラグは最適化に効きます。ソート済みと分かっていれば、範囲検索やマージのときに並べ直す手間を省けるからです。

df.flags
{'a': {'SORTED_ASC': False, 'SORTED_DESC': False},
 'b': {'SORTED_ASC': False, 'SORTED_DESC': False},
 'g': {'SORTED_ASC': False, 'SORTED_DESC': False}}

データフレームからデータ取得#

ここでは、データフレームから列・行・単一の値を取り出す方法を説明します。

列を取得#

Polars で DataFrame から列を Series として取り出す方法はいくつかあります。

  • DataFrame.to_series() — インデックスで列を取得します。

  • DataFrame.get_column() — 列名で列を取得します。

  • DataFrame.get_columns() — すべての列を取得します。

  • DataFrame.iter_columns() — 列のイテレーターを取得します。

DataFrame.to_series() は、指定したインデックスの列を Series として返します。DataFrame.get_column() は列名を指定して Series を取得します。DataFrame["column_name"] のように辞書形式で列名を指定することもできます。

s1 = df.to_series(0)
s2 = df.get_column('b')
s3 = df['g']
row(s1, s2, s3)
shape: (4,)
a
i64
3
3
3
4
shape: (4,)
b
f64
4.0
12.0
6.0
7.0
shape: (4,)
g
str
"A"
"B"
"A"
"B"

get_columns() は、DataFrame のすべての列を Series のリストとして返します。

sa, sb, sg = df.get_columns()
row(sa, sb, sg)
shape: (4,)
a
i64
3
3
3
4
shape: (4,)
b
f64
4.0
12.0
6.0
7.0
shape: (4,)
g
str
"A"
"B"
"A"
"B"

iter_columns() は、DataFrame の列を1つずつ返します。

for col in df.iter_columns():
    print(col.name, col.to_list())
a [3, 3, 3, 4]
b [4.0, 12.0, 6.0, 7.0]
g ['A', 'B', 'A', 'B']

Series オブジェクト#

print(f"{s1.name = }")
print(f"{s1.dtype = }")
print(f"{s1.flags = }")
print(f"{s1.shape = }")
print(f"{len(s1) = }")
s1.name = 'a'
s1.dtype = Int64
s1.flags = {'SORTED_ASC': False, 'SORTED_DESC': False}
s1.shape = (4,)
len(s1) = 4

to_numpy()メソッド#

to_numpy() または to_list() を使うと、Series を NumPy の配列やリストに変換できます。

to_numpy() は通常データをコピーしません(ゼロコピー)。一方 to_list() は必ず Python オブジェクトを作るので、Python 側のライブラリに渡す必要があるときだけ使います。

print(f'{sa.to_numpy() = }')
print(f'{sa.to_list() = }')
sa.to_numpy() = array([3, 3, 3, 4])
sa.to_list() = [3, 3, 3, 4]

DataFrame.to_numpy() で NumPy の配列に変換できます。既定では、すべての値がもっとも上位のデータ型に変換されます。数値と文字列が混在するデータでは、dtype=object の配列になります。

df.to_numpy()
array([[3, 4.0, 'A'],
       [3, 12.0, 'B'],
       [3, 6.0, 'A'],
       [4, 7.0, 'B']], dtype=object)

structured=True を渡すと、構造化配列に変換します。

df.to_numpy(structured=True)
array([(3,  4., 'A'), (3, 12., 'B'), (3,  6., 'A'), (4,  7., 'B')],
      dtype=[('a', '<i8'), ('b', '<f8'), ('g', '<U1')])

Series の要素がリストの場合、to_numpy() を呼ぶと dtype=object の配列が得られます。配列の各要素は、さらに配列になっています。このような構造は高速な処理には向きません。

s_list = pl.Series('s_list', [[1, 2, 3], [4, 5, 6], [7, 8, 9]])
s_list.to_numpy()
array([array([1, 2, 3]), array([4, 5, 6]), array([7, 8, 9])], dtype=object)

Series の要素が Array の場合、to_numpy() は多次元配列を返します。

s_arr = pl.Series('s_arr', [[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=pl.Array(pl.Int8, 3))
s_arr.to_numpy()
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]], dtype=int8)

Series の要素が Struct の場合、to_numpy() は2次元配列を返し、各要素のデータ型はもっとも上位の型にそろえられます。次の例では、Int64 と Float64 が Float64 にそろっています。

s_struct = pl.Series('s_struct', [{"x": 1, "y": 2.0}, {"x": 3, "y": 4.0}, {"x": 5, "y": 6.0}])
s_struct.to_numpy()
array([[1., 2.],
       [3., 4.],
       [5., 6.]])

NumPy の構造化配列に変換するには、まず struct.unnest() で DataFrame に変換し、そのあと to_numpy(structured=True) を呼びます。

s_struct.struct.unnest().to_numpy(structured=True)
array([(1, 2.), (3, 4.), (5, 6.)], dtype=[('x', '<i8'), ('y', '<f8')])

to_dict()メソッド#

to_dict() で dict[Series] 型の辞書に変換します。

df.to_dict()
{'a': shape: (4,)
 Series: 'a' [i64]
 [
 	3
 	3
 	3
 	4
 ],
 'b': shape: (4,)
 Series: 'b' [f64]
 [
 	4.0
 	12.0
 	6.0
 	7.0
 ],
 'g': shape: (4,)
 Series: 'g' [str]
 [
 	"A"
 	"B"
 	"A"
 	"B"
 ]}

to_dicts()メソッド#

to_dicts() で list[dict] 型のリストに変換します。1行につき1つの辞書です。

もっとも Python らしい形で、JSON へのシリアライズや外部ライブラリに渡すのに向いています。ただし行ごとにオブジェクトを作るので、大きな表では避けたほうが無難です。

df.to_dicts()
[{'a': 3, 'b': 4.0, 'g': 'A'},
 {'a': 3, 'b': 12.0, 'g': 'B'},
 {'a': 3, 'b': 6.0, 'g': 'A'},
 {'a': 4, 'b': 7.0, 'g': 'B'}]

to_arrow()メソッド#

to_arrow() で pyarrow の Table に変換します。Table は DataFrame と同じメモリを共有するため、変換は高速です。

df.to_arrow()
pyarrow.Table
a: int64
b: double
g: large_string
----
a: [[3,3,3,4]]
b: [[4,12,6,7]]
g: [["A","B","A","B"]]

行を取得#

  • DataFrame.row(index) — 特定のインデックスにある行をタプルとして取得します。

  • DataFrame.rows() — すべての行をタプルのリストとして取得します。

  • DataFrame.iter_rows() — 各行を順にたどり、タプルとして返します。

  • DataFrame.rows_by_key() — 指定したキーで行をグループ化し、グループごとに行のリストを返します。

DataFrame.row(index) は、指定したインデックス(行番号)の行をタプルで返します。

df.row(2)
(3, 6.0, 'A')

named=True を渡すと、列名を含む辞書として返します。

df.row(2, named=True)
{'a': 3, 'b': 6.0, 'g': 'A'}

by_predicate に条件式を渡すと、条件が True になる行を取得できます。条件に合う行が複数あるとエラーになります。

df.row(by_predicate=pl.col.a == pl.col.a.max())
(4, 7.0, 'B')

DataFrame.rows() は、すべての行をタプルのリストとして返します。データ全体を行単位で扱いたいときに便利です。DataFrame.iter_rows() は、各行を順に取り出すイテレーターを返します。

df.rows()
[(3, 4.0, 'A'), (3, 12.0, 'B'), (3, 6.0, 'A'), (4, 7.0, 'B')]
df.rows(named=True)
[{'a': 3, 'b': 4.0, 'g': 'A'},
 {'a': 3, 'b': 12.0, 'g': 'B'},
 {'a': 3, 'b': 6.0, 'g': 'A'},
 {'a': 4, 'b': 7.0, 'g': 'B'}]
for row_ in df.iter_rows():
    print(row_)
(3, 4.0, 'A')
(3, 12.0, 'B')
(3, 6.0, 'A')
(4, 7.0, 'B')

DataFrame.rows_by_key() は、指定したキー(列)で行をグループ化し、キーごとに行のリストを返します。

df.rows_by_key('g')
defaultdict(list, {'A': [(3, 4.0), (3, 6.0)], 'B': [(3, 12.0), (4, 7.0)]})
df.rows_by_key('g', named=True)
defaultdict(list,
            {'A': [{'a': 3, 'b': 4.0}, {'a': 3, 'b': 6.0}],
             'B': [{'a': 3, 'b': 12.0}, {'a': 4, 'b': 7.0}]})

単一の値#

DataFrame.item() は、データフレームから単一の値を取り出すメソッドです。

df.item(2, 'a')
3

データフレームに値が1つしかない場合は、引数を省略できます。列の統計値を取り出して別の計算に使うときによく用います。

df.select(pl.col('a').mean()).item() / 100
0.0325

[] によるインデックス操作#

Polars のデータフレームは、NumPy や Pandas と同じように [] によるインデックス操作に対応しています。

警告

[] によるインデックス操作は可能ですが、推奨される方法ではありません。filter() や select() などのメソッドを式と組み合わせて使うほうが望ましいです。

次の例では、サンプルデータを使ってインデックス操作を説明します。

df = pl.DataFrame({
    "a": [1, 2, 3, 4],
    "b": [5, 6, 7, 8],
    "c": ["x", "y", "z", "w"],
    "d": ["a", "b", "c", "d"]
})

要素の取得#

インデックスが1つの場合

インデックスが1つだけ指定される場合、その型によって挙動が異なります。

  1. 整数、整数のリスト、整数のスライスの場合 — 行を選択します。スライスの場合、終了値は含まれません。

row(df[0], df[[0, 2]], df[1:3])
shape: (1, 4)
abcd
i64i64strstr
15"x""a"
shape: (2, 4)
abcd
i64i64strstr
15"x""a"
37"z""c"
shape: (2, 4)
abcd
i64i64strstr
26"y""b"
37"z""c"
  1. 文字列、文字列のリスト、文字列のスライスの場合 — 列を選択します。スライスの場合、終了値が含まれます。

row(df["a"], df[["a", "d"]], df["a":"c"])
shape: (4,)
a
i64
1
2
3
4
shape: (4, 2)
ad
i64str
1"a"
2"b"
3"c"
4"d"
shape: (4, 3)
abc
i64i64str
15"x"
26"y"
37"z"
48"w"

インデックスが2つの場合

インデックスが2つ指定される場合、1つ目は行の選択、2つ目は列の選択に使われます。

  • 行のインデックスには整数、整数のリスト、またはスライスを使用します。

  • 列のインデックスには整数、文字列、整数のリスト、文字列のリスト、またはスライスを使用できます。

  1. 両方が単一の値の場合 — 特定の要素を抽出します。結果は Python の基本データ型になります。

print(f"{df[0, 'b'] = }")  # 行0、列"b"の値を抽出
print(f"{df[0, 1] = }")    # 行0、列インデックス1の値を抽出
df[0, 'b'] = 5
df[0, 1] = 5
  1. 列のインデックスがリストやスライスの場合 — 結果は DataFrame になります。

row(df[0, ["a", "c"]], df[1, "a":"c"], df[2, [0, 1, 3]], df[3, ::2])
shape: (1, 2)
ac
i64str
1"x"
shape: (1, 3)
abc
i64i64str
26"y"
shape: (1, 3)
abd
i64i64str
37"c"
shape: (1, 2)
ac
i64str
4"w"
row(df[[1, 2], ["a", "c"]], df[[1, 2], "a":"c"], df[2:, [0, 1, 3]], df[2:, ::2])
shape: (2, 2)
ac
i64str
2"y"
3"z"
shape: (2, 3)
abc
i64i64str
26"y"
37"z"
shape: (2, 3)
abd
i64i64str
37"c"
48"d"
shape: (2, 2)
ac
i64str
3"z"
4"w"
  1. 行のインデックスがリストやスライスで、列のインデックスが単独の要素の場合 — Series が返されます。Series は、選択された列の行データを保持します。

row(df[[1, 2], "a"], df[1:, 2])
shape: (2,)
a
i64
2
3
shape: (3,)
c
str
"y"
"z"
"w"

[] でもデータを書き換えられます。方法は2つあります。

  1. 列をまとめて置き換える — 列名のリストを指定し、同じ形の NumPy 配列を代入します。

# 列"a"のデータを設定
df[["a"]] = np.array([[10], [20], [30], [40]])
df
shape: (4, 4)
abcd
i64i64strstr
105"x""a"
206"y""b"
307"z""c"
408"w""d"
# 列"a"と列"b"のデータを同時に設定
df[["a", "b"]] = np.array([[100, 200, 300, 400], [50, 60, 70, 80]]).T
df
shape: (4, 4)
abcd
i64i64strstr
10050"x""a"
20060"y""b"
30070"z""c"
40080"w""d"
  1. 行・列を指定して特定の要素を変更する場合

  • 行のインデックス: 整数または整数のリストで指定します。

  • 列のインデックス: 整数または文字列で指定します。

この書き方は、わずかな値を直すときのものです。代入のたびに列全体がコピーされることがあり、ループで数千個を書き換えると極端に遅くなります。まとめて変更するなら with_columns() と when().then() を使ってください。

# 単一の行と列のデータを変更
df[1, "a"] = -1
df
shape: (4, 4)
abcd
i64i64strstr
10050"x""a"
-160"y""b"
30070"z""c"
40080"w""d"
# 複数の行のデータを変更
df[[1, 3], "b"] = -1, -2
df
shape: (4, 4)
abcd
i64i64strstr
10050"x""a"
-1-1"y""b"
30070"z""c"
400-2"w""d"