はじめに
フロントエンド(highchats)にデータを渡すAPI(python製)を自作する際、
Dataframe (pandas) からlist (二次元リスト) に変形する処理を
実装する機会が、とても増えました。
今回、備忘録も兼ねて、自分なりに良いと思ったコードを紹介したいと思います。
また、検討する際に、行った速度実験結果も示します。
環境 (Python関連)
python 3.7.4
pandas 0.25.3
コード
1. to_numpy().tolist()
# df:DataFrame
df.to_numpy().tolist()
これが、一番シンプルな方法で、
多くの場合は、これで問題ないです。
しかし、ある環境下では、次のような問題が起きます。
2. to_numpy().tolist()の問題点
生じる問題というのは、
型変換 (int → float) が自動的に起きることです。
具体例は、下記のとおりです。(下記は公式ドキュメントを引用)
>>> df = pd.DataFrame({"A": [1, 2], "B": [3.0, 4.5]}) >>> df.to_numpy() array([[1. , 3. ], [2. , 4.5]])
上記のA列に注目すると、
float型に自動変換されていることが分かると思います。(1 → 1. , 2 → 2.)
このような自動変換処理が起きる条件は、
DataFrameの列の型が、int型とfloat型のみで構成されている場合に起きています。
実際、下記の例のように、DateTime型が入れば、保存されていることが分かります。
(下記も公式ドキュメントを引用)
>>> df['C'] = pd.date_range('2000', periods=2) >>> df.to_numpy() array([[1, 3.0, Timestamp('2000-01-01 00:00:00')], [2, 4.5, Timestamp('2000-01-02 00:00:00')]], dtype=object)
したがって、次からは、
int,floatのみに起きる型の自動変換する問題を
解決できるコード例を3つ挙げていきます。
3. 解決方法1 (zipを用いた内包表現)
#df:DataFrame
list(map(list,zip(*[s[1].tolist() for s in df.items()])))
上記のコードは理解しにくいと思いますので、
以下に処理フローを示しています。
- df.items()は1行毎のSeriesを返すイテレーターになります。
- zip関数によって、1列ごとのタプルを返すイテレータになります。
- map関数とlist関数によって、二次元リストができます。
3つの例中、一番可読性の低いコードになっております・・・
4. 解決方法2 (to_numpy & 内包表現)
#df:DataFrame
[array.tolist() for array in df.astype('object').to_numpy()]
ここでのポイントは、**astype('object')**です。
DataFrameの型を無理やり、object型にすると、
int型が維持されていることを見つけました。
(正直、この維持される理由を、しっくり来る説明ができてないです・・・)
5. 解決方法3 (to_dict)
#df:DataFrame
df.to_dict('split')['data']
これは3例の中で、一番シンプルになっています。
ですので、公式ドキュメントを見てもらえば、分かると思います。
速度実験 (内包表現 vs to_dict)
続いて、3例の実行速度計測してみました。
計測に用いたDataframeは、910行 ✕ 4列 (int型が1列, float型が3列)です。
解決方法1: 230 ± 16 μs
解決方法2: 820 ± 40 μs
解決方法3: 2020 ± 64 μs
一番早い方法は、一番複雑だったzipを用いた内包表記になりました。
私としては、全く予想外の結果でした。
複雑のことをしていたので、遅くなるかなと思っていたので。
さらに、zip関数は、速いということを、気づくことができました。
まとめ
DataFrameから二重リストを作る際には、
基本方針は、df.to_numpy().tolist()
int,floatの2種類構成の場合、zipを用いた内包表記を使っていこうかと思います。
ただし、このコードの可読性は低いのが、一番の課題です。