57
60

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

VISITSAdvent Calendar 2019

Day 18

python-pptxでレポーティングを自動化する

Last updated at Posted at 2019-12-17

VISITS Technologies Advent Calendar 2019 18日目は@woods0918が担当します。
前日の@kshibata101の記事AWSしか触ったことのないサーバーエンジニアがGCPで本番運用してみた(後編)が読みごたえのある良記事だったので、是非ご覧ください。

本記事では、「データ分析の結果を定期的にPowerpointでレポーティングしてくれ」という面倒な依頼を快く引受けるために必須のpython-pptxを紹介できればと思います。

python-pptxで何をするか

分析した結果やそこから得られる示唆を自動でPowerpointのレポートにまとめることができます。
例えば、バッチで動いている日次分析処理の最後に、python-pptxによるレポート作成処理とIncoming Webhooks(slack)を使った配信処理を追加すれば、毎日自動的に報告者のもとへレポートが届くようになります。

インストール方法

pip install python-pptx

必要な機能をImport

from pptx import Presentation
from pptx.util import Inches
from pptx.chart.data import ChartData, CategoryChartData, XyChartData, BubbleChartData
from pptx.enum.chart import XL_CHART_TYPE, XL_LEGEND_POSITION, XL_LABEL_POSITION
from pptx.enum.text import PP_ALIGN
from pptx.util import Cm, Pt
from config.setting import _EMUS_PER_CM

pptxファイルの読み込み

新規でPowerpointファイルを作成する場合は、

prs = Presentation()

既存のファイルを読み込む場合には、

prs = Presentation(filepath)

スライドレイアウトの読み込み

prs.slide_layoutsはスライドレイアウトをリスト形式で保持している。スライドレウアウトとは以下の画像のこと。

layout = prs.slide_layouts[i]
スクリーンショット 2019-12-17 22.54.17.png

スライドの追加

slide = prs.slides.add_slide(layout)

タイトルの設定

def add_title(slide, msg):
    """
    args
        slide[slide]: Slide object
        msg[str] : Slide title message
    return:
        None
    """
    shapes = slide.shapes
    shapes.title.text = msg

テキストボックスの生成

以下に記述した設定以外にも、テキストの色など変えることができるので、是非公式ドキュメントを参照してみてください。

def add_text(slide, msg, left, top, width, height, font_size, is_bold):
    """
    args:
        slide[slide]: Slide object
        msg[str]: Text box message
        left[int]: Position from the left end
        top[int] : Position from top
        width[int]: Width of object
        height[int]: Height of object
        font_size[int]: Font size
        is_bold[int]: Whether to make the text bold
    return:
        None
    """
    textbox = slide.shapes.add_textbox(Cm(left), Cm(top), Cm(width), Cm(height))
    p = textbox.textframe.add_paragraph()
    p.text = msg
    p.font.size = Pt(font_size)
    p.font.bold = is_bold

テーブルの挿入

おそらくデータを扱う方だとpandas.DataFrameを使うことが多いと思うので、DataFrameをPowerpointにテーブルオブジェクトとして挿入する関数を定義します。

def add_table(slide, df, left, top, width, height, font_size):
    '''
    args:
        slide[slide]: Slide object
        df[DataFrame] : Display data
        left[int]: Position from the left end
        top[int] : Position from top
        width[int]: Width of object
        height[int]: Height of object
        font_size[int]: Font size
    return:
        None
    '''

    column_names = df.columns.tolist()
    index_names = df.index.tolist()
    col_num = len(column_names) + 1
    row_num = len(index_names) + 1

    table = shapes.add_table(
         row_num, 
         col_num, 
         Cm(left), 
         Cm(top), 
         Cm(width), 
         Cm(height)
    ).table
    
    # Columnの値を挿入 
    for i in range(col_num):
        table.columns[i].width = Cm(width/col_num)
        if i > 0:
            table.cell(0, i).text = column_names[i-1]
            table.cell(0, i).text_frame.paragraphs[0].font.size = Pt(font_size)
            # 中央揃え
            table.cell(0, i).text_frame.paragraphs[0].alignment = PP_ALIGN.CENTER
    
    # Indexの値を挿入
    for i in range(row_num):
        table.rows[i].height = Cm(height/row_num)
        if i > 0:
            table.cell(i, 0).text = index_names[i-1]
            table.cell(i, 0).text_frame.paragraphs[0].font.size = Pt(font_size)
            # 中央揃え
            table.cell(i, 0).text_frame.paragraphs[0].alignment = PP_ALIGN.CENTER
    
    # セルの値を挿入
    for i in range(1, row_num):
        for j in range(1, col_num):
            table.cell(i, j).text = str(df.iloc[i-1, j-1])
            table.cell(i, j).text_frame.paragraphs[0].font.size = Pt(font_size)
            # 中央揃え
            table.cell(i, j).text_frame.paragraphs[0].alignment = PP_ALIGN.CENTER

