Slide 1

Slide 1 text

NumPy、Pandas、Polarsの内部構造を AIと一緒に探索してみた件について 株式会社iimon 比嘉 一晃

Slide 2

Slide 2 text

プロフィール ● ● ● ● ● ● X:@higakazuaki9 比嘉 一晃(ひが かずあき) 沖縄県出身 株式会社iimon エンジニア ○ 不動産DXのツールを提供 https://iimon.co.jp/ 担当業務の技術スタック ○ vanilla-tsやReact、Django、 AWS、Google Cloud (Firebase)...etc 経験 ○ Angular、React、Vue.js、Spring Boot、Django、Fast API、Ruby on Rails…etc 性格 ○ 人懐っこい、面倒くさがり、ねこと ビール好き

Slide 3

Slide 3 text

株式会社iimon

Slide 4

Slide 4 text

Agenda はじめに 内部構造の探索:NumPy編 内部構造の探索:Pandas編 内部構造の探索:Polars編 まとめ

Slide 5

Slide 5 text

Agenda はじめに 内部構造の探索:NumPy編 内部構造の探索:Pandas編 内部構造の探索:Polars編 まとめ

Slide 6

Slide 6 text

はじめに モチベーション(心の声) 最近AIが全部コード書いてくれるようになったな・・・ プログラミングとかライブラリとかFWの 構文とかルールとか細かいところまで網羅的に 覚える重要性も薄くなってきてる気がする

Slide 7

Slide 7 text

はじめに モチベーション(心の声) 結構ほかのことに勉強時間使えそうかも💡 どうせなら、システムの仕組みとか どういう思想や工夫があって どうやって動いてるかとか そういうのがわかると楽しそうだな〜

Slide 8

Slide 8 text

はじめに モチベーション(心の声) でもそういうのって 膨大なドキュメントに埋もれてたりするんだよな〜 昔はよくその膨大さと果てしなさに絶望してたっけ・・・

Slide 9

Slide 9 text

はじめに モチベーション(心の声) あれ?でも今は相棒(AI)もついてくれているじゃないか 一緒に探索すれば面白い発見があるかも?

Slide 10

Slide 10 text

はじめに モチベーション ・・・やるか

Slide 11

Slide 11 text

はじめに 注意事項 ・本登壇の探索や実験ではAIを利用しています モデルは明示しませんが、推論能力はそこまで高くないモデルです ・NumPy、Pandas、Polarsの使い方や構造を網羅的には説明しません (時間が圧倒的に足りません) 基本の部分はある程度解説しつつ 主に個人的な興味や関心を持ったトピックについて解説していこうと思います 暖かい目で見守ってもらえると幸いです

Slide 12

Slide 12 text

Agenda はじめに 内部構造の探索:NumPy編 内部構造の探索:Pandas編 内部構造の探索:Polars編 まとめ

Slide 13

Slide 13 text

内部構造の探索:NumPy編 概要 NumPyはPythonにおける数値計算を効率的に行うための 拡張モジュール 効率的な計算を行うための多次元配列とルーチン(関数群)を提供し 科学技術やデータ分析、機械学習などの計算基盤として利用 名称は「Numerical Python」に由来

Slide 14

Slide 14 text

内部構造の探索:NumPy編 根幹的なオブジェクト NumPyは根幹的なオブジェクトとして、下記の二つを提供 ・ndarray(N-Dimensional Array) 通称、N次元配列 データを効率よく管理・操作できるように設計 Pythonのリストと似ているが、いくつか独自ルールを持つ ・ufunc(Universal Functions) 配列の各要素に対して、高速な演算を行うこうとができる関数を提供 高度な計算をシンプルな記法で表現することもできる

Slide 15

Slide 15 text

内部構造の探索:NumPy編 簡単な利用シーンを見てみよう

Slide 16

Slide 16 text

内部構造の探索:NumPy編 配列の簡単な例 pip install numpy import numpy as np arr = np.array([[1, 2, 3], [4, 5, 6]]) # [[1, 2, 3], [4, 5, 6]] 2次元の 配列オブジェクト 配列が持つ属性の一例 属性名 概要 出力例 1 2 3 shape 配列の形状 (2, 3) 4 5 6 dtype データ型 int64 size 要素の数 6 T 転置 [[1, 4], [2, 5], [3, 6]] arr.ndim # 次元数 → 2

Slide 17

Slide 17 text

内部構造の探索:NumPy編 dtypeについて NumPyがサポートするデータ型 dtypeの適切な指定により、処理のパフォーマンスも向上する データ型の例) ・整数 int8, int16, int32, int64 ・整数(符号なし) uint8, uint16, uint32, uint64 ・浮動小数点数 float16, float32, float64 ・複素数 complex64, complex128 ・ブール値・文字列 bool, str ・オブジェクト object Pythonのデータ型と比べ、豊富な種類を提供

Slide 18

Slide 18 text

内部構造の探索:NumPy編 様々な配列の操作 arr2 = arr.reshape(3, 2) # 形状変更 arr2[0] = 10 # インデックスによるアクセス arr3 = arr2[1:] # スライス arr4 = np.concatenate((arr2, arr2), axis=1) # 配列の結合 arr5 = arr2.flatten() # 1次元へのフラット化 arr6 = np.unique(arr5) # 重複削除 arr7 = np.sort(arr6, descending=True) # ソート(降順) arr2 arr3 10 10 3 3 4 5 6 5 arr4 4 10 10 10 10 6 3 4 3 4 5 6 5 6 arr5 10 10 3 4 5 arr6 3 4 5 6 10 arr7 10 6 5 4 3 6

Slide 19

Slide 19 text

内部構造の探索:NumPy編 配列を操作するための多様な関数 ・形状の変更 reshape, ravel, ndarray.flatten ・配列の結合 concatenate, stack, (h)vstack ・軸の入れ替え・転置 transpose(T), swapaxes, moveaxis ・配列の分割 split, (h)vsplit ・次元の変更・拡張 expand_dims, squeeze, broadcast_to ・要素の追加・削除・変更 delete, insert, append, unique ・要素の並び替え・反転 flip, roll, sort

Slide 20

Slide 20 text

内部構造の探索:NumPy編 データのファイルIO # バイナリ読み書き np.save('my_array.npy', arr) loaded_array = np.load('my_array.npy') 保存フォーマットの使い分け ・バイナリ 高速かつ正確な情報で保存 ・テキスト # バイナリ読み書き(複数配列) 人がみてわかるデータで保存 np.savez('multiple.npz', arr_1=arr, arr_2=arr2) data = np.load('multiple.npz') data['arr_2'] # [[10, 10], [3, 4], [5, 6]] # テキスト(CSVなど)読み書き np.savetxt('data.csv', arr2, delimiter=',', fmt='%.2f') loaded_data = np.loadtxt('data.csv', delimiter=',')

Slide 21

Slide 21 text

内部構造の探索:NumPy編 IOの応用:memmapについて ディスク上のファイルを仮想的なメモリ空間にマッピングし、メモリを節約 filename = 'huge_dataset.dat' shape = (10000, 10000) dtype = np.float64 必要な部分だけを読み書きする 巨大なデータを扱うときに重宝 mmap_arr = np.memmap(filename, dtype=dtype, mode='w+', shape=shape) mmap_arr[0, :] = np.arange(10000) * 1.5 mmap_arr[5000, 5000] = 12345.6789 mmap_arr.flush() read_only_arr = np.memmap(filename, dtype=dtype, mode='r', shape=shape) read_only_arr[0, :5] # [0. 1.5 3. 4.5 6. ] read_only_arr[5000, 5000] # 12345.6789

Slide 22

Slide 22 text

内部構造の探索:NumPy編 数値計算における関数 # 要素同士の加算と行列積 ndarray = np.arange(3).reshape(3, 1) np.add(ndarray, ndarray) # 加算 np.matmul(ndarray, ndarray.T) # 行列積 0 0 1 1 2 2 add => 0 0 2 1 4 2 0 1 2 matmul => 0 0 0 0 1 2 0 2 4

