[Python]爆速で報告パワポを生成する！

# はじめに
データ分析を行っているとたくさんのグラフやデータが出力されますよね。そんな時に体裁の整ったスライドをサクッと生成できれば、非エンジニアの上司への説明や自分で振り返る時、ライトニングトーク等で役に立ちます。そこで、Pythonからパワポを生成する時に便利な関数を用意しました。

# 出力サンプル
![1a84a9514174c88f216ca83791250eac-0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321692/04d1b4fa-fbad-e411-9324-67908febed81.png)
![1a84a9514174c88f216ca83791250eac-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321692/676d5feb-7331-3a81-5d0a-ab066c1a0121.png)
![1a84a9514174c88f216ca83791250eac-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321692/092707b5-f336-bebe-7b14-aa63b29a47b5.png)
![1a84a9514174c88f216ca83791250eac-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321692/0f5e49a6-5fe3-3c64-67fd-07fe0f7b8af5.png)
![1a84a9514174c88f216ca83791250eac-4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321692/8e8cf3a3-7e5a-af7d-7fef-5a12a2184c71.png)



# パワポを生成するライブラリ
今回使うモジュールはこれです。製作者に感謝。

[python-pptx](https://python-pptx.readthedocs.io/en/latest/#)

インストール方法は

```Python
pip install python-pptx
```

もしくは

```Python
conda install -c conda-forge python-pptx 
```

そして、利用している記事もいくつか存在します。

[Pythonで簡単なパワポファイルの作成 - Qiita](https://qiita.com/daiki7nohe/items/11019e90cd43c82095fa)
[Pythonでパワポの説明資料（報告書）を生成する - Qiita](https://qiita.com/code_440/items/22e8539da465686496d3)
[python-pptxまとめ - Qiita](https://qiita.com/pocket8137/items/3d8fda2c47664bf9130b)

# 今回、関数を作った理由
モジュールが高機能かつ複雑で、サクッと利用できなくてモヤモヤしました。
そこで、シンプルなAzusaをベースに、基本的なスライド様式を生成できるようにしました。

# 前準備
今回は以下のテンプレートを利用してスライドを作ります。

[とりあえずいい感じになるPowerPointテンプレート「Azusa Colors 改」を作った \| Webale](https://miki73.com/2016/06/21/power-point-templets-azusa-colors-kai/)

ダウンロードして以下の変更を加えました。
- 名前の変更
- 全スライドの削除(削除しない場合、スライドが追加されます)
- スライドマスターの変更
  - タイトルスライドを中央揃え
  - メインスライドのプレースホルダの中央配置
  - メインスライドのプレースホルダから段落記号を削除

# 便利関数
よく使うモジュールの機能をまとめました。

```Python:function
# タイトルスライド挿入
def ppt_title(prs,slide_layout,title="タイトル",name="名前"):
    prs.slides.add_slide(slide_layout)
    prs.slides[-1].placeholders[0].text = title
    prs.slides[-1].placeholders[1].text = name
    return prs

# check_placeholdes
def layout_placeholders(prs,layout_num):
    temp = copy.deepcopy(prs)
    slide_layout = temp.slide_layouts[layout_num]
    temp.slides.add_slide(slide_layout)
    for shape in temp.slides[-1].placeholders:
        print('%d %s' % (shape.placeholder_format.idx, shape.name))

# get_size_list
def get_size_list(shape):
    top = shape.top
    left = shape.left
    height = shape.height
    width = shape.width
    size_list = [top,left,height,width]
    return size_list

# プレースホルダサイズ取得
def get_size_placeholders(prs,slide):
    temp = copy.deepcopy(prs)
    shape = temp.slides[slide].placeholders[1]
    top,left,height,width = shape.top,shape.left,shape.height,shape.width
    size_list = [top,left,height,width]
    return size_list

# メインスライド挿入
def ppt_main(prs,slide_layout,title="タイトル"):
    prs.slides.add_slide(slide_layout)
    prs.slides[-1].placeholders[0].text = title
    return prs

# プレースホルダにテキスト挿入
def add_txt(prs,level,size,bold=False,text="main",font_name=False,layout=1):
    tf = prs.slides[-1].placeholders[layout].text_frame
    p = tf.add_paragraph()
    p.level = level
    run = p.add_run()
    run.text = text
    font = run.font
    if font_name:
        font.name = font_name
    font.size = pptx.util.Pt(size)
    font.bold = bold
    return prs

# プレースホルダに最初の段落にテキスト挿入
def add_txt_f(prs,level,size,bold=False,text="main",layout=1):
    tf = prs.slides[-1].placeholders[layout].text_frame
    tf.text = text
    tf.paragraphs[0].font.size = pptx.util.Pt(size)  # font size
    tf.paragraphs[0].font.bold = bold  # font bold
    return prs

# 複数段落分割
def split_txts(txts):
    # txt = ["level0",0,"level1",1]
    txt_list = [txts[idx:idx + 2] for idx in range(0,len(txts), 2)]
    txt_list = [ i + [40,False] for i in txt_list]
    return txt_list

# 複数行入力
def add_txt_multi(prs,txt_list,font_name=False):
    for i in txt_list:
        text,level,size,bold = i
        prs = add_txt(prs,level=level,size=size,text=text,bold=bold,font_name=font_name,layout=1)
    return prs

# 画像インメモリ保存
def save_memory_PIL(PIL_image):
    item = io.BytesIO()
    PIL_image.save(item,"png")
    item.seek(0)
    return item

# 横長アスペクト比チェック
def aspect_yokonaga(width,height):
    result = True
    if int(height/width):
        result = False
    return result,height/width

# プレースホルダ縮小
def shrink_size(size_list,h_mag=1,w_mag=1):
    top,left,height,width = [int(i) for i in size_list]
    height_sh = int(height * h_mag)
    width_sh = int(width * w_mag)
    
    top,left = (top+height)-height_sh,(left+width)-width_sh
    height,width = height_sh,width_sh
    
    size_list = [top,left,height,width]
    size_list = [pptx.util.Length(i) for i in size_list]
    return size_list

# プレースホルダ分割
def split_size(size_list,h_split,w_split,margin=0):
    result = []
    top,left,height,width = size_list
    mar_i = pptx.util.Cm(margin)
    for i in range(1,h_split+1,1):
        for j in range(1,w_split+1,1):
            top_s = top + height*((i-1)/h_split)
            left_s = left + width*((j-1)/w_split)
            height_s = height*(1/h_split)
            width_s = width*(1/w_split)
            top_s,left_s,height_s,width_s = top_s + mar_i ,left_s+mar_i,height_s-2*mar_i,width_s-2*mar_i
            split_temp = top_s,left_s,height_s,width_s
            split_temp = [pptx.util.Length(x) for x in split_temp]
            result.append(split_temp)
    return result

# 画像貼り付け
def image_plot(PIL_img,prs,size_list):
    item = save_memory_PIL(PIL_img)
    slide = prs.slides[-1]
    top,left,height,width = size_list
    img = Image.open(item)
    place_aspect_bool,place_aspect = aspect_yokonaga(width,height)
    image_aspect_bool,image_aspect = aspect_yokonaga(*img.size)
    if place_aspect>=image_aspect:
        pic = slide.shapes.add_picture(item, left=left, top=top,width=width)
    else:
        pic = slide.shapes.add_picture(item, left=left, top=top,height=height)
    pic.left = pic.left+pptx.util.Length((width-pic.width)/2)
    pic.top = pic.top+pptx.util.Length((height-pic.height)/2)
    return prs

# add table
def df_to_table(prs,df,size_list,font_size,RGB=[0,0,0],index=False):
    slide = prs.slides[-1]
    top,left,height,width = size_list
    if index:
        df = df.reset_index()
    row, col = df.shape
    shape = slide.shapes.add_table(rows=row+1, cols=col, left=left, top=top, width=width, height=height)
    table = shape.table
    
    def iter_cells(table):
        for row in table.rows:
            for cell in row.cells:
                yield cell
    
    # columns
    for i,j in enumerate(df.columns):
        cell = table.cell(0,i)
        cell.text = j
    
    # value
    for i in range(0,len(df),1):
        for j in range(0,len(df.columns),1):
            cell = table.cell(i+1,j)
            cell.text =str(df.iat[i,j])
            
    # font_size
    for cell in iter_cells(table):
        for paragraph in cell.text_frame.paragraphs:
            for run in paragraph.runs:
                run.font.size = pptx.util.Pt(font_size)
                run.font.color.rgb = pptx.dml.color.RGBColor(*RGB)
                
    return prs
```

# スライドに入れるデータの準備
スライドに張り付ける画像と表を用意しておきます。

```Python
# generate_image
from PIL import Image, ImageDraw, ImageFont
im_list = []
for i in range(1,5,1):
    im = Image.new("RGB",(128,128),"red")
    draw = ImageDraw.Draw(im)
    font = ImageFont.truetype("arial.ttf", 128)
    draw.text((24,0),str(i),size=100,font=font)
    im_list.append(im)

# generate_df
import pandas as pd
from sklearn import datasets
iris = datasets.load_iris()
df = pd.DataFrame(iris.data, columns=iris.feature_names)
df['target'] = iris.target_names[iris.target]
df = df.head()
```

# スライド生成
以下のコードを実行するとスライドを生成することができます。

```Python
import pptx
import copy
import subprocess
import io
import PIL 

temprate_pptx = "./azusa.pptx"
p_exe = r"C:\Program Files (x86)\Microsoft Office\root\Office16\POWERPNT.EXE"

# set temprate
prs = pptx.Presentation(temprate_pptx)

# set_layout
title_slide_layout = prs.slide_layouts[0]
main_slide_layout = prs.slide_layouts[3]

# add title slide
prs = ppt_title(prs,slide_layout=title_slide_layout)
# add main slide
prs = ppt_main(prs,slide_layout=main_slide_layout)
prs = add_txt_f(prs,0,40,text="first")
prs = add_txt(prs,0,40,text="second")
txts = ["level-0",0,"level-1",1,"level-2",2,"改行_lve\nl-2",2]
txt_list = split_txts(txts)
prs = add_txt_multi(prs,txt_list)

# add image
prs = ppt_main(prs,slide_layout=main_slide_layout,title="単一画像スライド")
size_list = get_size_placeholders(prs,-1)
prs = add_txt(prs,0,40,text="画像説明")
size_list = shrink_size(size_list=size_list,h_mag=0.7)
item = save_memory_PIL(im)
prs = image_plot(im_list[0],prs,size_list)

# add multi image
prs = ppt_main(prs,slide_layout=main_slide_layout,title="マルチ画像スライド")
prs = add_txt(prs,0,40,text="画像説明")
size_list = get_size_placeholders(prs,-1)
size_list = shrink_size(size_list=size_list,h_mag=0.7)
size_lists = split_size(size_list,2,2,margin=1)
for i,j in zip(im_list,size_lists):
    prs = image_plot(i,prs,j)

# add df to table
prs = ppt_main(prs,slide_layout=main_slide_layout,title="表スライド")
slide = prs.slides[-1]
size_list = get_size_placeholders(prs,-1)
size_list = shrink_size(size_list=size_list,h_mag=0.7)
prs = df_to_table(prs,df,size_list,22,index=True)

# export pptx
export_file = "./test.pptx"
prs.save(export_file)

# show pptx
subprocess.Popen([p_exe,export_file])
```

# まとめ
これからデータサイエンスが盛んになってくる時代に、如何にデータ出力から結果アウトプットまで爆速で回せるかが大切になります。
この記事を参考にしてもらって、より良いアウトプットを皆ができるようになればと思います。