画像の挿入

def add_img(slide, img_path, left, top, width, height):
    """
    args:
        slide[slide]: Slide object
        img_path[str] : Image file path
        left[int]: Position from the left end
        top[int] : Position from top
        width[int]: Width of object
        height[int]: Height of object 
    return:
        None       
    """
    pic = slide.shapes.add_picture(img_path, 0, 0)
    pic.width = Cm(width)
    pic.height = Cm(height)
    pic.left = Cm(left)
    pic.top = Cm(top)

グラフの挿入

棒グラフ

def bar(slide, categories, values, left, top, width, height, chart_title, x_label, y_label):
    """
    args:
        slide[slide]: Slide object
        categories[List[str]] : X-axis values
        values[Dict[str, List[float]]] : Key=Dataset name, Value=Dataset
        left[int]: Position from the left end
        top[int] : Position from top
        width[int]: Width of object
        height[int]: Height of object 
        chart_title[str] : Chart title
        x_label[str] : X-label title
        y_label[str] : Y-label title
    return:
        None
    """
    chart_data = CategoryChartData()
    chart_data.categories = categories

    for key, value in values.items():
        chart_data.add_series(key, value)
    
    chart = slide.shapes.add_chart( 
        XL_CHART_TYPE.COLUMN_CLUSTERED, 
        Cm(left), 
        Cm(top), 
        Cm(width), 
        Cm(height), 
        chart_data
    ).chart

    chart.font.size = Pt(10)

    if len(values.keys()) > 1:
        chart.has_legend = True
        chart.legend.position = XL_LEGEND_POSITION.RIGHT
        chart.legend.include_in_layout = False
    
    # グラフタイトルの設定
    chart.has_title = True
    chart.chart_title.has_text_frame = True
    chart.chart_title.text_frame.text = chart_title
    chart.chart_title.text_frame.paragraphs[0].font.size = Pt(12)
    value_axis = chart.value_axis
    category_axis = chart.category_axis
  
    # X軸ラベルの設定
    value_axis.has_title = True
    value_axis.axis_title.has_text_frame = True
    value_axis.axis_title.text_frame.text = y_label
    value_axis.axis_title.text_frame.paragraphs[0].font.size = Pt(10)

    # Y軸ラベルの設定
    category_axis.has_title = True
    category_axis.axis_title.has_text_frame = True
    category_axis.axis_title.text_frame.text = x_label
    category_axis.axis_title.text_frame.paragraphs[0].font.size = Pt(10)

    # データラベルの設定
    plot = chart.plots[0]
    plot.has_data_labels = True
    data_labels = plot.data_labels
    data_labels.font.size = Pt(10)
    data_labels.position = XL_LABEL_POSITION.INSIDE_END

散布図