Slide 23

Slide 23 text

内部構造の探索:NumPy編 いろいろなufunc ・基本的な数学関数 add, subtract, multiply, divide, power, mod, absolute, negative, sign, floor, ceil, round ・三角関数・逆三角関数 (arc)sin, (arc)cos, (arc)tan ・指数・対数関数 exp, expm1, log, log10 ・ビット演算 bitwise_and(or, xor), invert, left(right)_shift ・比較演算 greater(_equal), less(_equal), (not_)equal ・浮動小数点・その他の処理 isnan, isinf, isfinite, maximum, minimum

Slide 24

Slide 24 text

内部構造の探索:NumPy編 演算や関数の裏で利用されるufunc ndarray = np.arange(3).reshape(3, 1) ndarray + ndarray # = np.add(ndarray, ndarray) = [[0], [2], [4]] 演算の裏で実はufuncが動作 利用例) ・基本的な演算 + → add - → subtract * → multiply / → divide // → floor_divide ** → power % → mod ・比較演算 == → equal != → not_equal < → less <= → less_equal > → greater >= → greater_equal % → mod ・集計関数 sum → add.reduce prod → multiply.reduce max → maximum.reduce min → minimum.reduce all → logical_and.reduce any → logical_or.reduce

Slide 25

Slide 25 text

内部構造の探索:NumPy編 高度な数値計算 NumPyに梱包されている高度な数値計算のためのライブラリ Fortranベースで構成されている処理も多数あり Fortran(Formula Translation): 数値計算・科学技術計算に特化したプログラミング言語 BLAS (Basic Linear Algebra Subprograms): 線形代数の基礎的な演算を提供するライブラリ ベクトル同士の計算や行列とベクトルの演算などで活躍 LAPACK (Linear Algebra PACKage): BLASをベースに構築された高度な線形代数計算のためのライブラリ FFTPACK(Fast Fourier Transform PACKage): 高速フーリエ変換(FFT)とその逆変換などを計算するためのライブラリ

Slide 26

Slide 26 text

内部構造の探索:NumPy編 高度な数値計算の関数たち ・行列積・内積に関する関数 dot, matmul, inner, vdot ・numpy.linalg モジュール (線形代数ライブラリ) inv, solve, det, eig(h), qr, svd 【補足】F2PYについて Fortran to Python interface generatorの略称 Fortranで書かれた関数や サブルーチンをPythonから 呼び出すことを可能にする ・numpy.fft モジュール (高速フーリエ変換) fft, ifft 高度な数値の計算機能を提供 様々なツールの計算基盤として利用される

Slide 27

Slide 27 text

内部構造の探索:NumPy編 簡単な計算速度の比較(vs Pythonリスト) # 100万行の加算を計測 size = 10_000_000 py_list = list(range(size)) np_arr = np.arange(size) py_result = [x + 1 for x in py_list] # →0.66 s np_result = np_arr + 1 # →0.02 s NumPy配列の圧勝!

Slide 28

Slide 28 text

内部構造の探索:NumPy編 NumPyの内部構造をみていこう

Slide 29

Slide 29 text

内部構造の探索:NumPy編 配列が持つ制限について 配列が持つ3つの制限 ・配列のすべての要素は同じデータ型である必要がある ・一度作成された配列のサイズは変更できない(イミュータブル) ・形状は長方形で統一されていなければならない # 制限にかかる例 ndarray = np.array([ 1 , 1 , [ 1 ]]) # データ型が異なるため、エラー ndarray.append( 4 ) # 要素の個数は変更できない(そもそもappendがない)ため、エラー ndarray = np.array([[ 1 ], [ 1 ], [ 1 , 2 ]]) # 形状が統一されていないため、エラー # object を指定するとどの値でも作成可能だが非推奨 ndarray = np.array([[ 1 ], [ 1 ], [ 1 , 2 ]], dtype = object ) # array([list([1]), list([1]), list([1, 2])], dtype=object)

Slide 30

Slide 30 text

内部構造の探索:NumPy編 配列が持つ制限について 配列が持つ3つの制限 ・配列のすべての要素は同じデータ型である必要がある ・一度作成された配列のサイズは変更できない(イミュータブル) ・形状は長方形で統一されていなければならない これらの制約は # 制限にかかる例 NumPyのメモリレイアウトに由来 ndarray = np.array([ 1 , 1 , [ 1 ]]) # データ型が異なるため、エラー ndarray.append( 4 ) # 要素の個数は変更できない(そもそもappendがない)ため、エラー ndarray = np.array([[ 1 ], [ 1 ], [ 1 , 2 ]]) # 形状が統一されていないため、エラー # データ型が同じであれば良いので、object を指定することも可能だが非推奨 ndarray = np.array([[ 1 ], [ 1 ], [ 1 , 2 ]], dtype = object ) # array([list([1]), list([1]), list([1, 2])], dtype=object)

Slide 31

Slide 31 text

内部構造の探索:NumPy編 配列のメモリレイアウト NumPyの配列は、全要素を連続した一列のメモリブロック格納 Pythonのリストにおけるメモリレイアウト P1 p2 P3 P4 P5 P6 ・データの参照ポインタを格納 ・バラバラなデータ型でも柔軟に管理可能 ・データの実体は別の場所にあるため 探し出す手間がオーバーヘッドとなる NumPyの配列におけるメモリレイアウト 1 2 3 4 5 6 ・データの実体を連続して一列に格納 ・メモリ効率を最大化し高速処理を実現 ・実体を一列に連続して格納するため それに伴い制限が必要となる C言語やFortranと親和性があり、キャッシュヒット率が大幅に向上 このメモリレイアウトに加え、shapeとstridesを使って多次元を表現

Slide 32

Slide 32 text

内部構造の探索:NumPy編 shapeとstrides shape:配列の形状を表すタプル strides:メモリ上で次の要素に進むために必要なバイト数のタプル int64(8バイト)の3×2配列 [[1, 2], [3, 4], [5, 6]] 1 2 3 4 5 6 8バイト 8×2 = 16バイト shape: (3, 2) strides: (16, 8)

Slide 33

Slide 33 text

内部構造の探索:NumPy編 shapeとstrides 配列のインスタンス化時にデータの並び順を指定することも可能 import numpy as np ndarray_c = np.array([[ 1 , 2 ], [ 3 , 4 ], [ 5 , 6 ]], order = 'C' ) ndarray_f = np.array([[ 1 , 2 ], [ 3 , 4 ], [ 5 , 6 ]], order = 'F' ) ndarray_c.shape # (3, 2) ndarray_c.strides # (16, 8) ndarray_f.shape # (3, 2) ndarray_f.strides # (8, 24) C言語は行優先 Fortranは列優先 優先するデータの並び順に応じて stridesが変化 これらの情報をもとに、C言語やFortranとスムーズな連携を実現

Slide 34

Slide 34 text

内部構造の探索:NumPy編 Viewについて 既存の配列から新規作成された配列 データをコピーするのではなく、元の配列を参照する形で作成される ndarray = np.array([1, 2, 3, 4, 5]) ndarray_view = ndarray[::2] # [1, 3, 5] 【余談】スライスの挙動差 ・Pythonのリスト →コピーを作成 ndarray_view[0] = 0 # baseで参照元の配列を確認 ・NumPy配列 ndarray_view.base # array([[0, 2, 3, 4, 5]]) →Viewを作成 ndarray_view.strides # (16,) ← 2段飛ばしのstrides (別途copy関数あり) NumPyの配列生成は基本的にViewを返す

Slide 35

Slide 35 text

