Polars

[2023-10-13] reverse=Truedescending=True に変更されていましたので修正(Thanks: Nobu C. Shirai先生)。

概要

Polars(ポーラーズ)はRust・Python用のデータ操作ライブラリです。Pythonではpandasを置き換えるものです。日本語の解説として超高速…だけじゃない!Pandasに代えてPolarsを使いたい理由が参考になります。

Polarsのマスコットは白熊(ホッキョクグマ、polar bear)です。パンダより強そうです。実際、pandasより高速で、機能も豊富です。

インストールは pip install polars です。

例(フォントによっては表が崩れます):

import polars as pl

df = pl.read_csv("https://okumuralab.org/~okumura/stat/data/pop2022.csv")
df
shape: (47, 4)
┌──────┬────────────┬─────────┬─────────┐
│ 番号 ┆ 都道府県   ┆ 男      ┆ 女      │
│ ---  ┆ ---        ┆ ---     ┆ ---     │
│ i64  ┆ str        ┆ i64     ┆ i64     │
╞══════╪════════════╪═════════╪═════════╡
│ 1    ┆ 北海道     ┆ 2450393 ┆ 2733294 │
│ 2    ┆ 青森       ┆ 589143  ┆ 653938  │
│ 3    ┆ 岩手       ┆ 581809  ┆ 624670  │
│ 4    ┆ 宮城       ┆ 1106183 ┆ 1162172 │
│ ...  ┆ ...        ┆ ...     ┆ ...     │
│ 44   ┆ 大分       ┆ 538934  ┆ 592206  │
│ 45   ┆ 宮崎       ┆ 511039  ┆ 567274  │
│ 46   ┆ 鹿児島     ┆ 759364  ┆ 846055  │
│ 47   ┆ 沖縄       ┆ 732981  ┆ 752689  │
└──────┴────────────┴─────────┴─────────┘

1列目の都道府県番号は01〜47の文字列として読みたいですね:

df = pl.read_csv("https://okumuralab.org/~okumura/stat/data/pop2022.csv", dtypes=[pl.Utf8])
df
shape: (47, 4)
┌──────┬────────────┬─────────┬─────────┐
│ 番号 ┆ 都道府県   ┆ 男      ┆ 女      │
│ ---  ┆ ---        ┆ ---     ┆ ---     │
│ str  ┆ str        ┆ i64     ┆ i64     │
╞══════╪════════════╪═════════╪═════════╡
│ 01   ┆ 北海道     ┆ 2450393 ┆ 2733294 │
│ 02   ┆ 青森       ┆ 589143  ┆ 653938  │
│ 03   ┆ 岩手       ┆ 581809  ┆ 624670  │
│ 04   ┆ 宮城       ┆ 1106183 ┆ 1162172 │
│ ...  ┆ ...        ┆ ...     ┆ ...     │
│ 44   ┆ 大分       ┆ 538934  ┆ 592206  │
│ 45   ┆ 宮崎       ┆ 511039  ┆ 567274  │
│ 46   ┆ 鹿児島     ┆ 759364  ┆ 846055  │
│ 47   ┆ 沖縄       ┆ 732981  ┆ 752689  │
└──────┴────────────┴─────────┴─────────┘

dtypes はリストで例えば dtypes=[pl.Utf8, pl.Utf8, pl.Int32, pl.Int32] のように列ごとに指定できます。4列全部を文字列にするには dtypes=[pl.Utf8]*4 とします。

型は pl.Utf8(文字列)、pl.Float64pl.Float32pl.Int64pl.Int32pl.Int16pl.Int8pl.UInt64pl.UInt32pl.UInt16pl.UInt8 などが使えます。

Pandasと比べて、0から始まる行インデックスが付きません。

別の例:

URL = "https://www.data.jma.go.jp/cpdinfo/temp/list/csv/an_wld.csv"
df1 = pl.read_csv(URL, encoding="cp932")

これは悪名高き気象庁のCSVファイルで、ときどき数値の後に * が付いてエラーになります。pandasはエラーになったら面倒ですが、Polarsなら次のように回避できます:

df1 = pl.read_csv(URL, encoding="cp932", ignore_errors=True)
df1
shape: (132, 4)
┌──────┬────────────┬─────────┬─────────┐
│ 年   ┆ 世界全体   ┆ 北半球  ┆ 南半球  │
│ ---  ┆ ---        ┆ ---     ┆ ---     │
│ i64  ┆ f64        ┆ f64     ┆ f64     │
╞══════╪════════════╪═════════╪═════════╡
│ 1891 ┆ -0.78      ┆ -0.88   ┆ -0.68   │
│ 1892 ┆ -0.89      ┆ -1.0    ┆ -0.74   │
│ 1893 ┆ -0.94      ┆ -1.06   ┆ -0.79   │
│ 1894 ┆ -0.86      ┆ -0.93   ┆ -0.77   │
│ ...  ┆ ...        ┆ ...     ┆ ...     │
│ 2019 ┆ 0.31       ┆ 0.38    ┆ 0.23    │
│ 2020 ┆ 0.34       ┆ 0.51    ┆ 0.16    │
│ 2021 ┆ 0.22       ┆ 0.35    ┆ 0.09    │
│ 2022 ┆ null       ┆ null    ┆ null    │
└──────┴────────────┴─────────┴─────────┘

