【備忘録】Pythonイディオム（from DataFrame to List）

# はじめに


フロントエンド([**highchats**](https://www.highcharts.com/demo))にデータを渡す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) が自動的に起きることです。
具体例は、下記のとおりです。(下記は[公式ドキュメント](https://pandas.pydata.org/pandas-docs/version/0.25.3/reference/api/pandas.DataFrame.to_numpy.html)を引用)

<blockquote class="blogcardfu" style="width:auto;max-width:9999px;border:1px solid #E0E0E0;border-radius:3px;margin:10px 0;padding:15px;line-height:1.4;text-align:left;background:#FFFFFF;">
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="gp">&gt;&gt;&gt; </span><span class="n">df</span> <span class="o">=</span> <span class="n">pd</span><span class="o">.</span><span class="n">DataFrame</span><span class="p">({</span><span class="s2">&quot;A&quot;</span><span class="p">:</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">],</span> <span class="s2">&quot;B&quot;</span><span class="p">:</span> <span class="p">[</span><span class="mf">3.0</span><span class="p">,</span> <span class="mf">4.5</span><span class="p">]})</span>
<span class="gp">&gt;&gt;&gt; </span><span class="n">df</span><span class="o">.</span><span class="n">to_numpy</span><span class="p">()</span>
<span class="go">array([[1. , 3. ],</span>
<span class="go">       [2. , 4.5]])</span>
</pre></div>
</div>
</blockquote>

上記のA列に注目すると、
float型に自動変換されていることが分かると思います。(***1*** → ***1.*** , ***2*** → ***2.***)
このような自動変換処理が起きる条件は、
**DataFrameの列の型が、int型とfloat型のみで構成されている**場合に起きています。
実際、下記の例のように、DateTime型が入れば、保存されていることが分かります。
(下記も[公式ドキュメント](https://pandas.pydata.org/pandas-docs/version/0.25.3/reference/api/pandas.DataFrame.to_numpy.html)を引用)

<blockquote class="blogcardfu" style="width:auto;max-width:9999px;border:1px solid #E0E0E0;border-radius:3px;margin:10px 0;padding:15px;line-height:1.4;text-align:left;background:#FFFFFF;">
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="gp">&gt;&gt;&gt; </span><span class="n">df</span><span class="p">[</span><span class="s1">&#39;C&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="n">pd</span><span class="o">.</span><span class="n">date_range</span><span class="p">(</span><span class="s1">&#39;2000&#39;</span><span class="p">,</span> <span class="n">periods</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span>
<span class="gp">&gt;&gt;&gt; </span><span class="n">df</span><span class="o">.</span><span class="n">to_numpy</span><span class="p">()</span>
<span class="go">array([[1, 3.0, Timestamp(&#39;2000-01-01 00:00:00&#39;)],</span>
<span class="go">       [2, 4.5, Timestamp(&#39;2000-01-02 00:00:00&#39;)]], dtype=object)</span>
</pre></div>
</div>
</blockquote>

したがって、次からは、
**int,floatのみ**に起きる型の自動変換する問題を
解決できるコード例を３つ挙げていきます。

### 3. 解決方法1 (zipを用いた内包表現)
```
#df:DataFrame
list(map(list,zip(*[s[1].tolist() for s in df.items()])))
```
上記のコードは理解しにくいと思いますので、
以下に処理フローを示しています。

1. **df.items()**は1行毎の**Series**を返すイテレーターになります。
1. **zip関数**によって、１列ごとの**タプル**を返すイテレータになります。
1. **map関数**と**list関数**によって、**二次元リスト**ができます。

３つの例中、一番可読性の低いコードになっております・・・

### 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']
```
これは３例の中で、一番シンプルになっています。
ですので、[公式ドキュメント](https://pandas.pydata.org/pandas-docs/version/0.23.3/generated/pandas.DataFrame.to_dict.html)を見てもらえば、分かると思います。

# 速度実験 (内包表現 vs to_dict)
続いて、３例の実行速度計測してみました。
計測に用いたDataframeは、**910行 ✕ 4列** (**int**型が1列, **float**型が３列)です。
解決方法1: &nbsp;  **230 ± 16** μs
解決方法2: &nbsp;  **820 ± 40** μs
解決方法3: &nbsp;**2020 ± 64** μs

一番早い方法は、一番複雑だった**zipを用いた内包表記**になりました。
私としては、全く予想外の結果でした。
複雑のことをしていたので、遅くなるかなと思っていたので。
さらに、**zip関数**は、速いということを、気づくことができました。

# まとめ
DataFrameから二重リストを作る際には、
基本方針は、**df.to_numpy().tolist()**
**int,floatの２種類**構成の場合、**zipを用いた内包表記**を使っていこうかと思います。
ただし、このコードの可読性は低いのが、一番の課題です。