内部構造の探索:NumPy編 Viewについて 既存の配列から新規作成された配列 データをコピーするのではなく、元の配列を参照する形で作成される ndarray = np.array([1, 2, 3, 4, 5]) ndarray_view = ndarray[::2] # [1, 3, 5] 【余談】スライスの挙動差 ・Pythonのリスト 無駄なコピーの発生を抑え、省メモリで高速な処理に繋げている →コピーを作成 ndarray_view[0] = 0 # baseで参照元の配列を確認 ・NumPy配列 ndarray_view.base # array([[0, 2, 3, 4, 5]]) →Viewを作成 ndarray_view.strides # (16,) ← 2段飛ばしのstrides (別途copy関数あり) スライス以外の操作でも、コピーの必要がない場合はViewを返す

Slide 36

Slide 36 text

内部構造の探索:NumPy編 ここからNumPyの処理の裏側を見ていこう

Slide 37

Slide 37 text

内部構造の探索:NumPy編 NumPyの裏側 NumPyの処理の裏側では様々な工夫が存在 ・形状の自動調整とメモリの節約(ブロードキャスト) ・C言語のパワーを活用した高速処理(ベクトル化とC-API) ・Pythonの足枷を外す並列化(スレッドセーフティとGIL) ・ハードウェアの性能を引き出す演算(SIMD最適化)

Slide 38

Slide 38 text

内部構造の探索:NumPy編 NumPyの裏側 NumPyの処理の裏側では様々な工夫が存在 ・形状の自動調整とメモリの節約(ブロードキャスト) ・C言語のパワーを活用した高速処理(ベクトル化とC-API) ・Pythonの足枷を外す並列化(スレッドセーフティとGIL) ・ハードウェアの性能を引き出す演算(SIMD最適化)

Slide 39

Slide 39 text

内部構造の探索:NumPy編 ブロードキャスト 配列の演算において、形状を自動で調節する機能 ※ 適用するためルールもあり(引き伸ばしができることなど) np.array([[1,1,1],[2,2,2],[3,3,3]]) + np.array([[1,2,3]]) # array([[2, 3, 4], [3, 4, 5], [4, 5, 6]]) (3, 3) (1, 3) 1 1 1 2 2 2 3 3 3 + (3, 3) 1 2 3 1 2 3 1 2 3 = 2 3 4 3 4 5 4 5 6 手動による調整が不要で、柔軟な演算を可能にする

Slide 40

Slide 40 text

内部構造の探索:NumPy編 ブロードキャストの裏側 ブロードキャスト時もViewやshape、stridesを活用 余分なコピーは発生しない v = np.array([1, 2, 3]) # array([1, 2, 3]) # ブロードキャスト bc = np.broadcast_to(v, (3, 3)) # array([[1, 2, 3], [1, 2, 3], [1, 2, 3]]) # ブロードキャスト後の形状や参照 bc.shape # (3, 3) bc.strides # (0, 8) bc.base # array([1, 2, 3]) stridesを0で表現し 大きい方の形状に 沿って演算する

Slide 41

Slide 41 text

内部構造の探索:NumPy編 NumPyの裏側 NumPyの処理の裏側では様々な工夫が存在 ・形状の自動調整とメモリの節約(ブロードキャスト) ・C言語のパワーを活用した高速処理(ベクトル化とC-API) ・Pythonの足枷を外す並列化(スレッドセーフティとGIL) ・ハードウェアの性能を引き出す演算(SIMD最適化)

Slide 42

Slide 42 text

内部構造の探索:NumPy編 NumPyにおけるベクトル化 ベクトル化:各要素を一まとまりのデータとして一括処理する技法 Pythonのfor文を使わずに、最適化されたC言語ループを内部で活用する ・パフォーマンスの大幅な向上 ・コードの可読性向上 np.array([[1,1,1]]) + np.array([[1,1,1]]) # array([[2, 2, 2]]) ベクトル化の裏側ではNumPyの用意するC-APIが活用

Slide 43

Slide 43 text

内部構造の探索:NumPy編 C-APIの前に!配列の主要な構造について 配列(ndarray)は下記二つの主要な構造を持つ PyArrayObject: PyObject(CPython)を配列用に拡張したC言語の構造体 shape、strides、dtypeなど、配列に必要な情報を持つ データバッファ: 配列が持つ実際の数値データが配置されたメモリ領域 PyArrayObject1 [1, 2, 3, 4] shape: (4, ) strides: (8, ) データバッファ 参照 … 参照 1 2 3 4 … PyArrayObject2 [[1, 2], [3, 4]] shape: (2, 2) strides: (16, 8)

Slide 44

Slide 44 text

内部構造の探索:NumPy編 C-APIについて C-API:C言語で構成されたNumPy用のAPI 特徴 ・最適化されたCコードによる高速な処理 ・NumPyのメモリレイアウトを活かした効率的なメモリ管理 ・dtypeに関する柔軟な制御(型判定やキャストなど) ・外部ライブラリ、システム拡張のための機能の提供 など 用途や役割に応じて、様々な種類のAPIに分類される

Slide 45

Slide 45 text

内部構造の探索:NumPy編 C-APIの種類(用途・役割) C-APIの種類 用途・役割 Array API 配列自体の作成、形状やストライドの取得、メモリ管理 Data type API データ型(dtype)の判定や生成 Array iterator API 多次元配列におけるループの高速かつ安全なイテレーション ufunc API ufuncの拡張(カスタマイズ)や定義 gufunc API 汎用ufunc(行列の掛け算など)の拡張(カスタマイズ)や定義 NpyString API NumPyの文字列データをC言語側で安全かつ効率的に操作 Datetime API NumPyの時系列データをC言語側の構造にパース

Slide 46

Slide 46 text

内部構造の探索:NumPy編 C-APIの種類(主なデータ構造の例) C-APIの種類 主要なデータ構造の例 Array API PyArrayObject、PyArray_Chunk、PyArray_Conn Data type API PyArray_Descr Array iterator API NpyIter、NpyIter_IterNextFunc ufunc API PyUFuncObject、PyUFuncGenericFunction gufunc API PyUFuncObject NpyString API NpyString_Type Datetime API PyArray_DatetimeMetaData

Slide 47

Slide 47 text

内部構造の探索:NumPy編 C言語のパワーを活かしたカスタムな処理の実装例 nditer:NumPyが提供している配列イテレータ用のオブジェクト 内部でNpyIterを用いた高速な処理を利用 # 例:1000万個の配列同士の加算処理の比較(VS Pythonのリスト) [x + y for x, y in zip(list_a, list_b)] # 0.85 s with np.nditer([array_a, array_b, None], flags=['external_loop', 'buffered'], op_flags=[['readonly'], ['readonly'], ['writeonly']]) as it: for x, y, o in it: print('Hi!') # カスタム処理(ループ挨拶) ・引数で様々な設定が可能 ・C言語による高速処理 o[...] = x + y # 0.06 sですべてのループ完了 ・柔軟なカスタマイズ

Slide 48

Slide 48 text

内部構造の探索:NumPy編 NumPyの裏側 NumPyの処理の裏側では様々な工夫が存在 ・形状の自動調整とメモリの節約(ブロードキャスト) ・C言語のパワーを活用した高速処理(ベクトル化とC-API) ・Pythonの足枷を外す並列化(スレッドセーフティとGIL) ・ハードウェアの性能を引き出す演算(SIMD最適化)

Slide 49

Slide 49 text

内部構造の探索:NumPy編 スレッドセーフティとGIL GIL(Global Interpreter Lock): 一つのプロセス内で一つのスレッドしか処理できないPythonの排他制御 NumPyにはGILを解放し、パフォーマンスを上げるための工夫がある NPY_BEGIN_ALLOW_THREADS:Pythonの状態を保存しGILを解放するマクロ NPY_END_ALLOW_THREADS:GILを取得しPythonの状態を復元するマクロ Python層 C言語層 Python層 GILの解放 配列の演算など 並列処理 GILの再取得 結果の出力

Slide 50

Slide 50 text