得られた df の扱い方は、pandasとやや異なります。例:

df.query("都道府県 == '東京'")          # pandas
df.filter(pl.col("都道府県") == "東京") # Polars  

df.to_pandas()df.to_numpy() でpandasやnumpyに変換することもできます。

pandasによるクエリと同じことをPolarsで行ってみましょう。使うのは、さきほど

df = pl.read_csv("https://okumuralab.org/~okumura/stat/data/pop2022.csv", dtypes=[pl.Utf8])

で読み込んだデータです。まずは東京都だけ選びます:

df.filter(pl.col("都道府県") == "東京")
shape: (1, 4)
┌──────┬────────────┬─────────┬─────────┐
│ 番号 ┆ 都道府県   ┆ 男      ┆ 女      │
│ ---  ┆ ---        ┆ ---     ┆ ---     │
│ str  ┆ str        ┆ i64     ┆ i64     │
╞══════╪════════════╪═════════╪═════════╡
│ 13   ┆ 東京       ┆ 6775557 ┆ 7019376 │
└──────┴────────────┴─────────┴─────────┘

元の df は変わりません。変えたいなら df = df.filter(pl.col("都道府県") == "東京") のように代入し直します(ここでは行いません)。

男と女の列だけ表示させたいなら、次のようにします(これはpandasと同様です):

df.filter(pl.col("都道府県") == "東京")[["男", "女"]]
shape: (1, 2)
┌─────────┬─────────┐
│ 男      ┆ 女      │
│ ---     ┆ ---     │
│ i64     ┆ i64     │
╞═════════╪═════════╡
│ 6775557 ┆ 7019376 │
└─────────┴─────────┘

男女の人口はわかりましたが、合計の人口もほしいですね。「計」という欄を追加しましょう:

df = df.with_columns((pl.col("男") + pl.col("女")).alias("計"))
df.head()  # 頭の部分だけ見る
shape: (5, 5)
┌──────┬────────────┬─────────┬─────────┬─────────┐
│ 番号 ┆ 都道府県   ┆ 男      ┆ 女      ┆ 計      │
│ ---  ┆ ---        ┆ ---     ┆ ---     ┆ ---     │
│ str  ┆ str        ┆ i64     ┆ i64     ┆ i64     │
╞══════╪════════════╪═════════╪═════════╪═════════╡
│ 01   ┆ 北海道     ┆ 2450393 ┆ 2733294 ┆ 5183687 │
│ 02   ┆ 青森       ┆ 589143  ┆ 653938  ┆ 1243081 │
│ 03   ┆ 岩手       ┆ 581809  ┆ 624670  ┆ 1206479 │
│ 04   ┆ 宮城       ┆ 1106183 ┆ 1162172 ┆ 2268355 │
│ 05   ┆ 秋田       ┆ 452370  ┆ 504466  ┆ 956836  │
└──────┴────────────┴─────────┴─────────┴─────────┘

df = df.with_columns(pl.Series(name="計", values=df["男"] + df["女"])) でもよさそうです。

全国の合計も知りたいですね:

df[["男", "女", "計"]].sum()
shape: (1, 3)
┌──────────┬──────────┬───────────┐
│ 男       ┆ 女       ┆ 計        │
│ ---      ┆ ---      ┆ ---       │
│ i64      ┆ i64      ┆ i64       │
╞══════════╪══════════╪═══════════╡
│ 61420626 ┆ 64507276 ┆ 125927902 │
└──────────┴──────────┴───────────┘

この時点での日本の人口は 125927902 人でした。男より女のほうが多いようですね。

都道府県別に男女比が知りたくなりました。「男女比」という欄を追加しましょう。

df = df.with_columns((pl.col("男") / pl.col("女")).alias("男女比"))

男が少ない都道府県、例えば男女比が 90% 未満のデータを抽出しましょう:

df.filter(pl.col("男女比") < 0.9)

&(and)や |(or)で繋げるときは、各不等式を括弧で囲みます:

df.filter((pl.col("男女比") < 0.9) & (pl.col("計") >= 1000000))

抽出されたものは、元の順番(都道府県コード順)に並んでいます。これを男女比の昇順(小さい順)に並べ替えてみます:

df.filter(pl.col("男女比") < 0.9).sort("男女比")

これは次のように分けて書いてもかまいません:

df1 = df.filter(pl.col("男女比") < 0.9)
df1.sort("男女比")

降順(大きい順)にするには descending=True というオプションを付けます:

df.filter(pl.col("男女比") < 0.9).sort("男女比", descending=True)

データ全体を人口の合計でソートし、大きい順に並び替えて、頭の3行を表示しましょう:

df.sort("計", descending=True).head(3)

もう一つデータを読み込んでみます。こちらは各都道府県の面積です:

df2 = pl.read_csv("https://okumuralab.org/~okumura/stat/data/area.csv", dtypes=[pl.Utf8])

dfdf2 を、共通する「番号」「都道府県」で結合(join)し、df に代入します:

df = df.join(df2, on=["番号", "都道府県"])

人口密度を求めてみましょう:

df = df.with_columns((pl.col("計") / pl.col("面積_km2")).alias("人口密度"))

問題:人口密度の大きい順に並べて、上位10件(.head(10))、下位10件(.tail(10))を表示してください。