pandasのDataFrameを元に、画像入りの表をつくる

# 概要
機械学習をしていると、データの可視化をしたいことが多く、ときたま画像も入った表を出したくなることがある。
（↓例えばこんなの。画像認識したときのネコである判定スコアとか。）

![image.png](https://qiita-image-store.s3.amazonaws.com/0/20539/5fecd759-cc8d-624a-62a7-9e12624ad6d9.png)

データ可視化はExcelとか、pandasとか使うことが多いが、数値や文字列程度ならいいものの、画像の入った表はパパっと作る方法がすぐには思いつかなかったりする。
今回はpandasのDataFrameを元データとして、画像入りの表をできるだけ簡単に作る方法を検討したので、メモしておく。

今回メモする方法は下記3つ。
1. DataFrame.to_html() を使ってHTMLにする
2. DataFrame.to_dict() とjinja2を使ってHTMLにする
3. DataFrame.to_excel() を使って作ったExcelファイルにopenpyxlで画像を入れる

## 1. DataFrame.to_html() を使ってHTMLにする

DataFrameには[to_html](https://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.to_html.html)というメソッドがあり、htmlへの変換は簡単にできる。

```data.csv
black cat,static/imgs/000.jpg,0.98516
white cat,static/imgs/001.jpg,0.74079
my cat,static/imgs/002.jpg,0.99067
```

```python
import pandas as pd

df = pd.read_csv("data.csv", names=["name", "path", "cat score"])
print(df.to_html())
```

出力は以下の通り。

```html
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>name</th>
      <th>path</th>
      <th>cat score</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>black cat</td>
      <td>static/imgs/000.jpg</td>
      <td>0.98516</td>
    </tr>
    <tr>
      <th>1</th>
      <td>white cat</td>
      <td>static/imgs/001.jpg</td>
      <td>0.74079</td>
    </tr>
    <tr>
      <th>2</th>
      <td>my cat</td>
      <td>static/imgs/002.jpg</td>
      <td>0.99067</td>
    </tr>
  </tbody>
</table>
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/20539/bedd18a0-06ea-b20f-290c-c3a891a58847.png)



これをhtmlのひな型文字列に入れてあげれば、html全体を作れる。
引数classesでtableタグのclassの指定ができるので、[bootstrap](https://getbootstrap.com/)などと合わせて使うことも可能。

```python
import pandas as pd

html_template = """
<!doctype html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
  </head>
  <body>
    <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
    <div class="container">
        {table}
    </div>
  </body>
</html>
"""


df = pd.read_csv("data.csv", names=["name", "path", "cat score"])
table = df.to_html(classes=["table", "table-bordered", "table-hover"])
html = html_template.format(table=table)

with open("test.html", "w") as f:
    f.write(html)
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/20539/38504f9a-5ef3-798c-c0cd-49d15ce0f869.png)




### 画像を入れる

画像を入れるにはDataFrameの要素をHTMLのimgタグにする。
ただしそのままだとHTML表示向けにエスケープされてしまうので、to_htmlの引数でescape=Falseを指定する。

逆にエスケープしないといけない文字が入っている列については、標準ライブラリの[html.escape](https://docs.python.jp/3/library/html.html)を使って自分でエスケープしておく必要がある。

```data.csv
<black cat>,static/imgs/000.jpg,0.98516
<white cat>,static/imgs/001.jpg,0.74079
<my cat>,static/imgs/002.jpg,0.99067
```

```python
import pandas as pd
import html

df = pd.read_csv("data.csv", names=["name", "path", "cat score"])
# imgタグの文字列へ変換
df["image"] = df["path"].map(lambda s: "<img src='{}' width='200' />".format(s))
# 必要な列について、html向けにエスケープ
df["name"] = df["name"].map(lambda s: html.escape(s))

table = df.to_html(classes=["table", "table-bordered", "table-hover"], escape=False)
html_str = html_template.format(table=table)

with open("test.html", "w") as f:
    f.write(html_str)
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/20539/5fecd759-cc8d-624a-62a7-9e12624ad6d9.png)


## 2. DataFrame.to_dict() とjinja2を使ってHTMLにする

DataFrameの[to_dict](https://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.to_dict.html)メソッドを使うと辞書型にできるので、それを[jinja2](http://jinja.pocoo.org/docs/2.10/)に渡してしまう方法。
ポイントは、

* to_dictにて、各行の各列要素をOrderedDictにしたリストに変換する
* jinjaにて辞書のキーをif文で判定し、imgタグに変換する

```python
import pandas as pd
from jinja2 import Environment, FileSystemLoader
from collections import OrderedDict

df = pd.read_csv("data.csv", names=["name", "path", "cat score"])
# 3列目に画像のパスが入った列を挿入する。この列をテンプレート内でimgタグに変換する。
df.insert(2, "image", df["path"])
# DataFrameを、各行の各列要素をOrderedDictにしたリストに変換する。
data = df.to_dict("records", into=OrderedDict)

# jinja2のテンプレートのロード。html向けに自動でエスケープする設定で読み込む
env = Environment(loader=FileSystemLoader("."), autoescape="html")
tmpl = env.get_template("template.html")

# 辞書型のデータをインプットしてhtml作成。UTF8でエンコードする
html = tmpl.render(data=data).encode("utf8")

# UTF8でエンコードしたのでバイナリで書き出す必要あり
with open("test.html", "wb") as f:
    f.write(html)
```

```template.html
<!doctype html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
</head>
<body>
    <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
    <div class="container">
        <table class="dataframe table table-bordered table-hover">
            <thead>
                <tr>
                {% for k in data[0] %}
                    <th>{{ k }}</th>
                {% endfor %}
                </tr>
            </thead>
            <tbody>
            {% for d in data %}
                <tr>
                    {% for k in d %}
                    {% if k.startswith("image") %}
                    <td><img src="{{ d[k] }}" width="200px"/></td>
                    {% else %}
                    <td>{{ d[k] }}</td>
                    {% endif %}
                    {% endfor %}
                </tr>
            {% endfor %}
            </tbody>
        </table>
    </div>
</body>
</html>

```

これで1と同じ結果のHTMLが得られます。

## 3. DataFrame.to_excel() を使って作ったExcelファイルにopenpyxlで画像を入れる

DataFrameの[to_excel](https://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.to_excel.html)で表をExcelファイルに書き出すことができます。
[openpyxl](https://openpyxl.readthedocs.io/en/stable/)はExcelファイルを直接編集できるpythonライブラリです。
これらを両方使ってExcelファイルで画像入りの表を作ります。

この方法は最終出力がExcelファイルでなので、あとからいろいろデータ分析作業でいじくりまわす場合に特に有用です。
画像はセル内に貼り付けられるため、フィルタやソートをしたときにちゃんとセルについてきます。

```python
import pandas as pd
import openpyxl
from openpyxl.drawing.image import Image

df = pd.read_csv("data.csv", names=["name", "path", "cat score"])
# 3列目に画像のパスが入った列を挿入する。この列をテンプレート内でimgタグに変換する。
df.insert(2, "image", df["path"])

# 画像埋め込み先の列
IMAGE_COL = "C"

# Excel変換
xl_path = "table.xlsx"
df.to_excel(xl_path, index=False)

# Excelロード
wb = openpyxl.load_workbook(xl_path)
# Activeシート取得
ws = wb.active

# 列幅を調整
for c in ws.columns:
    ws.column_dimensions[c[0].column].width = 30

def image_column(worksheet, image_column):
    ws = worksheet

    # 列数を取得
    R_NUM = len(list(ws.rows))

    # 画像貼り付け
    # 行番号は1始まり。1行目はヘッダなので飛ばす
    for i in range(1, R_NUM):
        idx = i + 1
        cell_name = image_column + str(idx)
        c = ws[cell_name]
        try:
            # 画像ファイルのロード
            img = Image(c.value)
            # 画像の幅を200pxにする
            aspect = float(img.height) / img.width
            img.width = 200
            img.height = aspect * 200
            # 画像を貼り付ける
            ws.add_image(img, cell_name)
            # セルの高さを調整
            ws.row_dimensions[idx].height = img.height
            # セルの文字列は消しておく
            c.value = ""
        except FileNotFoundError:
            # 画像ファイルが見つからない場合はスキップ
            pass

image_column(ws, IMAGE_COL)

# 保存
wb.save(xl_path)
```

これで↓のようなExcelファイルが作れます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/20539/b5a04551-1093-ecf0-bdbe-aba2bf716704.png)


## データ分析・レポート用向けの工夫

- HTMLの場合
    - jqueryの[datatables](https://datatables.net/)と組み合わせると簡単にソートや検索機能を付けられます。
    - [Flask](http://flask.pocoo.org/)と合わせて使えばwebアプリにできます。
    - 2の方法でthやtrに列名のclassを割り当てておくと、色を付けるなどの処理がjavascript側でできるようになります。
- Excelの場合
    - openpyxlの他のメソッドを使えば、罫線・数式・条件付き書式などできることは幅広いので、応用が利きます。


## Github
https://github.com/nshinya/image_table