内部構造の探索:NumPy編 CPythonのスレッド管理 実はNumPyのスレッドセーフティはCPythonの処理と同等 CPythonが用意している構造体やマクロ PyThreadState:Stack TraceやException state、属しているPython インタプリターなどスレッド処理の中で保持している情報 Py_BEGIN_ALLOW_THREADS:PyThreadStateの退避とGILの解放 Py_END_ALLOW_THREADS:GILの再取得とPyThreadStateの復元 NumPy側のマクロではビルド設定(マルチスレッドオフなど)なども 考慮してGIL解放の判定を実施

Slide 51

Slide 51 text

内部構造の探索:NumPy編 NumPyの裏側 NumPyの処理の裏側では様々な工夫が存在 ・形状の自動調整とメモリの節約(ブロードキャスト) ・C言語のパワーを活用した高速処理(ベクトル化とC-API) ・Pythonの足枷を外す並列化(スレッドセーフティとGIL) ・ハードウェアの性能を引き出す演算(SIMD最適化)

Slide 52

Slide 52 text

内部構造の探索:NumPy編 SIMDの活用 SIMD: 単一命令・複数データ(Single Instruction, Multiple Data)の略称 一つの命令で複数のデータを並列処理することができるCPUの機能 NumPyのメモリレイアウトとも相性が良く、メモリアクセスで 待たされる時間も短いためSIMDの機能をフル活用できる Universal Intrinsics: CPUの種類によって命令セットが異なるため、共通のインタフェース としてNumPyが提供するマクロのセット コンパイル時の命令の有効化や実行時のディスパッチなどもサポート ※ 最適化のオフなどもユーザー側で柔軟に設定することも可能

Slide 53

Slide 53 text

内部構造の探索:NumPy編 NumPyの探索まとめ NumPyの内部構造を探索してみると様々な工夫や仕組みがあった ・C言語やFortranと相性の良いデータ型やメモリレイアウト ・無駄なコピーの抑制 ・ブロードキャストによる柔軟な計算 ・ベクトル化(C-API)やスレッドセーフティによる処理の高速化 ・ハードウェアの機能(SIMD)を活かす構成 などなど 「Pythonに数値計算機能を持たせたい」という熱から生まれたNumPy たくさんの工夫を積み重ねて、爆発的なパフォーマンスと利便性を実現 計算基盤として利用されつつ、今後も活躍の場を広げていきそう

Slide 54

Slide 54 text

Agenda はじめに 内部構造の探索:NumPy編 内部構造の探索:Pandas編 内部構造の探索:Polars編 まとめ

Slide 55

Slide 55 text

内部構造の探索:Pandas編 概要 Pandasは表形式のデータを効率的に操作・分析することに特化した Pythonライブラリ 土台はNumPyで構築され、統計処理やデータのクリーニング、 表の結合、グループ化、ファイル入出力など多様な機能を備える 名称は「Panel Data」に由来する

Slide 56

Slide 56 text

内部構造の探索:Pandas編 基本的な構成 Pandasは根幹的なデータ構造として、下記の二つを持つ ・Series 任意のデータ型を保持できる1次元ラベル付きの配列 (ただし異なる型の混在は不可) ndarrayにインデックスがついたイメージ ・DataFrame 2次元の表形式のデータ構造 各行にインデックス(Index)、各列に名前(Columns)を持つ

Slide 57

Slide 57 text

内部構造の探索:Pandas編 簡単な利用シーンを見てみよう

Slide 58

Slide 58 text

内部構造の探索:Pandas編 SeriesとDataFrameの具体例 pip install pandas import pandas as pd import numpy as np df = pd.DataFrame({ NumPy配列のデータも利用可 'ID': pd.Series(np.array([100, 200, 300]), index=['r1', 'r2', 'r3']), 'Category': ['X', 'Y', 'Z'] }) df df["Category"] DataFrameの各列は Seriesで構成

Slide 59

Slide 59 text

内部構造の探索:Pandas編 DataFrameの基本操作(フィルタ、マージ、欠損値処理) df2 = df[df["Category"]=="X"] # フィルタ df3 = pd.merge(df, pd.DataFrame({ 'ID': pd.Series(np.array([100, 200, 300]), index=['r1', 'r2', 'r3']), 'Category': ['A', 'B', None] }), on="ID", how="inner") # マージ 多様で便利なデータ操作 df4 = df3.fillna("!!!") # 欠損値処理 df2 df3 df4

Slide 60

Slide 60 text

内部構造の探索:Pandas編 Pandasの基本操作(データ変形(集計・グループ化・構造変更)) df["ID_2x"] = df["ID"] * 2 df["ID_3x"] = df["ID"] * 3 df["ID_total"] = df["ID_2x"] + df["ID_3x"] 列の計算や構造変更も スムーズに実行 df5 = df.groupby("Category")[["ID_2x", "ID_3x", "ID_total"]].sum() df df5

Slide 61

Slide 61 text

内部構造の探索:Pandas編 Pandasの基本操作(ファイルIO) csv_filename = "df_pd.csv" # CSVへの読み書き df.to_csv(csv_filename) pd.read_csv(csv_filename) 表データの保存形式一覧 幅広い保存形式に対応 必要最低限のデータ取得 など、オプションも豊富 read_csvのオプション例 usecols:取得列の指定 dtype:データ型の指定 chunksize:分割のサイズ メソッド名 保存形式 read_csv / to_csv CSV、テキスト read_excel / to_excel エクセル read_parquet / to_parquet Parquet read_ json / to_ json JSON read_sql / to_sql SQL read_pickle / to_pickle Pickle

Slide 62

Slide 62 text

内部構造の探索:Pandas編 Pandasの基本操作(グラフ描画) df_loaded = pd.read_csv(csv_filename) df_loaded.plot( x="month", y="revenue", kind="bar", title="月別売上推移",color="steelblue", grid=True, ) import matplotlib.pyplot as plt # Matplotlibによる描画 plt.show() Matplotlib:Pythonのあらゆるデータ を柔軟に可視化することに特化した ライブラリ

Slide 63

Slide 63 text

内部構造の探索:Pandas編 DataFrameが提供している関数 ・基本属性・構造確認 ・変形・操作・結合 shape, ndim, size, dtypes, apply, map, applymap, columns, index, values sort_values, sort_index, ・インデックス・選択 pd.merge, pd.concat loc, iloc, head, tail, query ・グループ化・集計 ・集計・統計演算 groupby, pivot_table, melt sum, mean, std, min, max, describe ・入出力・変換 ・欠損値・データ整形 to_numpy, to_dict, to_csv isna, notna, fillna, dropna to_parquet, to_ json ・特殊アクセサ (拡張)、可視化 複雑で大量の現実データを柔軟に効率 str, dt, cat, plot よく捌けるように多くの機能を提供

Slide 64

Slide 64 text

内部構造の探索:Pandas編 Pandasの内部構造を少し深ぼってみる

Slide 65

Slide 65 text

内部構造の探索:Pandas編 データのアライメントと演算について アライメント: 異なる形式のデータに対して、インデックスやカラムを自動で揃える機能 pd.DataFrame(np.ones((3, 3)),index=["r1", "r2", "r3"],columns=["A", "B", "C"]) pd.Series([10, 20], index=["B", "X"]) A B C r1 1 1 1 r2 1 1 1 r3 1 1 1 B + X 10 20 =

Slide 66

Slide 66 text

内部構造の探索:Pandas編 データのアライメントと演算について アライメント: 異なる形式のデータに対して、インデックスやカラムを自動で揃える機能 pd.DataFrame(np.ones((3, 3)),index=["r1", "r2", "r3"],columns=["A", "B", "C"]) pd.Series([10, 20], index=["B", "X"]) A B C r1 N 1 N r2 N 1 N N r3 N 1 N N ※ NはNanの略 + B X 10 N =

Slide 67

Slide 67 text

