ファイルの入出力#
Polars はファイルの読み書きが速く、CSV・Excel・JSON・Parquet・IPC といった形式をひととおり扱えます。この章では、実際によく使う読み込みオプションと、書き出しのしかたを順に見ていきます。
import polars as pl
from helper.jupyter import row, capture_except
CSV ファイル#
CSV は形式が緩いぶん、ヘッダーの有無、型の推定、余分な空白など、読み込み前に決めておくことが多くあります。ここでは代表的なオプションを、実際のデータで確認します。
読み込み#
ヘッダー#
CSV にはヘッダーがあるものとないものがあり、ヘッダーが2行にまたがることもあります。次のデータを例に、ヘッダーの扱い方を確認します。
%%writefile data/csv_header.csv
A,B
a,b
0,1
2,3
4,5
Overwriting data/csv_header.csv
df1— 既定では1行目をヘッダーとして読み込みます。df2—has_header=Falseにすると1行目もデータになります。列名はnew_columnsで指定します。df3—skip_rowsで先頭の N 行を読み飛ばします。df4—skip_rows_after_headerでヘッダーの次の N 行を読み飛ばします。df5— 先頭2行をヘッダーなしで読み、列ごとに連結して列名を作ります。2行のヘッダーをまとめたいときに使います。
fn = 'data/csv_header.csv'
df1 = pl.read_csv(fn)
df2 = pl.read_csv(fn, has_header=False, new_columns=['x', 'y'])
df3 = pl.read_csv(fn, skip_rows=1)
df4 = pl.read_csv(fn, skip_rows_after_header=1)
df_header = pl.read_csv(fn, n_rows=2, has_header=False)
columns = df_header.select(pl.all().str.join('-')).row(0)
df5 = pl.read_csv(fn, has_header=False, skip_rows=2, new_columns=columns)
row(df1, df2, df3, df4, df5)
shape: (4, 2)
|
shape: (5, 2)
|
shape: (3, 2)
|
shape: (3, 2)
|
shape: (3, 2)
|
infer_schema=True(既定)のとき、Polars は先頭の infer_schema_length 行だけを見て各列の型を決めます。その範囲の外に違う型の値があるとエラーになります。次のデータで確認します。
%%writefile data/csv_different_type.csv
A,B
0,1
2,3
4,5
a,5.5
10,20
Overwriting data/csv_different_type.csv
infer_schema_length の既定値は100です。次のコードでは2行に絞るため、4行目の a でエラーになります。
%%capture_except
df = pl.read_csv('data/csv_different_type.csv', infer_schema_length=2)
ComputeError: could not parse `a` as dtype `i64` at column 'A' (column number 1)
The current offset in the file is 15 bytes.
You might want to try:
- increasing `infer_schema_length` (e.g. `infer_schema_length=10000`),
- increasing `infer_schema_files` (e.g. `infer_schema_files=50`),
- specifying correct dtype with the `schema_overrides` argument
- setting `ignore_errors` to `True`,
- adding `a` to the `null_values` list.
Original error: ```invalid primitive value found during CSV parsing```
エラーメッセージには解決方法がいくつか示されます。次のコードはそれらを順に試したものです。
df1—infer_schema_lengthを増やし、Aをstr、Bをf64として読む。df2—infer_schema_length=Noneですべての行から型を推定し、null_values=['a']でaを NULL 扱いにしてAをi64にする。df3—ignore_errors=Trueで、推定した型に合わない値を NULL にする。df4—schema_overridesで型を明示し、ignore_errors=Trueで外れた値を捨てる。型を決め打ちできるぶん、メモリも節約できます。
fn = 'data/csv_different_type.csv'
df1 = pl.read_csv(fn, infer_schema_length=1000)
df2 = pl.read_csv(fn, infer_schema_length=None, null_values=['a'])
df3 = pl.read_csv(fn, infer_schema_length=2, ignore_errors=True)
df4 = pl.read_csv(fn, schema_overrides={'A':pl.Int16, 'B':pl.Float32}, ignore_errors=True)
row(df1, df2, df3, df4)
shape: (5, 2)
|
shape: (5, 2)
|
shape: (5, 2)
|
shape: (5, 2)
|
列の値の末尾に空白があると、Polars の標準エンジンはそれをそのまま残し、列を str と判定します。次のデータで確認します。
%%writefile data/csv_trailing_space.csv
str,int,float
日本語 ,4 ,5.67
abc ,5 ,1.23
Overwriting data/csv_trailing_space.csv
このデータは、標準エンジンと use_pyarrow=True で結果が変わります。
df1— 標準エンジンでは、すべての列がstrになります。末尾に空白があるため数値と判定できないからです。df2—use_pyarrow=Trueにすると、intとfloatの列が数値として解釈されます。PyArrow のパーサーが空白を自動で落とします。
どちらを選ぶかはデータの状態で決めます。書式が整っていれば標準エンジン、崩れていれば PyArrow のほうが寛容です。
fn = 'data/csv_trailing_space.csv'
df1 = pl.read_csv(fn)
df2 = pl.read_csv(fn, use_pyarrow=True)
row(df1, df2)
shape: (2, 3)
|
shape: (2, 3)
|
標準エンジンで読んだ文字列を数値にしたいときは、自前で変換します。空白を落としてから、整数 → 小数 → 元の文字列の順に試す関数を用意します。
from polars.exceptions import InvalidOperationError
def try_cast_to_number(s: pl.Series) -> pl.Series:
"""空白を落として、Int64 → Float64 → 元の文字列 の順に変換を試す。"""
stripped = s.str.strip_chars()
for dtype in (pl.Int64, pl.Float64):
try:
return stripped.cast(dtype)
except InvalidOperationError:
continue
return stripped
# 列ごとに型が変わるので、列単位で適用してから 1 つのフレームにまとめる
df_converted = pl.DataFrame({name: try_cast_to_number(df1[name]) for name in df1.columns})
row(df1, df_converted)
shape: (2, 3)
|
shape: (2, 3)
|
複数のファイルを読み込み#
ストック価格の CSV をまとめて読み込む例です。まず data/stock にデータをダウンロードします。
from helper.utils import download_folder_from_github, print_folder_structure
download_folder_from_github("https://github.com/jeroenjanssens/python-polars-the-definitive-guide/tree/main/data/stock", "data/stock")
data/stock exists, please delete and try again.
ダウンロードしたフォルダは、銘柄ごとのサブフォルダに年ごとの CSV が入った構造です。
print_folder_structure("data/stock", max_file_count=3)
|-- asml
|-- 1999.csv
|-- 2000.csv
|-- 2001.csv
|-- ...
|-- nvda
|-- 1999.csv
|-- 2000.csv
|-- 2001.csv
|-- ...
|-- tsm
|-- 1999.csv
|-- 2000.csv
|-- 2001.csv
|-- ...
すべての CSV をまとめて読むには、パスにワイルドカードを使います。
df_stock = pl.read_csv('data/stock/**/*.csv', try_parse_dates=True)
df_stock
| symbol | date | open | high | low | close | adj close | volume |
|---|---|---|---|---|---|---|---|
| str | date | f64 | f64 | f64 | f64 | f64 | i64 |
| "ASML" | 1999-01-04 | 11.765625 | 12.28125 | 11.765625 | 12.140625 | 7.522523 | 1801867 |
| "ASML" | 1999-01-05 | 11.859375 | 14.25 | 11.71875 | 13.96875 | 8.655257 | 8241600 |
| "ASML" | 1999-01-06 | 14.25 | 17.601563 | 14.203125 | 16.875 | 10.456018 | 16400267 |
| "ASML" | 1999-01-07 | 14.742188 | 17.8125 | 14.53125 | 16.851563 | 10.441495 | 17722133 |
| "ASML" | 1999-01-08 | 16.078125 | 16.289063 | 15.023438 | 15.796875 | 9.787995 | 10696000 |
| … | … | … | … | … | … | … | … |
| "TSM" | 2023-06-26 | 102.019997 | 103.040001 | 100.089996 | 100.110001 | 99.125954 | 8560000 |
| "TSM" | 2023-06-27 | 101.150002 | 102.790001 | 100.019997 | 102.080002 | 101.076591 | 9732000 |
| "TSM" | 2023-06-28 | 100.5 | 101.879997 | 100.220001 | 100.919998 | 99.927986 | 8160900 |
| "TSM" | 2023-06-29 | 101.339996 | 101.519997 | 100.019997 | 100.639999 | 99.650742 | 7383900 |
| "TSM" | 2023-06-30 | 101.400002 | 101.889999 | 100.410004 | 100.919998 | 99.927986 | 11701700 |
このデータには銘柄名の列があるため、ファイル名を列にする必要はありません。どのファイルから来た行かを知りたいときは include_file_paths で列名を指定します。
Polars 2.0 では read_csv() もこの引数を受け付けますが、ここでは遅延評価の scan_csv() を使います。LazyFrame が返るため、中身を見るときは collect() を呼びます。
df_stock = pl.scan_csv('data/stock/**/*.csv', try_parse_dates=True, include_file_paths="path")
df_stock
NAIVE QUERY PLAN
run LazyFrame.show_graph() to see the optimized version
df_stock.collect()
| symbol | date | open | high | low | close | adj close | volume | path |
|---|---|---|---|---|---|---|---|---|
| str | date | f64 | f64 | f64 | f64 | f64 | i64 | str |
| "ASML" | 1999-01-04 | 11.765625 | 12.28125 | 11.765625 | 12.140625 | 7.522523 | 1801867 | "data/stock/asml/1999.csv" |
| "ASML" | 1999-01-05 | 11.859375 | 14.25 | 11.71875 | 13.96875 | 8.655257 | 8241600 | "data/stock/asml/1999.csv" |
| "ASML" | 1999-01-06 | 14.25 | 17.601563 | 14.203125 | 16.875 | 10.456018 | 16400267 | "data/stock/asml/1999.csv" |
| "ASML" | 1999-01-07 | 14.742188 | 17.8125 | 14.53125 | 16.851563 | 10.441495 | 17722133 | "data/stock/asml/1999.csv" |
| "ASML" | 1999-01-08 | 16.078125 | 16.289063 | 15.023438 | 15.796875 | 9.787995 | 10696000 | "data/stock/asml/1999.csv" |
| … | … | … | … | … | … | … | … | … |
| "TSM" | 2023-06-26 | 102.019997 | 103.040001 | 100.089996 | 100.110001 | 99.125954 | 8560000 | "data/stock/tsm/2023.csv" |
| "TSM" | 2023-06-27 | 101.150002 | 102.790001 | 100.019997 | 102.080002 | 101.076591 | 9732000 | "data/stock/tsm/2023.csv" |
| "TSM" | 2023-06-28 | 100.5 | 101.879997 | 100.220001 | 100.919998 | 99.927986 | 8160900 | "data/stock/tsm/2023.csv" |
| "TSM" | 2023-06-29 | 101.339996 | 101.519997 | 100.019997 | 100.639999 | 99.650742 | 7383900 | "data/stock/tsm/2023.csv" |
| "TSM" | 2023-06-30 | 101.400002 | 101.889999 | 100.410004 | 100.919998 | 99.927986 | 11701700 | "data/stock/tsm/2023.csv" |
CSV でないテキストでも、1行を 1列として読み込めば、Polars の文字列操作で解析できます。次の variables_dump.txt は、1行に複数の「変数 = 値」が並んだダンプです。
%%writefile data/variables_dump.txt
A = 1, B = 2, C = 3.23
A = 11, B = 22, C = 33.23
B = 30, C = 20.0
D = 10, A = 3
Overwriting data/variables_dump.txt
読み込むときの separator にヌル文字 "\x00" を指定すると、行が分割されず、各行がまるごと 1つの値になります。そのあと次の操作で表に組み直します。
.str.split(",")— 行を,で分割する.explode("s")— 分割したリストを行に展開する.str.extract_groups(...)— 正規表現で変数名(name)と値(value)を取り出す.pivot(...)— 変数名を列にし、1行を 1サンプルとして並べ直す.cast(pl.Float64)— 値を浮動小数点にする
最後に、小数部分がすべて 0の列を見つけて Int64 に戻します。
df = pl.read_csv(
"data/variables_dump.txt", has_header=False, separator="\x00", new_columns=["s"]
)
df_vars = (
df.select(pl.col("s").str.split(","))
.with_row_index()
.explode("s") # 1 セル 1 変数にする
.with_columns(
pl.col("s").str.extract_groups(r"(?<name>\w)+\s*=\s*(?<value>.+)")
)
.unnest("s") # name / value の 2 列に分ける
.pivot(on="name", index="index")
.with_columns(pl.exclude("index").cast(pl.Float64))
)
# 小数部分が常に 0 の列は Int64 に戻す
int_cols = (
df_vars.select((pl.col(pl.Float64) % 1 == 0).all())
.transpose(include_header=True)
.filter("column_0")
.get_column("column")
.to_list()
)
df_vars = df_vars.with_columns(pl.col(int_cols).cast(pl.Int64))
row(df, df_vars)
shape: (4, 1)
|
shape: (4, 5)
|
書き出し#
DataFrame.write_csv() を使うと、データフレームを CSV ファイルとして出力できます。
df1.write_csv('data/csv_output_utf8.csv')
write_csv() では include_header、separator、line_terminator などを指定できますが、文字コードを直接指定する引数はありません。Shift-JIS で書き出したいときは、いったん StringIO に書いてから、その中身を変換して保存します。
import io
buf = io.StringIO()
df1.write_csv(buf)
with open('data/csv_output_shiftjis.csv', 'w', encoding='shift-jis') as f:
f.write(buf.getvalue())
Excel ファイル#
read_excel() と DataFrame.write_excel() で Excel ファイルの入出力を行います。読み込みには calamine、xlsx2csv、openpyxl の3つのエンジンが使え、既定の calamine がもっとも高速です。書き出しには xlsxwriter を使います。次のコマンドで必要なライブラリをインストールします。
conda install fastexcel xlsxwriter
読み込み#
read_excel() でシートを指定して読み込みます。sheet_id には1から始まるシート番号、またはそのリストを渡します。0 を渡すとすべてのシートを読み、{シート名: DataFrame} の辞書を返します。sheet_names でシート名を指定することもできます。
fn = 'data/xlsx_example.xlsx'
df1, df2 = pl.read_excel(fn, sheet_id=[1, 2]).values()
row(df1, df2)
shape: (3, 2)
|
shape: (3, 3)
|
df1, df2, df3 = pl.read_excel(fn, sheet_id=0).values()
row(df1, df2, df3)
shape: (3, 2)
|
shape: (3, 3)
|
shape: (4, 2)
|
Sheet3 は 2行のヘッダーを持つため、そのまま読むと 2行目がデータ扱いになり、すべての列が文字列になります。read_options でエンジンに渡す設定を変えれば解決できます。
次のコードは、1回目の読み込みで先頭 2行だけをデータとして取り出して列名を組み立て、2回目でヘッダーを読み飛ばしてその列名を適用します。
df_header = pl.read_excel(fn, sheet_id=3, read_options=dict(n_rows=2, header_row=None))
columns = df_header.select(pl.all().str.join('-')).row(0)
df3 = pl.read_excel(fn, sheet_id=3, read_options=dict(skip_rows=2, column_names=columns))
df3
| A-a | B-b |
|---|---|
| i64 | i64 |
| 0 | 1 |
| 2 | 3 |
| 4 | 5 |
calamine エンジンに渡せる引数は、ExcelReader.load_sheet? で確認できます。
from fastexcel import ExcelReader
ExcelReader.load_sheet?
書き出し#
DataFrame.write_excel() を使うと、指定したファイル名とシート名にデータを書き出せます。複数のデータフレームを同じファイルの別々のシートに書き出す場合は、次のコードのように Workbook オブジェクトを作り、そのオブジェクトに対して .write_excel() を複数回呼びます。
import xlsxwriter
with xlsxwriter.Workbook("data/xlsx_example_output.xlsx") as wb:
df1.write_excel(wb, "df1")
df2.write_excel(wb, "df2")
df3.write_excel(wb, "df3")
JSON は入れ子をそのまま表せるので、Web API の応答や設定ファイルでよく使われます。read_json() に bytes をそのまま渡せます。
json_data1 = b'''
[
{"color":"red","value":"#f00"},
{"color":"green","value":"#0f0"},
{"color":"blue","value":"#00f"}
]
'''
df = pl.read_json(json_data1)
df
| color | value |
|---|---|
| str | str |
| "red" | "#f00" |
| "green" | "#0f0" |
| "blue" | "#00f" |
1行に 1つの JSON オブジェクトを並べた形式が NDJSON です。こちらは read_ndjson() で読みます。
ndjson_data = b'''
{"color":"red","value":"#f00"}
{"color":"green","value":"#0f0"}
{"color":"blue","value":"#00f"}
'''
df = pl.read_ndjson(ndjson_data)
df
| color | value |
|---|---|
| str | str |
| "red" | "#f00" |
| "green" | "#0f0" |
| "blue" | "#00f" |
JSON をデータフレームにするときのルールは2つです。
JSON は辞書のリストとして扱われ、各辞書が 1行、キーが列になります。最上位が辞書なら、1要素のリストとして扱います。
入れ子のリストは
List型、入れ子の辞書はStruct型になります。
次のデータを読み込むと、id、type、name、ppu、topping の列を持つ3行のデータフレームになります。topping は「構造体のリスト」(List[Struct])です。
json_data2 = b'''
[
{
"id": "0001",
"type": "donut",
"name": "Cake",
"ppu": 0.55,
"topping":
[
{ "id": "5001", "type": "None" },
{ "id": "5002", "type": "Glazed" },
{ "id": "5005", "type": "Sugar" },
{ "id": "5007", "type": "Powdered Sugar" },
{ "id": "5006", "type": "Chocolate with Sprinkles" },
{ "id": "5003", "type": "Chocolate" },
{ "id": "5004", "type": "Maple" }
]
},
{
"id": "0002",
"type": "donut",
"name": "Raised",
"ppu": 0.55,
"topping":
[
{ "id": "5001", "type": "None" },
{ "id": "5002", "type": "Glazed" },
{ "id": "5005", "type": "Sugar" },
{ "id": "5003", "type": "Chocolate" },
{ "id": "5004", "type": "Maple" }
]
},
{
"id": "0003",
"type": "donut",
"name": "Old Fashioned",
"ppu": 0.55,
"topping":
[
{ "id": "5001", "type": "None" },
{ "id": "5002", "type": "Glazed" },
{ "id": "5003", "type": "Chocolate" },
{ "id": "5004", "type": "Maple" }
]
}
]
'''
df = pl.read_json(json_data2)
df
| id | type | name | ppu | topping |
|---|---|---|---|---|
| str | str | str | f64 | list[struct[2]] |
| "0001" | "donut" | "Cake" | 0.55 | [{"5001","None"}, {"5002","Glazed"}, … {"5004","Maple"}] |
| "0002" | "donut" | "Raised" | 0.55 | [{"5001","None"}, {"5002","Glazed"}, … {"5004","Maple"}] |
| "0003" | "donut" | "Old Fashioned" | 0.55 | [{"5001","None"}, {"5002","Glazed"}, … {"5004","Maple"}] |
topping 列のデータ型は次のとおりです。
print(df.schema['topping'])
List(Struct({'id': String, 'type': String}))
入れ子が扱いにくいときは、explode() でリストを行に、struct.unnest() で構造体を列に展開します。
df.explode('topping').select(
pl.exclude('topping'),
pl.col('topping').struct.unnest().name.prefix('topping_')
)
| id | type | name | ppu | topping_id | topping_type |
|---|---|---|---|---|---|
| str | str | str | f64 | str | str |
| "0001" | "donut" | "Cake" | 0.55 | "5001" | "None" |
| "0001" | "donut" | "Cake" | 0.55 | "5002" | "Glazed" |
| "0001" | "donut" | "Cake" | 0.55 | "5005" | "Sugar" |
| "0001" | "donut" | "Cake" | 0.55 | "5007" | "Powdered Sugar" |
| "0001" | "donut" | "Cake" | 0.55 | "5006" | "Chocolate with Sprinkles" |
| … | … | … | … | … | … |
| "0002" | "donut" | "Raised" | 0.55 | "5004" | "Maple" |
| "0003" | "donut" | "Old Fashioned" | 0.55 | "5001" | "None" |
| "0003" | "donut" | "Old Fashioned" | 0.55 | "5002" | "Glazed" |
| "0003" | "donut" | "Old Fashioned" | 0.55 | "5003" | "Chocolate" |
| "0003" | "donut" | "Old Fashioned" | 0.55 | "5004" | "Maple" |
元のデータフレームを3つの表に分けてから結合し直す方法もあります。本体(ドーナツ)の表、具材(重複を除いた topping)の表、両者の対応を表す表です。
これはリレーショナルモデリングの定石で、データ量が多く、具材の情報を再利用したいときは、平坦化するより容量を抑えられます。
df_topping = (
df.select(
pl.col('topping').explode().unique().struct.unnest().name.prefix(f'topping_')
)
.sort(by=f'topping_id')
)
df_donut = df.drop('topping')
df_donut_topping = (
df.select(
'id',
topping_id = pl.col('topping').list.eval(
pl.element().struct.field('id')
)
)
.explode(f'topping_id')
)
row(df_donut, df_donut_topping, df_topping)
shape: (3, 4)
|
shape: (16, 2)
|
shape: (7, 2)
|
分けた3つの表は、join() で1つに戻せます。
df_donut.join(df_donut_topping, on='id').join(df_topping, on='topping_id')
| id | type | name | ppu | topping_id | topping_type |
|---|---|---|---|---|---|
| str | str | str | f64 | str | str |
| "0001" | "donut" | "Cake" | 0.55 | "5001" | "None" |
| "0001" | "donut" | "Cake" | 0.55 | "5002" | "Glazed" |
| "0001" | "donut" | "Cake" | 0.55 | "5005" | "Sugar" |
| "0001" | "donut" | "Cake" | 0.55 | "5007" | "Powdered Sugar" |
| "0001" | "donut" | "Cake" | 0.55 | "5006" | "Chocolate with Sprinkles" |
| … | … | … | … | … | … |
| "0002" | "donut" | "Raised" | 0.55 | "5004" | "Maple" |
| "0003" | "donut" | "Old Fashioned" | 0.55 | "5001" | "None" |
| "0003" | "donut" | "Old Fashioned" | 0.55 | "5002" | "Glazed" |
| "0003" | "donut" | "Old Fashioned" | 0.55 | "5003" | "Chocolate" |
| "0003" | "donut" | "Old Fashioned" | 0.55 | "5004" | "Maple" |
もう少し複雑な例として、ニューヨーク市の地区ポリゴン(GeoJSON)を読んでみます。
df = pl.read_json("data/nyc-neighborhoods.geojson")
df
| type | crs | features |
|---|---|---|
| str | struct[2] | list[struct[3]] |
| "FeatureCollection" | {"name",{"urn:ogc:def:crs:OGC:1.3:CRS84"}} | [{"Feature",{"Allerton","2","Bronx","http://nyc.pediacities.com/Resource/Neighborhood/Allerton"},{"Polygon",[[[-73.848597, 40.87167], [-73.845823, 40.870239], … [-73.848597, 40.87167]]]}}, {"Feature",{"Alley Pond Park","4","Queens","http://nyc.pediacities.com/Resource/Neighborhood/Alley_Pond_Park"},{"Polygon",[[[-73.743333, 40.738883], [-73.743714, 40.739403], … [-73.743333, 40.738883]]]}}, … {"Feature",{"Harlem","1","Manhattan",null},{"Polygon",[[[-73.934568, 40.828149], [-73.934424, 40.827043], … [-73.934568, 40.828149]]]}}] |
このファイルは最上位が 1つの辞書であるため、結果は1行のデータフレームになります。地理情報はすべて features 列に入っています。
print(df.schema['features'])
List(Struct({'type': String, 'properties': Struct({'neighborhood': String, 'boroughCode': String, 'borough': String, 'X.id': String}), 'geometry': Struct({'type': String, 'coordinates': List(List(List(Float64)))})}))
複雑にネストした構造を読みやすくするため、本書では dtype_pretty_format() 関数を用意しています。この関数を使うと、ネストしたデータ型を見やすい形式で表示できます。
from helper.polars import dtype_pretty_format
print(dtype_pretty_format(df.schema['features']))
List(
Struct(
type: String
properties:
Struct(
neighborhood: String
boroughCode: String
borough: String
X.id: String
)
geometry:
Struct(
type: String
coordinates:
List(
List(
List(Float64)
)
)
)
)
)
features は properties と geometry という 2つの構造体を要素に持つリストです。次のコードは、それぞれのフィールドを列に展開し、リストの各要素を 1行にします。
coordinates は三重のリストですが、いちばん外側に要素が 1つしかないので、その階層を落とします。
df_nb = (
df
.select(
pl.col('features').list.explode().struct.unnest()
)
.select(
pl.col('properties', 'geometry').struct.unnest()
)
.with_columns(
pl.col('coordinates').list.get(0)
)
)
df_nb
| neighborhood | boroughCode | borough | X.id | type | coordinates |
|---|---|---|---|---|---|
| str | str | str | str | str | list[list[f64]] |
| "Allerton" | "2" | "Bronx" | "http://nyc.pediacities.com/Res… | "Polygon" | [[-73.848597, 40.87167], [-73.845823, 40.870239], … [-73.848597, 40.87167]] |
| "Alley Pond Park" | "4" | "Queens" | "http://nyc.pediacities.com/Res… | "Polygon" | [[-73.743333, 40.738883], [-73.743714, 40.739403], … [-73.743333, 40.738883]] |
| "Arden Heights" | "5" | "Staten Island" | "http://nyc.pediacities.com/Res… | "Polygon" | [[-74.169827, 40.561078], [-74.169823, 40.561057], … [-74.169827, 40.561078]] |
| "Arlington" | "5" | "Staten Island" | "http://nyc.pediacities.com/Res… | "Polygon" | [[-74.159748, 40.641417], [-74.159979, 40.641446], … [-74.159748, 40.641417]] |
| "Arrochar" | "5" | "Staten Island" | "http://nyc.pediacities.com/Res… | "Polygon" | [[-74.06078, 40.593188], [-74.060792, 40.593178], … [-74.06078, 40.593188]] |
| … | … | … | … | … | … |
| "Woodside" | "4" | "Queens" | "http://nyc.pediacities.com/Res… | "Polygon" | [[-73.900866, 40.757674], [-73.90014, 40.75615], … [-73.900866, 40.757674]] |
| "East Williamsburg" | "3" | "Brooklyn" | null | "Polygon" | [[-73.9413, 40.7218], [-73.9406, 40.7186], … [-73.9413, 40.7218]] |
| "Hamilton Heights" | "1" | "Manhattan" | null | "Polygon" | [[-73.9505, 40.8349], [-73.9411, 40.8312], … [-73.9505, 40.8349]] |
| "Williamsburg" | "3" | "Brooklyn" | null | "Polygon" | [[-73.957572, 40.725097], [-73.952998, 40.722253], … [-73.957572, 40.725097]] |
| "Harlem" | "1" | "Manhattan" | null | "Polygon" | [[-73.934568, 40.828149], [-73.934424, 40.827043], … [-73.934568, 40.828149]] |
holoviews の Polygons で描くには、経度と緯度のリストを別々の列に分けます。list.eval() はリストの各要素に対する式で、pl.element() がその要素、list.get() が入れ子のリストからの取り出しです。
df_nb2 = df_nb.select(
pl.col('neighborhood'),
pl.col('borough'),
pl.col('coordinates').list.eval(pl.element().list.get(0)).alias('lng'),
pl.col('coordinates').list.eval(pl.element().list.get(1)).alias('lat'),
)
hv.Polygons(df_nb2.to_dicts(), kdims=['lng', 'lat'], vdims=['borough']).opts(cmap='tab10', data_aspect=1)
バイナリ形式として、Polars は IPC(Arrow IPC)と Parquet をサポートしています。
IPC — プロセス間で受け渡すためのバイナリ形式です。シリアライズとデシリアライズが速く、大きなデータをそのまま渡せます。
Parquet — 列指向の保存形式です。圧縮効率がよく、読み込み時に必要な列だけを取り出せます。Spark などの分散処理でも標準的に使われます。
DataFrame は read_*() / write_*()、LazyFrame は scan_*() / sink_*() を使います。
read_ipc()/read_parquet()— ファイルからDataFrameを読むwrite_ipc()/write_parquet()—DataFrameをファイルに書くscan_ipc()/scan_parquet()— ファイルをLazyFrameとして読むsink_ipc()/sink_parquet()—LazyFrameをチャンク単位で書き出す
sink_*() はデータを一度にメモリへ載せず、チャンクで処理するので、メモリに収まらない規模でも扱えます。次のコードは、複数の CSV を 1つの IPC ファイルに変換します。
df_stock = pl.scan_csv('data/stock/**/*.csv', try_parse_dates=True)
df_stock.sink_ipc('data/stock.arrow')
df_stock = pl.read_ipc('data/stock.arrow')
df_stock
| symbol | date | open | high | low | close | adj close | volume |
|---|---|---|---|---|---|---|---|
| str | date | f64 | f64 | f64 | f64 | f64 | i64 |
| "ASML" | 1999-01-04 | 11.765625 | 12.28125 | 11.765625 | 12.140625 | 7.522523 | 1801867 |
| "ASML" | 1999-01-05 | 11.859375 | 14.25 | 11.71875 | 13.96875 | 8.655257 | 8241600 |
| "ASML" | 1999-01-06 | 14.25 | 17.601563 | 14.203125 | 16.875 | 10.456018 | 16400267 |
| "ASML" | 1999-01-07 | 14.742188 | 17.8125 | 14.53125 | 16.851563 | 10.441495 | 17722133 |
| "ASML" | 1999-01-08 | 16.078125 | 16.289063 | 15.023438 | 15.796875 | 9.787995 | 10696000 |
| … | … | … | … | … | … | … | … |
| "TSM" | 2023-06-26 | 102.019997 | 103.040001 | 100.089996 | 100.110001 | 99.125954 | 8560000 |
| "TSM" | 2023-06-27 | 101.150002 | 102.790001 | 100.019997 | 102.080002 | 101.076591 | 9732000 |
| "TSM" | 2023-06-28 | 100.5 | 101.879997 | 100.220001 | 100.919998 | 99.927986 | 8160900 |
| "TSM" | 2023-06-29 | 101.339996 | 101.519997 | 100.019997 | 100.639999 | 99.650742 | 7383900 |
| "TSM" | 2023-06-30 | 101.400002 | 101.889999 | 100.410004 | 100.919998 | 99.927986 | 11701700 |
上の関数は 1ファイル 1フレームですが、write_ipc_stream() / read_ipc_stream() を使うと、複数のフレームを順番に 1つのファイルへ書けます。次のコードは 2つのフレームを書き出す例です。
df1 = pl.DataFrame({"a":[1, 2, 3], "b":["xx", "yyy", "zzzz"]})
df2 = pl.DataFrame({"x":[3.0, 4.0, 6.0, 7.0], "y":[1.0, 2.0, 3.0, 4.0], "z":[0.0, 0.1, 0.2, 0.3]})
with open('data/stream_test.arrow', 'wb') as f:
df1.write_ipc_stream(f)
df2.write_ipc_stream(f)
読み込むときは、書き出した順に取り出します。
注釈
Polars 2.0 でも、続けて読み込むには use_pyarrow=True が必要です。既定のままでは2つ目以降が正しく読めません。
with open("data/stream_test.arrow", "rb") as f:
df1_r = pl.read_ipc_stream(f, use_pyarrow=True)
df2_r = pl.read_ipc_stream(f, use_pyarrow=True)
print(df1.equals(df1_r), df2.equals(df2_r))
True True