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]
スライドの追加
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を学んでみてはいかがでしょうか。
内容に誤りや不明点があればコメントいただけると幸いです🙇♂️