内部構造の探索:Pandas編 データのアライメントと演算について アライメント: 異なる形式のデータに対して、インデックスやカラムを自動で揃える機能 pd.DataFrame(np.ones((3, 3)),index=["r1", "r2", "r3"],columns=["A", "B", "C"]) pd.Series([10, 20], index=["B", "X"]) A B C B X r1 N 1 N 10 N r2 N 1 N 10 N r3 N 1 N 10 N ※ NはNanの略 + =

Slide 68

Slide 68 text

内部構造の探索:Pandas編 データのアライメントと演算について アライメント: 異なる形式のデータに対して、インデックスやカラムを自動で揃える機能 pd.DataFrame(np.ones((3, 3)),index=["r1", "r2", "r3"],columns=["A", "B", "C"]) pd.Series([10, 20], index=["B", "X"]) A B C B X r1 N 1 N 10 N r2 N 1 N 10 r3 N 1 N 10 ※ NはNanの略 + A C X r1 N 11 N N N r2 N 11 N N N r3 N 11 N N = B 形式の異なるデータ でも手動の調整は不要 ブロードキャストも サポート ※ アライメント優先

Slide 69

Slide 69 text

内部構造の探索:Pandas編 Copy-on-Write (CoW)について スライスや列抽出、フィルタリングなどで作成されたオブジェクトは df2 論理的なコピーとして扱われる df df = pd.DataFrame( {"A": [0,1],"B": [2,3]}, index=["r1", "r2"] ) df2 = df[:] df3 = df[["A"]] df4 = df.loc[df["B"] > 2] df2.loc["r1","A"]=2 値を変更しても元の df3.loc["r1","A"]=3 値に影響を受けない df4.loc["r2","A"]=4 df3 df4

Slide 70

Slide 70 text

内部構造の探索:Pandas編 CoWの背景 従来、PandasはNumPy配列を基盤で活用しパフォーマンスを上げてきた 一方で、それに伴い下記のような課題があった ・配列の操作によってViewを返したりコピーを返したり混在していた ・copyの明示的な指定により、コードの煩雑化や処理の遅延に繋がった ・SettingWithCopyWarningなど不要な警告がストレスになっていた CoWをデフォルト導入することでこれらの課題を解決

Slide 71

Slide 71 text

内部構造の探索:Pandas編 CoWの背景 従来、PandasはNumPy配列を基盤で活用しパフォーマンスを上げてきた 一方で、それに伴い下記のような課題があった ・配列の操作によってViewを返したりコピーを返したり混在していた ・copyの明示的な指定により、コードの煩雑化や処理の遅延に繋がった ・SettingWithCopyWarningなど不要な警告がストレスになっていた CoWをデフォルト導入することでこれらの課題を解決 【補足】CoWは遅延コピーによりパフォーマンスの低下も防ぐ 遅延コピー: 実際にコピーが必要になるまでメモリ共有の状態を保つ コピーの必要性(値が変更されたかなど)はBlockManagerで管理

Slide 72

Slide 72 text

内部構造の探索:Pandas編 BlockManagerについて Block: 同じデータ型のデータを一つの塊としてまとめたもの BlockManager: Blockの配置やライフサイクルなどを管理する機能 データの参照状態も追跡し、コピーの必要性の判断も行う 簡単なイメージ BlockManagerにより、 NumPy由来の高速処理を 活かしつつDataFrameの 柔軟な表現力を両立

Slide 73

Slide 73 text

内部構造の探索:Pandas編 Pandasがサポートするデータ型たち 各データ型例) ・NumPyのデータ型 int64, float64, boolなど ・Pandas独自の拡張型 Nullable拡張, Categorical 時系列・空間型, Sparse Int64Dtype, CategoricalDtype PeriodDtype, IntervalDtype, SparseDtypeなど ・PyArrow系 数値型, 文字列・真偽値型 日時・時間型, 複雑な構造・高度な型 int64, float64, decimal128 string, bool_ timestamp, date64, time64, duration dictionary, list_, map_など NumPyのデータ型をベースに、多種多様なデータ型をサポート

Slide 74

Slide 74 text

内部構造の探索:Pandas編 ここからさらにPandasの内部を深ぼってみよう

Slide 75

Slide 75 text

内部構造の探索:Pandas編 Pandasの欠損値の取り扱いについて Pandasはデータの中に欠損値が混ざるとアップキャストが発生 s_int = pd.Series([1, 2, None]) s_bool = pd.Series([True, False, None]) s_int.dtype # →float64 データの中に欠損値が混ざると アップキャストが発生 s_bool.dtype # object アップキャストによる弊害 ・意図しない変換による結合時のエラー ・無駄なメモリ消費量の増大 ・一意性などの判定ミス ・機械学習モデルへの誤ったバイアスの持ち込み など

Slide 76

Slide 76 text

内部構造の探索:Pandas編 Nullable拡張型の役割 Nullable拡張型で不要なアップキャストを防ぐ s_int = pd.Series([1, 2, None], dtype="Int64") s_bool = pd.Series([True, False, None], dtype="bool") s_int.dtype # →Int64 欠損値を含んでいても 指定した型を維持 s_bool.dtype # bool 補足:マスクの情報 Pandasは本体のデータとは別にマスク情報 を保持し、この情報をもとに欠損値を捌く 演算や集計、IOなど様々な場面でも利用 print(s_int.mask) # # dtype: Int64>

Slide 77

Slide 77 text

内部構造の探索:Pandas編 Sparseデータ構造 巨大なデータの中で、意味のある値のみを保存するデータ構造 欠損値(Nan)や0などが大量に含まれるデータを扱う場合に有効 例)95%がNanの1000×4のDataFrame sdf = df.astype( pd.SparseDtype("float64", np.nan) ) # メモリ使用量の比較 df.memory_usage().sum() / 1024 # 31.38 KB sdf.memory_usage().sum() / 1024 # 0.36 KB 大幅なメモリ使用の削減!!!

Slide 78

Slide 78 text

内部構造の探索:Pandas編 カテゴリ型について データのカテゴライズにより、効率的で高速な処理を実現する型 種類が限られている文字列などで特に有効 カテゴリ型の内部構造 categories:ユニークな値のリスト codes:各行の値がcategoriesの何番目に該当するかを示す整数配列 文字列型 ["A", "A", "B", "B", "C", "C"] カテゴリ型 categories: ["A", "B", "C"] codes: [0, 0, 1, 1, 2, 2] 辞書エンコーディング:繰り返す値を一意の辞書としてまとめ、 実際のデータは小さな整数で表現することでデータを圧縮する技術

Slide 79

Slide 79 text

内部構造の探索:Pandas編 カテゴリ型による改善の検証 # 100万行のシリーズにおける比較(vs 文字列) data = np.random.choice(["ワイン", "ハイボール", "ビール"], size=1_000_000) s_obj = pd.Series(data) s_cat = pd.Series(data).astype("category") s_cat = s_cat.cat.set_categories( ["ワイン", "ハイボール", "ビール"], ordered=True ) s_obj.memory_usage() # 18.12 MB s_cat.memory_usage() # 0.95 MB s_obj.groupby(s_obj).count() # 0.03 s s_cat.groupby(s_cat).count() # 0.01 s s_cat.max() # ビール メモリと処理速度の改善 orderを設定することで データ同士の比較も可能

Slide 80

Slide 80 text

内部構造の探索:Pandas編 PyArrowについて PyArrowはApache Arrow(後述)が提供するPythonライブラリ 列指向のデータ構造を採用し、高いパフォーマンスを実現する 特徴 ・効率的なメモリ管理 ・処理速度の大幅な向上 ・欠損値のスムーズな扱い ・Arrowエコシステムとの「ゼロコピー」連携 PandasもPyArrowとの連携をサポート

Slide 81

Slide 81 text

内部構造の探索:Pandas編 PyArrowの簡単なデータ構造 配列:同一データ型で構成される1次元のデータ構造 データの値はメモリ上で連続配置され、欠損値(Null)も対応 テーブル:複数の配列から構成されるデータ構造 データの連結や結合、ほかフォーマットへの変換など多様な役割を持つ import pyarrow as pa col_id = pa.array([100, 200, 300], type=pa.int32()) col_category = pa.array(["X", "Y", None], type=pa.string()) arrow_table = pa.Table.from_arrays( [col_id, col_category], names=["ID", "Category"] ) PandasのDataFrameとも相性◎