def scatter(slide, data, left, top, width, height, chart_title, x_label, y_label):
    """
    args:
        slide[slide]: Slide object
        data[Dict[str, List[List[float]]]] : Key=Dataset name, Value=Dataset
        left[int]: Position from the left end
        top[int] : Position from top
        width[int]: Width of object
        height[int]: Height of object 
        chart_title[str] : Chart title
        x_label[str] : X-label title
        y_label[str] : Y-label title
    return:
        None
    """
    chart_data = XyChartData()
    
    for key, values in data.items():
        series = chart_data.add_series(key)
        for i in range(len(values[0])):
            series.add_data_point(values[0][i], values[1][i])

    chart = slide.shapes.add_chart(
        XL_CHART_TYPE.XY_SCATTER, 
        Cm(left), 
        Cm(top), 
        Cm(width), 
        Cm(height), 
        chart_data
    ).chart

    chart.font.size = Pt(8)

    # データラベルの設定
    for i, (key, values) in enumerate(data.items()):
        for j, value in enumerate(values[2]):
            chart.series[i].points[j].data_label.has_text_frame = True
            chart.series[i].points[j].data_label.text_frame.text = value
            chart.series[i].points[j].data_label.text_frame.paragraphs[0].font.size = Pt(6)
    
    # Legendの設定
    chart.has_legend = True
    chart.legend.position = XL_LEGEND_POSITION.RIGHT
    chart.legend.include_in_layout = False
    
    # グラフタイトルの設定
    chart.has_title = True
    chart.chart_title.has_text_frame = True
    chart.chart_title.text_frame.text = chart_title
    chart.chart_title.text_frame.paragraphs[0].font.size = Pt(12)

    # X軸ラベルの設定
    category_axis = chart.category_axis
    category_axis.has_title = True
    category_axis.axis_title.has_text_frame = True
    category_axis.axis_title.text_frame.text = x_label
    category_axis.axis_title.text_frame.paragraphs[0].font.size = Pt(10)

    # Y軸ラベルの設定
    value_axis = chart.value_axis
    value_axis.has_title = True
    value_axis.axis_title.has_text_frame = True
    value_axis.axis_title.text_frame.text = y_label
    value_axis.axis_title.text_frame.paragraphs[0].font.size = Pt(10)

円グラフ

def pie(slide, categories, values, left, top, width, height, chart_title):
    """
    args:
        slide[slide]: Slide object
        categories[List[str]] : The name of each data
        values[List[float]] : Dataset
        left[int]: Position from the left end
        top[int] : Position from top
        width[int]: Width of object
        height[int]: Height of object 
        chart_title[str] : Chart title
    return:
        None
    """

    chart_data = ChartData()
    chart_data.categories = categories

    chart_data.add_series("Pie", values)
    
    chart = slide.shapes.add_chart(
        XL_CHART_TYPE.PIE, 
        Cm(left), 
        Cm(top), 
        Cm(width), 
        Cm(height), 
        chart_data
    ).chart

    chart.font.size = Pt(8)

    chart.has_legend = True
    chart.legend.position = XL_LEGEND_POSITION.RIGHT
    chart.legend.include_in_layout = False
    
    # グラフタイトルの設定
    chart.has_title = True
    chart.chart_title.has_text_frame = True
    chart.chart_title.text_frame.text = chart_title
    chart.chart_title.text_frame.paragraphs[0].font.size = Pt(11)

    # データラベルの設定
    chart.plots[0].has_data_labels = True
    data_labels = chart.plots[0].data_labels
    if is_percentile:
        data_labels.number_format = '0%'
    data_labels.position = XL_LABEL_POSITION.INSIDE_END

その他

上記のグラフ以外にも、バブルチャートや折れ線グラフを挿入することもできます。
バブルチャートであれば散布図を参考に、折れ線グラフであれば棒グラフを参考にしていただくと、簡単に実装できるかと思いますので、是非チャレンジしてみてください。

スライドの保存

prs.save(filepath)

まとめ

JupyterやGoogle Colaboratoryでレポートを共有できると楽なのですが、やはりビジネスにおける資料作成のデファクトであるPowerpointでのレポートを求められる機会はどうしても発生し得るかと思います。
そのような時でも涼しい顔で対応できるように、是非python-pptxを学んでみてはいかがでしょうか。

内容に誤りや不明点があればコメントいただけると幸いです🙇‍♂️

57
60
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
57
60

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?