Slide 82

Slide 82 text

内部構造の探索:Pandas編 PyArrowとPandasのデータ連携(Pandas → PyArrow) import pyarrow.parquet as pq import pyarrow as pa df_pd = pd.DataFrame({ "name": ["Aさん", "Bさん"], "age": [28, 34], "pref": ["広島", "東京"] }) # DataFrame → Tableへの変換とParquetへの保存 table_from_pd = pa.Table.from_pandas(df_pd) pq.write_table(table_from_pd, "data.parquet") Parquet:大規模なデータの取り扱いに特化した ファイル保存形式(ストレージフォーマット) Arrow形式のデータを圧縮してコンパクトに保存

Slide 83

Slide 83 text

内部構造の探索:Pandas編 PyArrowとPandasのデータ連携(PyArrow → Pandas) table_loaded = pq.read_table("data.parquet") new_column = pa.array([200, 100]).sort() new_table = table_loaded.append_column("score", new_column) new_df = new_table.to_pandas() # テーブル → DataFrameへの変換 # DataFrame ↔ ParquetのIO new_df.to_parquet("df_pd_data.parquet") df_loaded = pd.read_parquet("df_pd_data.parquet")

Slide 84

Slide 84 text

内部構造の探索:Pandas編 PyArrow&ParquetとPandas&CSVのIO速度比較 N = 1_000_000 # 100万行データのDataFrame df = pd.DataFrame({ "id": range(N), "category": [f"cat_{i % 10}" for i in range(N)], "value": [float(i) * 1.5 for i in range(N)], }) table = pa.Table.from_pandas(df) df.to_csv("test.csv", index=False) # 1.754s pq.write_table(table, "test.parquet") # 0.115s df_csv = pd.read_csv("test.csv") # 0.269s table_par = pq.read_table("test.parquet") # 0.027s 読み書きともに PyArrow&Parquetが 圧勝!

Slide 85

Slide 85 text

内部構造の探索:Pandas編 Apache Arrowについて Apache Arrow: 効率的なデータ交換とインメモリ分析に特化したソフトウェア メモリの標準規格として列指向のデータフォーマットを採用し、 異なるシステムやプログラミング言語間においてもスムーズかつ 高速な処理を実現する 従来のメモリ配置(行単位) 表データ Aさん name age pref Aさん 28 広島 Bさん 34 東京 28 広島 Bさん 34 東京 Apache Arrowのメモリ配置 Aさん Bさん 28 34 広島 東京

Slide 86

Slide 86 text

内部構造の探索:Pandas編 Apache Arrowを採用することによるメリット ・省メモリかつ高速な処理の実現 キャッシュヒット率の向上やゼロコピー、 効率的な欠損値の管理、SIMDの活用、 ネスト構造など複雑なデータへも対応 Pandas Polars シリアライズや コピー・変換などの オーバーヘッド Parquet ・「言語・システム間の壁」の払拭 プログラミング言語やシステムの広いサポート メモリの標準規格による処理のコストカット 不要なシリアライズ・パース処理の抑制 ・ストレージ、ネットワーク通信の効率化 Apache ParquetやApache Featherなど 連携コストの大幅削減 Pandas Arrow Memory Parquet Polars

Slide 87

Slide 87 text

内部構造の探索:Pandas編 Apache Arrowの広がり 様々な言語やそれらのライブラリ、エコシステムなどでApache Arrowの サポートが広がってきている ・各プログラミング言語 .NET、C++、C GLib、Go、Java、JavaScript、Julia MATLAB、Python、R、Ruby、Rust、Swift... ・ライブラリやエコシステム NumPy、Pandas、Polars、Hugging Face、PyTorch、TensorFlow DuckDB、Snowflake、BigQuery、Databricks Apache Spark、Apache Flink、Apache Parquet、Apache Iceberg ...

Slide 88

Slide 88 text

内部構造の探索:Pandas編 Apache Arrowの広がり 様々な言語やそれらのライブラリ、エコシステムなどでApache Arrowの サポートが広がってきている ・各プログラミング言語 .NET、C++、C 大規模データを扱う様々なシステムにおいて GLib、Go、Java、JavaScript、Julia 急速にサポートが広がっている MATLAB、Python、R、Ruby、Rust、Swift... ・ライブラリやエコシステム NumPy、Pandas、Polars、Hugging Face、PyTorch、TensorFlow DuckDB、Snowflake、BigQuery、Databricks Apache Spark、Apache Flink、Apache Parquet、Apache Iceberg ...

Slide 89

Slide 89 text

内部構造の探索:Pandas編 Pandasの探索まとめ PandasもNumPyをベースに、内部で様々な工夫があった ・表データの操作に特化したDataFrame ・アライメントやCoWなどによるユーザーの使い勝手の向上 ・BlockManagerによる細かな制御 ・Sparseやカテゴリなどによる大規模データへの効率化 ・PyArrowによる他システムとのスムーズな連携 など 「Pythonで強力なデータフレーム操作を行いたい」 そのような起源を持ち、成熟してきたPandas 今後も継続的な改善でパワーアップが期待される

Slide 90

Slide 90 text

Agenda はじめに 内部構造の探索:NumPy編 内部構造の探索:Pandas編 内部構造の探索:Polars編 まとめ

Slide 91

Slide 91 text

内部構造の探索:Polars編 概要 Polarsはあらゆる規模のデータを処理できるように設計された DataFrameライブラリ 高パフォーマンスかつ表現力豊かなAPIを提供しており、 特に大規模なデータを高速に処理したい場面で威力を発揮する 名称は「Polar Bears」に由来する

Slide 92

Slide 92 text

内部構造の探索:Polars編 基本的な構成 Polarsは根幹的なデータ構造として、下記の三つを持つ ・Series 1次元のラベル付き配列データ、DataFrameの1つの「列」も構成する ・DataFrame 2次元の表形式データを表す、Polarsの中核となるデータ構造 行と列から構成される ・LazyFrame 大規模データを扱うために用意された 「遅延評価(Lazy Evaluation)」専用のデータ構造

Slide 93

Slide 93 text

内部構造の探索:Polars編 簡単な利用シーンを見てみよう

Slide 94

Slide 94 text

内部構造の探索:Polars編 Polarsの基本操作(DataFrame) pip install polars import numpy as np import polars as pl fruits = ["apple", "banana", "cherry", "date"] prices = pl.Series("price", [120, 80, 150, 200]) stocks = np.random.randint(10, 50, size=4) category_ids = [1, 2, 1, 3] df = pl.DataFrame({ "fruit": fruits, "category_id": category_ids, "price": prices, "stock": stocks Pandasにインスパイアされた }) DataFrame

Slide 95

Slide 95 text

内部構造の探索:Polars編 Polarsの基本操作(カラム追加とフィルタ) df_loaded df_processed = df_loaded.with_columns( (pl.col("price") * pl.col("stock")).alias("total_value") ).filter( 直感的な式による表現 pl.col("stock") >= 25 こちらもシンプルかつ高速に処理 )

Slide 96

Slide 96 text

内部構造の探索:Polars編 Polarsの基本操作(データの結合) df_processed 柔軟な指定によるデータの結合 やっぱりシンプルかつ高速に処理 df_categories = pl.DataFrame({ "category_id": [1, 2, 3], "category_name": ["Rosaceae (バラ科)", "Tropical", "Dried Fruit"], }) df_processed.join(df_categories, on="category_id", how="left")

Slide 97

Slide 97 text

内部構造の探索:Polars編 Polarsの基本操作(ファイルIO) csv_filename = "df_pd.csv" # CSVへの読み書き df.to_csv(csv_filename) pl.read_csv(csv_filename) 表データの保存形式一覧 幅広い保存形式に対応 Apache Arrow形式の データのサポートや スキャンによる高速な データ連携が可能 (後述) メソッド名 保存形式 read_csv / write_csv CSV、テキスト read_excel / write_excel エクセル read_parquet / write_parquet Parquet read_ json / write_ json JSON read_ipc / write_ipc (プロセス間通信) Arrowの ネイティブ形式

Slide 98

Slide 98 text

内部構造の探索:Polars編 500万行のデータにおける処理速度の比較 vs Pandas # Pandas(1.36 s) pd_df = pd.read_csv(csv_path) pd_df["total"] = pd_df["price"] * pd_df["quantity"] pd_filtered = pd_df[pd_df["category"].isin(["A", "B"])] pd_filtered.groupby("category").agg(sum_total=("total", "sum")) # Polars(0.15 s) Polarsだと9倍近く速い!!! pl_df = pl.read_csv(csv_path) pl_df.with_columns( (pl.col("price") * pl.col("quantity")) .alias("total")).filter(pl.col("category") .is_in(["A", "B"])).group_by("category").agg(pl.col("total") .sum().alias("sum_total")).sort("category")

Slide 99

Slide 99 text

内部構造の探索:Polars編 PandasとPolarsの比較(主な相違点) Pandas Polars インデックスの有無 行ラベル 自動採番インデックス なし すべてフラットな列データ メモリ構造とストレージ NumPyベース Apache Arrowベース 評価モデル(遅延評価) 即時評価 遅延評価 並列処理の仕組み 基本はシングルスレッド マルチスレッド 記述の思想と一貫性 多様な記法 「式(Expressions)とコン テキスト」 で一貫した記法

Slide 100

Slide 100 text

内部構造の探索:Polars編 Polarsの内部構造を深ぼってみていこう

Slide 101

Slide 101 text

内部構造の探索:Polars編 Polarsのコア Rustによるベースの構築 ・C/C++に匹敵する生パフォーマンス ・GCがなく、予測可能で無駄のないメモリ管理 ・データ競合を防ぎ、安全で高速な並行処理 Apache Arrowのメモリ形式を採用 ・列指向のデータフォーマットを活かした細かな最適化 ・Arrowデータを介した他システムとの連携 ・ストレージの効率的な操作や利便性の高い機能 モダンな技術で基盤を構成し、パフォーマンスを追求

Slide 102

Slide 102 text

内部構造の探索:Polars編 Polarsのデータ型 Polarsのデータ型はApache Arrowに準拠 データ型の例) ・整数型 Int8, Int16, Int32, Int64 UInt8, UInt16, UInt32, UInt64 ・浮動小数点型 Float32, Float64 ・ブール型 Boolean ・文字列・バイナリ型 String, Binary ・日時・時間型 Date, Datetime, Time, Duration ・複合・入れ子型 List, Array, Struct ・特殊型 Categorical, Enum, Null

Slide 103

Slide 103 text

内部構造の探索:Polars編 Pythonバインディング PolarsにおけるPythonバインディングのイメージ Python層 Rust層 Python層 Pythonの GILを解放 Pythonコード LazyFrameの クエリなど マルチスレッドで並列実行 ・SIMD命令 ・ベクトル化バッチ処理など 抽象的なクエリ プランを作成 (DSL / 中間表現) 結果を Apache Arrow の メモリバッファに格納 Arrowの形で受け取り スムーズに処理

Slide 104

Slide 104 text

内部構造の探索:Polars編 ファイルのスキャン機能 Polarsはファイルのスキャン機能も提供 ファイルのメタデータを先読みしてアクセスを最適化する ・min、maxによるデータチェック ・Nullの数と位置の把握し、Nullチェックの効率化 ・複数ファイルのスキャンおよびファイルの開閉是非の判定 # 100万行データ × 3ファイルでフィルタ処理の速度比較(VS read) df_all = pl.read_parquet("sample_data/data_*.parquet") df_eager_result = df_all.filter(pl.col("year") == 2026) # 0.0484 s df_lazy_result = ( scan_parquetだと pl.scan_parquet("sample_data/data_*.parquet") .filter(pl.col("year") == 2026).collect() 約2.8倍速!!! ) # 0.0175 s

Slide 105

Slide 105 text

内部構造の探索:Polars編 遅延評価とクエリ最適化(オプティマイザ)について Polarsの遅延評価はコードを即時実行せず、クエリプランを構築 →オプティマイザが実行前にプランを解析し効率的な順序で実行 df = pl.DataFrame({"id": range(100), "category": ["A", "B"] * 50, "price": [100, 1500, 200, 2500] * 25, "heavy_description": ["Very long text data..." * 10] * 100 }).write_parquet("sample.parquet") lf = pl.scan_parquet("sample.parquet") LazyFrameの例 この段階ではまだ メモリ上にデータを 展開しない

Slide 106

Slide 106 text

内部構造の探索:Polars編 クエリ最適化の流れをみてみよう # 冗長なクエリで対象のデータを取得する処理 query = ( df.select( ["id", "price", "heavy_description"] ).filter(pl.col("price") > 1000) .select(["id", "price"]) ) クエリ実行前に query.explain() query.show_graph() プランを確認できる res = query.collect() # クエリの実行

Slide 107

Slide 107 text

内部構造の探索:Polars編 クエリ最適化の前後比較(explain) query.explain(optimized=False) # 最適化前 query.explain(optimized=True) # 最適化後 ユーザーが書いた冗長なコード も最適化されシンプルな処理に!

Slide 108

Slide 108 text

内部構造の探索:Polars編 クエリ最適化の前後比較(show_graph) query.show_graph(optimized=False) query.show_graph(optimized=True) 最適化の前後比較をグラフ化 処理が効率化されていることが より直感的にわかる

Slide 109

Slide 109 text

内部構造の探索:Polars編 最適化の様々なオプション 最適化のオプション 一例) オプション名 オプションの内容 predicate_pushdown スキャン時にフィルタを適用してメモリを節約する projection_pushdown 必要な列データだけ読み込んでメモリを節約する comm_subexpr_elim 同じ処理をキャッシュして再利用する simplify_expression 冗長な式をシンプルで高速な処理に書き換える slice_pushdown スキャン時にスライスを適用してメモリを節約する LazyFrameでcollectする際はこれらがデフォルトONで実行される

Slide 110

Slide 110 text

内部構造の探索:Polars編 ハードウェア(CPU)の性能を活かす構成 PolarsはCPUの性能を最大限に活かす ・ベクトル化実行 列ごとに同じデータ型の塊(バッチやチャンク)をCPUに連携し、 スムーズかつ高速に処理することができる ・SIMD による並列演算 Apache ArrowのメモリレイアウトでSIMD命令をスムーズに生成する ・マルチスレッド CPUコアを指定し、自動でデータを分割し並列処理する

Slide 111

Slide 111 text

内部構造の探索:Polars編 CPUコア数による処理速度の違い os.environ["POLARS_MAX_THREADS"] = "1" 3000万行のデータ ... で複雑な処理を実行 result = ( df コア数1:2.76 s .group_by("category") .agg([ pl.col("val1").mean().alias("val1_mean"), pl.col("val2").max().alias("val2_max"), pl.col("val1").std().alias("val1_std"), pl.col("val2").quantile(0.5).alias("val2_median"), ]) ) result.collect() if isinstance(result, pl.LazyFrame) else result

Slide 112

Slide 112 text

内部構造の探索:Polars編 CPUコア数による処理速度の違い os.environ["POLARS_MAX_THREADS"] = "8" 3000万行のデータ ... で複雑な処理を実行 result = ( df コア数1:2.76 s .group_by("category") コア数8:0.79 s .agg([ コア数を増やすことで pl.col("val1").mean().alias("val1_mean"), 処理速度があがる pl.col("val2").max().alias("val2_max"), pl.col("val1").std().alias("val1_std"), pl.col("val2").quantile(0.5).alias("val2_median"), ]) ) result.collect() if isinstance(result, pl.LazyFrame) else result

Slide 113

Slide 113 text

内部構造の探索:Polars編 Polarsの並列処理の制御 Polarsにおける並列処理はすべてRust側で管理 Rayon:Rust製の並列処理ライブラリ ループなどの処理をシンプルに並列処理へ変換できるように設計 ・スレッドプールの管理 ・データの分割や各CPUコアへの割り当て ・タスクの動的な負荷分散(Work-stealingアルゴリズム) などの処理を担当 Rustのメモリ安全性の保証のもと ハードウェアの限界までパフォーマンスを引き出す

Slide 114

Slide 114 text

内部構造の探索:Polars編 Polarsの探索まとめ Porlasも次世代のデータフレームとして様々な工夫があった ・Rustによるメモリ安全かつC/C++並みのパフォーマンス ・Apache Arrowのメモリ規格を活用した細かな最適化 ・式とコンテキストによる直感的なコード ・スキャンなどによるストレージの工夫、処理の軽量化 ・遅延評価とクエリ最適化による無駄のない処理 ・CPUを最大限に活かす構成 「既存のデータフレームが抱える処理効率の限界を解決したい」 表データにおける純粋な処理速度だけならPolarsがPandasを圧倒 今後のエコシステムとしての成熟も期待される

Slide 115

Slide 115 text

まとめ それぞれのライブラリの構造が巨大すぎて泣きそうになりました ただ、これまでなんとなく使っていたライブラリがどのような設計思想で どのような作りでどのように動いているかを見ていく中で、各ライブラリ への理解も深められて勉強になりました また調べていく中で、各ライブラリの誕生秘話や歴史的な流れに触れる瞬間 もあり、今後の展望なども知ることができたことは収穫でした 特にApache Arrowの急速な広がりは個人的に驚いた点の一つです AI関連のシステム基盤での利用も含め、さらに発展していくと感じました

Slide 116

Slide 116 text

Appendix 相棒とのやり取り編 今回各ライブラリの内部探索をする上で、相棒(AI)に 助けてもらうシーンがとても多くありました ここからはそんな相棒とのやり取りのワンシーンを残します 原文をそのまま残すため、誤字だったり正確性が欠けている 部分もあるかもしれません あくまでイメージとして捉えてください 今後AIに尋ねて深掘りしたい方に向けて、AIへの尋ね方や そのリアクションの参考となれば幸いです

Slide 117

Slide 117 text

Appendix 相棒への質問(NumPyで色々した調べたあとに思ったことを質問) ここまで見てて少し思ったのですが、ndarrayといいdtypeといい、ブ ロードキャストやインデックシングなども含めて全部NumPyのものとし て実装されてますよね。NumPyの根本の実装ってどうなってるのでしょ うか?PythonのListのメソッド(Lenなど)も使えるということはList を継承してたりするのかと思ったのですが、そのあたりってどうなって るんですか?

Slide 118

Slide 118 text

Appendix 相棒の回答 実は、NumPyの内部実装に関する「PythonのListを継承しているの か?」という疑問は、内部の仕組みを知る上で最も面白くて、かつ多く の人が驚くポイントです。 結論からお伝えすると、NumPyの ndarray は、Pythonの list を一切継 承していません。 全く別の、C言語ベースの独立したデータ構造として 一から作られています。 では、なぜPythonの len() が使えたり、リストのように振る舞ったりで きるのでしょうか? その核心にあるNumPyの根本実装の秘密を解説しま す。

Slide 119

Slide 119 text

Appendix 相棒の回答(続) 1. なぜ len() などのPythonの機能が使えるのか? NumPyの配列(ndarray)がPythonのリストを継承していないにもかか わらず、Pythonの組み込み関数や作法と仲良くできるのは、Pythonが 持つ 「C拡張(C-Extension)」 と 「プロトコル(作法)」 という仕組 みのおかげです。 Pythonは、裏側(CPython)がC言語で動いています。そのため、C言 語で作った独自のオブジェクトに対して「Python風の振る舞いをするた めの専用スロット(API)」を登録することができます。

Slide 120

Slide 120 text

Appendix 相棒の回答(続) len() が動く理由 ndarray のC言語側の構造体の中に、「長さ(要素数)を聞かれたらこの 関数を呼びなさい」という専用の関数ポインタ(スロット)が登録され ています。そのため、Python側で len(arr) と書くと、Pythonはそれを 察知して、C言語側で計算された配列のサイズ(shape[0])を瞬時に返 すようになっています。

Slide 121

Slide 121 text

Appendix 相棒の回答(続) インデクシング(arr[0] など)が動く理由 同様に「要素を指定して取り出されたときはこのCの関数を動かす (Sq_itemスロット)」という取り決めが裏側で結ばれているため、見 た目はPythonのリストと同じように扱えます。 つまり、継承関係にあるわけではなく、Pythonの通訳(インタプリタ) が理解できる「共通の共通言語(Python C API)」を完璧に流暢に話す 外国人のようなイメージです。

Slide 122

Slide 122 text

Appendix 相棒の回答(続) 2. NumPyの根本の実装はどうなっているのか? NumPyの正体は、Pythonで書かれたコードではありません。大半がC言 語(および一部Fortran)で書かれた、低レイヤの巨大なプログラムで す。 その根本の構造を分解すると、主に次の2つの要素で成り立っています。 C言語の構造体(PyArrayObject) C言語レベルで、メモリ上のデータのアドレス(どこからデータが始まっ ているか)、配列の形状(shape)、歩幅(strides)、そしてデータ型 (dtype)などを管理する「超高性能なメタデータ管理ボックス」が定義 されています。

Slide 123

Slide 123 text

Appendix 相棒の回答(続) 生データへのポインタ(Data Buffer) その構造体が、綺麗に整列した1次元のメモリ領域(数値の生データ)を 指し示しています。 私たちがPythonで import numpy as np と書き、np.array([1, 2, 3]) を 実行した瞬間、Pythonは裏側でC言語のプログラムを呼び出し、OSに 「メモリーをこれだけくれ!」と直接要求して、このC言語の構造体をメ モリ上にスパッと構築しています。

Slide 124

Slide 124 text

Appendix 相棒の回答(続) 3. ブロードキャストや演算(+ や *)の正体 「ブロードキャストや足し算(arr1 + arr2)などもすべてNumPyで実装 されている」というご推察の通りです。 Pythonの通常のリストで [1, 2] + [3, 4] をやると、リストの結合([1, 2, 3, 4])になってしまいます。しかし、NumPyの ndarray 同士の足し 算が「要素ごとの足し算([4, 6])」になるのは、Pythonの通常の演算 機能ではなく、NumPyのC言語コード側で「算術演算子の意味を完全に 書き換えている(オーバーライドしている)」からです。 裏側では、C言語や最適化されたCのループ(SIMD命令などを使った超 高速なCPUの並列計算)が走っており、Pythonの「遅い for ループ」を 一切使わずに、一瞬で計算を終わらせて結果を返しています。

Slide 125

Slide 125 text

Appendix 相棒の回答(続) まとめ Pythonの list を継承している? ❌ していない。まったくの別物。 正体は何か? ⭕ C言語で書かれた、メモリ管理と高速計算のための超強力なネイティ ブ・オブジェクト。 なぜPythonらしく動くのか? ⭕ PythonのC API(共通の作法)を完璧に実装しているから。 「見た目はPythonの顔をしていながら、中身はゴリゴリのC言語で最適 化された怪物マシン」──それがNumPyの根本の姿です!

Slide 126

Slide 126 text

参考文献 NumPy https://numpy.org/doc/stable/index.html Pandas https://pandas.pydata.org/ Polars https://pola.rs/ Apache Arrow https://arrow.apache.org/

Slide 127

Slide 127 text

ご清聴ありがとうございました! 引き続き、PyCon JP 2026をお楽しみください