【Python】XlsxWriterとpandasでグラフ付きExcel売上レポート作成CLIツールを作ってみた
公開:
売上CSVを渡すだけで、明細・集計表・グラフまで入ったExcelレポートが出てくるCLIツールをPythonで作りました。集計はpandasのgroupby、Excel生成はXlsxWriterに任せ、コマンドの入口はargparseのサブコマンドでまとめています。
サンプルCSVの作成、集計内容の表示、レポート出力までを4回のコマンドで通し、いずれも終了コード0で完了しました。ファイル1本で完結する構成なので、手元にコピーすればそのまま追体験できます。
XlsxWriterの基本概念、要件定義、実装、動作確認までを順番に学べる構成です。動画は目次から確認したい場面へ移動でき、本文だけでも手順と考え方が完結します。
動画の内容をテキストで確認する
オープニング。XlsxWriterとpandasを使ってグラフ付きExcel売上レポートCLIを作るカリキュラムを始めます。概要紹介。
XlsxWriterとpandasの役割と使い方を学ぶ グラフ付きExcel売上レポートCLIの要件を整理する 完成コードと実行結果を確認する 最後に実コマンドとファイル状態で完成挙動を確かめる 具体的にやること。
sampleコマンドでサンプル売上CSVを生成する --rowsと--seedで件数と乱数シードを指定するsummaryコマンドで月別集計と商品別集計を画面へ表示するreportコマンドでグラフ付きExcelを出力する 単価と数量の積を売上金額の列として計算する 実装環境・必須アプリ。
OS:Windows 11 Pro Python:3.13.3シェル:PowerShell 7.6.4必須アプリ:コードエディター、ターミナル、エクスプローラー パッケージ:pip、pandas、XlsxWriter XlsxWriterとpandasとは。
XlsxWriter:XlsxWriterは、Pythonのコードから拡張子がxlsxのExcelブックを新しく書き出すためのライブラリpandas:pandasは、CSVのような表形式データをDataFrameという表オブジェクトとして扱えるデータ分析ライブラリ XlsxWriterで作る売上レポートCLIの要点。
sampleはwrite_sample_csvでCSVを作るsummaryはformat_summaryの文字列を表示するreportはwrite_excel_reportでxlsxを書く グラフ付きExcel売上レポートCLIの要件定義。
sampleコマンドが指定件数のCSVを作成する 作成したCSVが6列の構成になるsummaryコマンドが対象期間と売上合計を表示する 月別集計が年月の昇順で並ぶ 商品別集計が売上金額の降順で並ぶ 構成比がパーセント表示になる INTRO: Monaco Editorでグラフ付きExcel売上レポートCLIを実装。
コードを1行ずつ入力し、補完と自動インデントを使いながら実行結果を確認します。LINE 003: モジュール全体の説明文。このファイル全体が何をするツールかを説明するドキュメント文字列です。
売上CSVを集計してグラフ付きExcelレポートを作るCLIツールであることを示しています。LINE 005: 将来のアノテーション機能を有効化。型ヒントの評価方法を将来のPythonの挙動に合わせるためのインポートです。
これにより関数の引数や戻り値の型注釈を柔軟に書けるようになります。LINE 007: コマンドライン引数解析用モジュール。argparseモジュールを読み込んでいます。
sample・summary・reportといったサブコマンドを扱うために使用します。LINE 008: OS操作用モジュール。osモジュールを読み込んでいます。
ファイルパスの絶対パス化やフォルダ作成、ファイルサイズ取得などに使用します。LINE 009: 乱数生成用モジュール。randomモジュールを読み込んでいます。
サンプルデータをランダムに生成する際に使用します。LINE 010: 日付操作用クラスの読み込み。datetimeモジュールからdateとtimedeltaを読み込んでいます。
サンプルデータの売上日を計算するために使用します。LINE 012: データ分析ライブラリの読み込み。pandasライブラリをpdという別名で読み込んでいます。
CSVの読み込みや集計処理の中心となるライブラリです。LINE 013: Excel出力ライブラリの読み込み。xlsxwriterライブラリを読み込んでいます。
グラフ付きのExcelファイルを作成するために使用します。LINE 017: 商品マスタの定義開始。サンプルデータ生成に使う商品情報のリストを定義し始めています。
商品名・カテゴリ・単価の組み合わせをここから列挙します。LINE 018: 商品マスタ1件目。商品名「ブレンドコーヒー豆」、カテゴリ「食品」、単価1200円という商品情報の1件目を定義しています。
LINE 019: 商品マスタ2件目。商品名「有機緑茶ティーバッグ」、カテゴリ「食品」、単価800円という商品情報の2件目を定義しています。LINE 020: 商品マスタ3件目。
商品名「ステンレスボトル」、カテゴリ「雑貨」、単価2800円という商品情報の3件目を定義しています。LINE 021: 商品マスタ4件目。商品名「陶器マグカップ」、カテゴリ「雑貨」、単価1500円という商品情報の4件目を定義しています。
LINE 022: 商品マスタ5件目。商品名「ドリップスタンド」、カテゴリ「調理器具」、単価3600円という商品情報の5件目を定義しています。LINE 023: 商品マスタ6件目。
商品名「電動コーヒーミル」、カテゴリ「調理器具」、単価5400円という商品情報の6件目を定義しています。LINE 024: 商品マスタ定義の終了。PRODUCT_MASTERリストの定義を閉じています。
ここまでの6商品がサンプルデータ生成の材料になります。LINE 025: 地域リストの定義。サンプルデータで使用する地域名のリストを定義しています。
東京・大阪・名古屋・福岡の4地域からランダムに選ばれます。LINE 026: CSV列名の定義。売上CSVに含まれる列名の並び順をリストとして定義しています。
この列構成がサンプル出力やCSV読み込み時のチェックに使われます。LINE 027: 明細シート用の列名を拡張。CSV列名のリストに「売上金額」を加えて、Excelの明細シート用の列名リストを作っています。
LINE 028: サンプルデータの開始日を定義。サンプル売上データの起点となる日付を2024年1月1日として定義しています。LINE 029: サンプルデータの期間日数を定義。
サンプルデータが生成される日数の範囲を182日として定義しています。開始日からこの日数以内でランダムに日付が選ばれます。LINE 030: 明細シート名の定義。
Excel出力時に使用する明細シートの名前を「明細」という文字列で定義しています。LINE 031: 月別集計シート名の定義。Excel出力時に使用する月別集計シートの名前を「月別集計」という文字列で定義しています。
LINE 032: 商品別集計シート名の定義。Excel出力時に使用する商品別集計シートの名前を「商品別集計」という文字列で定義しています。LINE 033: グラフシート名の定義。
Excel出力時に使用するグラフシートの名前を「グラフ」という文字列で定義しています。RUN 1/9: 定数とマスタデータを置いた時点の確認。商品マスタ、列名、シート名、サンプル期間を定数としてまとめた状態です。
後続の関数がどの値を共有するのかを、一覧にして確かめておきます。CHECK 1/9: 途中実行に成功。商品マスタ、列名、シート名、サンプル期間を定数としてまとめた状態です。
後続の関数がどの値を共有するのかを、一覧にして確かめておきます。RETURN 01: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。
LINE 038: フォルダ作成関数の定義開始。出力先パスの親フォルダを準備するための関数ensure_parent_dirを定義しています。引数にはパス文字列を受け取ります。
LINE 039: 関数の説明文。この関数が出力パスの親フォルダを必要に応じて作成する処理であることを説明するドキュメント文字列です。LINE 040: 親フォルダのパスを取得。
渡されたパスを絶対パスに変換し、そのフォルダ部分だけを取り出してparent変数に格納しています。LINE 041: フォルダ有無の判定。parentが空文字でなければ、つまりフォルダパスが存在する場合に次の処理へ進む条件分岐です。
LINE 042: フォルダの作成。指定したフォルダが存在しない場合に新しく作成しています。すでに存在してもエラーにならないようにexist_okをTrueにしています。
RUN 2/9: 出力先フォルダを用意する関数の確認。ensure_parent_dirを書き終えた状態です。まだ存在しない入れ子のフォルダを指定し、呼び出しの前後でフォルダが作られることを見ます。
CHECK 2/9: 途中実行に成功。ensure_parent_dirを書き終えた状態です。まだ存在しない入れ子のフォルダを指定し、呼び出しの前後でフォルダが作られることを見ます。
RETURN 02: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。LINE 047: サンプルデータ生成関数の定義開始。
件数とシード値を受け取り、擬似的な売上明細データを作るbuild_sample_frame関数を定義しています。LINE 048: 関数の説明文。シード値を固定することで再現性のある擬似売上明細を、pandasのDataFrame形式で作成する処理であることを説明しています。
LINE 049: 件数の妥当性チェック。引数rowsが1未満かどうかを判定しています。不正な件数指定を早い段階で検出するための条件分岐です。
LINE 050: 不正な件数のエラー通知。件数が1未満だった場合に、エラーメッセージ付きでValueErrorを発生させ処理を中断しています。LINE 052: 乱数生成器の作成。
渡されたseed値を使って乱数生成器rngを作成しています。同じシードを指定すれば毎回同じデータが再現できます。LINE 053: レコード格納用リストの初期化。
生成した売上明細のデータを1件ずつ格納していくための空リストrecordsを用意しています。LINE 054: 件数分の繰り返し処理開始。指定された件数分だけループを繰り返し、1件ずつ売上データを作成していく処理の開始です。
LINE 055: 商品情報のランダム選択。商品マスタの中からランダムに1件を選び、商品名・カテゴリ・単価をそれぞれの変数に取り出しています。LINE 056: 売上日のランダム生成。
開始日にランダムな日数を加算することで、サンプルデータの売上日をランダムに決定しています。LINE 057: 明細レコードの追加開始。1件分の売上データを辞書としてrecordsリストに追加する処理を開始しています。
LINE 058: 辞書データの開始。1件分の売上明細を表す辞書オブジェクトの定義を開始しています。LINE 059: 売上日の格納。
生成した売上日をISO形式の文字列に変換して、辞書の「売上日」キーに格納しています。LINE 060: 商品名の格納。先ほど選んだ商品名を辞書の「商品名」キーに格納しています。
LINE 061: カテゴリの格納。先ほど選んだカテゴリを辞書の「カテゴリ」キーに格納しています。LINE 062: 地域のランダム選択と格納。
地域リストからランダムに1つ選び、辞書の「地域」キーに格納しています。LINE 063: 単価の格納。先ほど選んだ単価の値を辞書の「単価」キーに格納しています。
LINE 064: 数量のランダム生成と格納。1から8の範囲でランダムな数量を生成し、辞書の「数量」キーに格納しています。LINE 065: 辞書データの終了。
1件分の売上明細を表す辞書オブジェクトの定義を閉じています。LINE 066: リストへの追加処理の終了。作成した辞書をrecordsリストへ追加する処理の呼び出しを閉じています。
LINE 068: DataFrameへの変換。収集したrecordsリストを、指定した列順でpandasのDataFrameに変換しています。LINE 069: 日付順の並べ替えと返却。
作成したDataFrameを売上日順に並べ替え、インデックスを振り直してから関数の戻り値として返しています。RUN 3/9: サンプル明細のDataFrameを確認する。build_sample_frameが完成した状態です。
シードを固定して少量の明細を作り、列の並びと先頭行の内容を目で確かめます。CHECK 3/9: 途中実行に成功。build_sample_frameが完成した状態です。
シードを固定して少量の明細を作り、列の並びと先頭行の内容を目で確かめます。RETURN 03: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。
LINE 072: サンプルCSV保存関数の定義開始。パスと件数、シード値を受け取り、サンプル売上CSVをファイルに保存するwrite_sample_csv関数を定義しています。LINE 073: 関数の説明文。
この関数がサンプル売上CSVをBOM付きのUTF-8で保存する処理であることを説明するドキュメント文字列です。LINE 074: サンプルデータの生成呼び出し。先ほど定義したbuild_sample_frame関数を呼び出し、指定件数分のサンプル売上データを作成しています。
LINE 075: 出力フォルダの準備呼び出し。ensure_parent_dir関数を呼び出し、出力先パスの親フォルダが無ければ作成しています。LINE 076: CSVファイルへの書き出し。
作成したDataFrameを、インデックス列を含めずBOM付きUTF-8エンコーディングでCSVファイルとして保存しています。LINE 077: 結果情報の辞書返却開始。処理結果をまとめた辞書を関数の戻り値として返す処理を開始しています。
LINE 078: 出力パスの記録。保存したCSVファイルの絶対パスを辞書の「path」キーに格納しています。LINE 079: 件数の記録。
生成した明細データの件数を辞書の「rows」キーに整数として格納しています。LINE 080: 最初の売上日の記録。データの先頭にある売上日を文字列に変換し、辞書の「first_date」キーに格納しています。
LINE 081: 最後の売上日の記録。データの末尾にある売上日を文字列に変換し、辞書の「last_date」キーに格納しています。LINE 082: 商品数の記録。
生成データに含まれるユニークな商品名の数を数え、辞書の「products」キーに格納しています。LINE 083: 結果辞書の定義終了。write_sample_csv関数が返す結果情報の辞書の定義を閉じています。
RUN 4/9: サンプルCSVの書き出しを確認する。write_sample_csvまで入力した状態です。一時フォルダへCSVを保存し、返ってくる件数や期間の情報とファイルの有無を確認します。
CHECK 4/9: 途中実行に成功。write_sample_csvまで入力した状態です。一時フォルダへCSVを保存し、返ってくる件数や期間の情報とファイルの有無を確認します。
RETURN 04: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。LINE 088: CSV読み込み関数の定義開始。
パスを受け取り、売上CSVを読み込んで前処理を行うload_sales関数を定義しています。LINE 089: 関数の役割説明。この関数がCSVを読み込んでから型変換や列追加までを行う処理であることを示すドキュメント文字列です。
LINE 090: ファイル存在チェック。指定されたパスにファイルが存在するかどうかを確認しています。存在しない場合は次の行でエラーを発生させる準備です。
LINE 091: ファイル未検出エラー。CSVファイルが見つからない場合に、絶対パス付きのメッセージとともにFileNotFoundErrorを発生させています。LINE 093: CSV読み込み。
pandasのread_csvを使ってCSVファイルを読み込み、DataFrameとして変数frameに格納しています。文字コードはBOM付きUTF-8を指定しています。LINE 094: 欠落列の抽出。
必要な列名一覧CSV_COLUMNSのうち、読み込んだDataFrameに存在しない列名だけをリストとして取り出しています。LINE 095: 欠落列の判定。missingリストに何か要素が入っている、つまり必要な列が足りない場合に次の行の処理へ進みます。
LINE 096: 列不足エラー。不足している列名をカンマ区切りで表示しながらValueErrorを発生させ、処理を中断しています。LINE 098: 売上日の型変換。
売上日列の文字列を日付型に変換しています。変換できない値はエラーにせずNaT(欠損値)にしています。LINE 099: 単価の型変換。
単価列を数値型に変換しています。数値に変換できない値は欠損値として扱われます。LINE 100: 数量の型変換。
数量列を数値型に変換しています。単価と同様に変換できない値は欠損値になります。LINE 101: 欠損行の除去。
売上日・単価・数量のいずれかが欠損している行を取り除き、コピーを作成して以降の処理で安全に使えるようにしています。LINE 102: データ空チェック。欠損値を除去した結果、DataFrameが空になっていないかを確認しています。
LINE 103: 空データエラー。集計できる明細が1件も残っていない場合に、その旨のメッセージとともにValueErrorを発生させています。LINE 105: 売上金額列の作成。
単価と数量を掛け合わせて新しく売上金額という列を作成しています。この列が以降の集計処理の元になります。LINE 106: 月列の作成。
売上日から年月部分だけを取り出し、月別集計に使う月という列を新たに追加しています。LINE 107: 日付順に整形して返却。売上日の昇順に並べ替え、インデックスを振り直したDataFrameを戻り値として返しています。
RUN 5/9: CSV読み込みと列追加を確認する。load_salesを書き終えた状態です。保存したCSVを読み直し、売上日が日付型になっていることと、売上金額と月の列が増えたことを見ます。
CHECK 5/9: 途中実行に成功。load_salesを書き終えた状態です。保存したCSVを読み直し、売上日が日付型になっていることと、売上金額と月の列が増えたことを見ます。
RETURN 05: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。LINE 112: 集計関数の定義。
整形済みの売上明細DataFrameを受け取り、月別集計と商品別集計をまとめたdictを作る関数を定義しています。LINE 113: 関数の役割説明。groupbyを使って月別集計と商品別集計を作成し、構成比を付け加える処理であることを示すドキュメント文字列です。
LINE 114: 月別集計の開始。月ごとの集計結果を作るための処理をここから複数行に分けて記述し、最終的にmonthlyという変数に代入しています。LINE 115: 月でグルーピング。
売上明細を月列の値ごとにグループ化し、グループを示す列がそのまま残るようにインデックスをリセットしています。LINE 116: 集計方法の指定。グループごとに売上金額と数量は合計を、売上日は件数をカウントするよう集計方法を指定しています。
LINE 117: 列名の変更。件数を表す売上日列の名前を、わかりやすい件数という名前に変更しています。LINE 118: 月順に並べ替え。
集計結果を月の昇順に並べ替え、表示や利用がしやすい順序にしています。LINE 119: インデックスの振り直し。並べ替え後にばらばらになったインデックスを0から振り直し、きれいなDataFrameに整えています。
LINE 120: 月別集計の完了。ここまでの処理の結果を月別集計としてmonthly変数に確定させています。LINE 122: 商品別集計の開始。
商品ごとの集計結果を作るための処理をここから複数行に分けて記述し、最終的にproduct変数に代入しています。LINE 123: 商品名とカテゴリでグルーピング。売上明細を商品名とカテゴリの組み合わせごとにグループ化し、集計の単位を作っています。
LINE 124: 集計方法の指定。グループごとに売上金額と数量をそれぞれ合計するよう指定しています。LINE 125: 売上金額の降順に並べ替え。
集計結果を売上金額が多い順に並べ替え、売れている商品が上に来るようにしています。LINE 126: インデックスの振り直し。並べ替え後のインデックスを0から振り直し、扱いやすいDataFrameに整えています。
LINE 127: 商品別集計の完了。ここまでの処理の結果を商品別集計としてproduct変数に確定させています。LINE 129: 売上合計の算出。
全明細の売上金額を合計し、float型に変換して総売上金額を求めています。LINE 130: 構成比列の作成。各商品の売上金額を売上合計で割り、構成比という新しい列を追加しています。
合計が0の場合は0.0を設定して割り算エラーを防いでいます。LINE 132: 集計結果の返却。月別集計、商品別集計、売上合計をひとまとめのdictにして呼び出し元に返しています。
RUN 6/9: 月別と商品別の集計結果を確認する。aggregate_salesまで進んだ状態です。読み込んだ明細から2つの集計表を作り、行数と並び順、売上合計を確認します。
CHECK 6/9: 途中実行に成功。aggregate_salesまで進んだ状態です。読み込んだ明細から2つの集計表を作り、行数と並び順、売上合計を確認します。
RETURN 06: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。LINE 137: 文字列整形関数の定義。
集計結果を画面表示用の文字列に整形する関数を定義しています。LINE 138: 関数の役割説明。集計表の数値を3桁区切りやパーセント表示に整えて文字列を組み立てる処理であることを示すドキュメント文字列です。
LINE 139: 月別集計のコピー。元の集計結果を書き換えないよう、月別集計のコピーを作成して表示用の変数monthlyに代入しています。LINE 140: 売上金額の表示整形。
月別集計の売上金額列を3桁区切りの文字列表示に変換しています。LINE 141: 数量の表示整形。月別集計の数量列を3桁区切りの文字列表示に変換しています。
LINE 142: 件数の表示整形。月別集計の件数列を3桁区切りの文字列表示に変換しています。LINE 144: 商品別集計のコピー。
元の集計結果を書き換えないよう、商品別集計のコピーを作成して表示用の変数productに代入しています。LINE 145: 売上金額の表示整形。商品別集計の売上金額列を3桁区切りの文字列表示に変換しています。
LINE 146: 数量の表示整形。商品別集計の数量列を3桁区切りの文字列表示に変換しています。LINE 147: 構成比の表示整形。
商品別集計の構成比列を小数点1桁のパーセント表示に変換しています。LINE 149: 表示行リストの開始。画面に出力する文字列を1行ずつ格納するリストの作成を開始しています。
LINE 150: 見出しの追加。集計対象の情報を表す見出し文字列をリストに追加しています。LINE 151: ファイルパスの追加。
読み込んだCSVの絶対パスを表示するための行をリストに追加しています。LINE 152: 明細件数の追加。読み込んだ明細の件数を3桁区切りで表示するための行をリストに追加しています。
LINE 153: 対象期間の追加。売上日の最小値と最大値から集計対象期間を表す行を作成し、リストに追加しています。LINE 154: 売上合計の追加。
集計結果に含まれる売上合計金額を表示するための行をリストに追加しています。LINE 155: 空行の追加。表示を見やすくするための空行をリストに追加しています。
LINE 156: 月別集計見出しの追加。月別集計の表を区切るための見出し文字列をリストに追加しています。LINE 157: 月別集計表の追加。
整形済みの月別集計DataFrameを文字列に変換し、リストに追加しています。LINE 158: 空行の追加。表示を見やすくするための空行をリストに追加しています。
LINE 159: 商品別集計見出しの追加。売上金額の降順であることを示す商品別集計の見出し文字列をリストに追加しています。LINE 160: 商品別集計表の追加。
整形済みの商品別集計DataFrameを文字列に変換し、リストに追加しています。LINE 161: リスト定義の終了。ここまでに追加した表示行をまとめたリストの定義を閉じています。
LINE 162: 文字列として結合して返却。リストに入った各行を改行文字でつなげて1つの文字列にし、呼び出し元へ返しています。RUN 7/9: 画面表示用の整形結果を確認する。
format_summaryを書き終えた状態です。3桁区切りやパーセント表示が効いた集計テキストが、そのまま画面に出せる形か確かめます。CHECK 7/9: 途中実行に成功。
format_summaryを書き終えた状態です。3桁区切りやパーセント表示が効いた集計テキストが、そのまま画面に出せる形か確かめます。RETURN 07: エディターへ戻りました。
入力済みのコードを維持したまま、次の実装へ進みます。LINE 167: Excel出力関数の定義。整形済みの明細と集計結果を受け取り、グラフ付きのExcelファイルを書き出す関数を定義しています。
LINE 168: 関数の役割説明。XlsxWriterを使って明細・集計表・グラフの4つのシートを持つExcelを作成する処理であることを示すドキュメント文字列です。LINE 169: 月別集計の取り出し。
集計結果dictから月別集計DataFrameを取り出し、以降で使いやすいように変数へ代入しています。LINE 170: 商品別集計の取り出し。集計結果dictから商品別集計DataFrameを取り出し、以降で使いやすいように変数へ代入しています。
LINE 171: 明細列の絞り込み。明細データからExcel出力に必要な列だけを選び出し、detail変数に代入しています。LINE 173: 出力フォルダの作成。
Excelファイルを保存する先のフォルダが存在しなければ作成する処理を呼び出しています。LINE 174: Excelブックの新規作成。xlsxwriterのWorkbookクラスを使い、出力先パスに対応する新しいExcelファイルのオブジェクトを作成します。
この後の処理では、このbookに対してシートや書式を追加していきます。LINE 176: ヘッダー用書式の定義開始。見出しセルに使う書式をadd_formatで作り始めています。
太字や背景色などの見た目をまとめて指定するための準備です。LINE 177: ヘッダー書式の詳細設定。文字色を白、背景色を紺色にし、枠線と中央揃えを指定して、見出し行が目立つように装飾しています。
LINE 178: ヘッダー書式定義の閉じ括弧。add_formatに渡す辞書の指定を閉じ、header_fmtという変数に見出し用の書式を確定させています。LINE 179: 日付用書式の定義。
日付を「年/月/日」の形式で表示し、枠線を付けるための書式をdate_fmtとして作成しています。明細シートの売上日列に使います。LINE 180: 文字列用書式の定義。
枠線だけを付けたシンプルな書式をtext_fmtとして作成しています。商品名や地域など文字列を表示するセルに使います。LINE 181: 整数用書式の定義。
3桁区切りのカンマ表示と枠線を付けた書式をint_fmtとして作成しています。数量や件数などの整数値に使います。LINE 182: 金額用書式の定義。
円マークとカンマ区切りを付けた書式をyen_fmtとして作成しています。売上金額などの金額を分かりやすく表示するために使います。LINE 183: パーセント用書式の定義。
小数点以下1桁のパーセント表示と枠線を付けた書式をpct_fmtとして作成しています。構成比の列に使います。LINE 184: 合計行の文字用書式の定義。
太字と薄い青の背景色を付けた書式をtotal_text_fmtとして作成しています。集計表の「合計」行の文字セルを目立たせるために使います。LINE 185: 合計行の金額用書式の定義。
太字・円マーク付き数値表示・薄い背景色を組み合わせた書式をtotal_yen_fmtとして作成しています。合計行の金額セルに使います。LINE 186: 合計行の整数用書式の定義。
太字・カンマ区切り数値表示・薄い背景色を組み合わせた書式をtotal_int_fmtとして作成しています。合計行の数量や件数のセルに使います。LINE 189: 明細シートの作成。
book.add_worksheetで「明細」という名前のワークシートを追加し、detail_wsという変数で操作できるようにしています。LINE 190: 明細ヘッダーの列位置を取得。DETAIL_COLUMNSに定義された列名を列番号付きで1つずつ取り出すループを開始し、見出し行を書き込む準備をしています。
LINE 191: 明細ヘッダーの書き込み。1行目(0行目)に各列の見出し文字列を、先ほど定義したheader_fmtの装飾付きで書き込んでいます。LINE 192: 明細データ行の取り出し。
明細データを1行ずつタプルとして取り出し、行番号は1から始まるように設定してループしています。行番号0は見出し行なので、データは1行目から書き込みます。LINE 193: 売上日の書き込み。
タプルの1番目の値(売上日)をPythonのdatetime型に変換し、date_fmtの日付書式付きでセルに書き込んでいます。LINE 194: 商品名の書き込み。タプルの2番目の値(商品名)を文字列に変換し、text_fmtの書式でセルに書き込んでいます。
LINE 195: カテゴリの書き込み。タプルの3番目の値(カテゴリ)を文字列に変換し、text_fmtの書式でセルに書き込んでいます。LINE 196: 地域の書き込み。
タプルの4番目の値(地域)を文字列に変換し、text_fmtの書式でセルに書き込んでいます。LINE 197: 単価の書き込み。タプルの5番目の値(単価)を数値に変換し、yen_fmtの円マーク付き書式でセルに書き込んでいます。
LINE 198: 数量の書き込み。タプルの6番目の値(数量)を整数に変換し、int_fmtのカンマ区切り書式でセルに書き込んでいます。LINE 199: 売上金額の書き込み。
タプルの7番目の値(売上金額)を数値に変換し、yen_fmtの円マーク付き書式でセルに書き込んでいます。LINE 200: 見出し行の固定。freeze_panesで1行目を固定し、明細データをスクロールしても見出し行が常に表示されるようにしています。
LINE 201: オートフィルタの設定。見出し行から最終行・最終列までの範囲にオートフィルタを設定し、Excel上で列ごとの絞り込みができるようにしています。LINE 202: A列の幅調整。
A列(売上日の列)の幅を12文字分に設定し、日付が見切れずに表示されるようにしています。LINE 203: B列の幅調整。B列(商品名の列)の幅を24文字分に設定し、商品名が長くても読みやすいようにしています。
LINE 204: C〜D列の幅調整。C列からD列(カテゴリ・地域の列)の幅をまとめて12文字分に設定し、見た目を整えています。LINE 205: E〜G列の幅調整。
E列からG列(単価・数量・売上金額の列)の幅をまとめて12文字分に設定し、数値が読みやすいようにしています。LINE 208: 月別集計の見出し名リスト作成。月別集計シートで使う列見出し「月」「売上金額」「数量」「件数」をリストとして定義し、後続の処理で使い回せるようにしています。
LINE 209: 月別集計シートの作成。book.add_worksheetで「月別集計」という名前のワークシートを追加し、month_wsという変数で操作できるようにしています。LINE 210: 月別ヘッダーの列位置を取得。
month_headersに定義した見出し名を列番号付きで1つずつ取り出すループを開始し、見出し行を書き込む準備をしています。LINE 211: 月別ヘッダーの書き込み。1行目に各見出し文字列を、header_fmtの装飾付きでセルに書き込んでいます。
LINE 212: 月別集計データ行の取り出し。月別集計データを見出しの列順に並べ替えたうえで1行ずつタプルとして取り出し、行番号は1から始まるようにしてループしています。LINE 213: 月の書き込み。
タプルの1番目の値(年月)を文字列に変換し、text_fmtの書式でセルに書き込んでいます。LINE 214: 月別売上金額の書き込み。タプルの2番目の値(売上金額)を数値に変換し、yen_fmtの円マーク付き書式でセルに書き込んでいます。
LINE 215: 月別数量の書き込み。タプルの3番目の値(数量)を整数に変換し、int_fmtのカンマ区切り書式でセルに書き込んでいます。LINE 216: 月別件数の書き込み。
タプルの4番目の値(件数)を整数に変換し、int_fmtのカンマ区切り書式でセルに書き込んでいます。LINE 217: 合計行の位置計算。月別集計データの件数に1を足すことで、見出し行の分を考慮した合計行の行番号を計算しています。
LINE 218: 合計行の見出し書き込み。合計行のA列に「合計」という文字列を、total_text_fmtの装飾付きで書き込み、この行が合計であることを分かりやすくしています。LINE 219: 売上金額合計の数式書き込み開始。
write_formulaを使い、月別売上金額の列に対してSUM関数の数式を書き込む処理を開始しています。LINE 220: 売上金額合計セルへの数式設定。B列の2行目から合計行の1つ上までを合計するSUM数式を書き込み、表示用の書式と、Pythonで計算した合計値のキャッシュも一緒に指定しています。
LINE 221: 売上金額合計数式の閉じ括弧。write_formula呼び出しの引数指定を閉じ、売上金額の合計セルへの数式設定を完了させています。LINE 222: 数量合計の数式書き込み開始。
write_formulaを使い、月別数量の列に対してSUM関数の数式を書き込む処理を開始しています。LINE 223: 数量合計セルへの数式設定。C列の2行目から合計行の1つ上までを合計するSUM数式を書き込み、書式と計算済み合計値のキャッシュも一緒に指定しています。
LINE 224: 数量合計数式の閉じ括弧。write_formula呼び出しの引数指定を閉じ、数量の合計セルへの数式設定を完了させています。LINE 225: 件数合計の数式書き込み開始。
write_formulaを使い、月別件数の列に対してSUM関数の数式を書き込む処理を開始しています。LINE 226: 件数合計セルへの数式設定。D列の2行目から合計行の1つ上までを合計するSUM数式を書き込み、書式と計算済み合計値のキャッシュも一緒に指定しています。
LINE 227: 件数合計数式の閉じ括弧。write_formula呼び出しの引数指定を閉じ、件数の合計セルへの数式設定を完了させています。LINE 228: 月別シートA列の幅調整。
A列(月の列)の幅を12文字分に設定し、年月の表示が見切れないようにしています。LINE 229: 月別シートB〜D列の幅調整。B列からD列(売上金額・数量・件数の列)の幅をまとめて14文字分に設定し、数値が読みやすいようにしています。
LINE 232: 商品別集計の見出し名リスト作成。商品別集計シートで使う列見出し「商品名」「カテゴリ」「売上金額」「数量」「構成比」をリストとして定義しています。LINE 233: 商品別集計シートの作成。
book.add_worksheetで「商品別集計」という名前のワークシートを追加し、product_wsという変数で操作できるようにしています。LINE 234: 商品別ヘッダーの列位置を取得。product_headersに定義した見出し名を列番号付きで1つずつ取り出すループを開始し、見出し行を書き込む準備をしています。
LINE 235: 商品別ヘッダーの書き込み。1行目に各見出し文字列を、header_fmtの装飾付きでセルに書き込んでいます。LINE 236: 商品別集計データ行の取り出し。
商品別集計データを見出しの列順に並べ替えたうえで1行ずつタプルとして取り出し、行番号は1から始まるようにしてループしています。LINE 237: 商品名の書き込み。タプルの1番目の値(商品名)を文字列に変換し、text_fmtの書式でセルに書き込んでいます。
LINE 238: カテゴリの書き込み。タプルの2番目の値(カテゴリ)を文字列に変換し、text_fmtの書式でセルに書き込んでいます。LINE 239: 商品別売上金額の書き込み。
タプルの3番目の値(売上金額)を数値に変換し、yen_fmtの円マーク付き書式でセルに書き込んでいます。LINE 240: 商品別数量の書き込み。タプルの4番目の値(数量)を整数に変換し、int_fmtのカンマ区切り書式でセルに書き込んでいます。
LINE 241: 構成比の書き込み。タプルの5番目の値(構成比)を数値に変換し、pct_fmtのパーセント表示書式でセルに書き込んでいます。LINE 242: 商品別合計行の位置計算。
商品別集計データの件数に1を足すことで、見出し行の分を考慮した合計行の行番号を計算しています。LINE 243: 商品別合計行の見出し書き込み。合計行のA列に「合計」という文字列を、total_text_fmtの装飾付きで書き込み、この行が合計であることを分かりやすくしています。
LINE 244: 商品別合計行のカテゴリ欄の空白書き込み。合計行のB列(カテゴリ)に空文字列をtotal_text_fmtの書式で書き込み、装飾を合わせつつ内容は空欄にしています。LINE 245: 売上金額の合計行を書き込む開始。
商品別集計シートの合計行に、売上金額列の合計を書き込む処理を開始します。write_formulaを使ってExcel上でSUM関数として計算される値を設定します。LINE 246: 売上金額の合計式を設定。
商品別集計シートのC列で、2行目から最後のデータ行までを合計するSUM式を書き込み、あわせてPythonで計算した売上金額の合計値をキャッシュとして渡しています。これによりExcelを開いた際にも正しい合計が表示されます。LINE 247: write_formula呼び出しの終了。
売上金額の合計を書き込むwrite_formula関数の呼び出しを閉じています。ここまでで合計行のC列に合計値の数式が設定されます。LINE 248: 数量の合計行を書き込む開始。
商品別集計シートの合計行に、数量列の合計を書き込む処理を開始します。売上金額と同様にwrite_formulaでSUM関数を設定します。LINE 249: 数量の合計式を設定。
商品別集計シートのD列で、2行目から最後のデータ行までを合計するSUM式を書き込み、Pythonで事前に計算した数量の合計値を一緒に渡しています。これによりExcel上でも正しい数量合計が表示されます。LINE 250: write_formula呼び出しの終了。
数量の合計を書き込むwrite_formula関数の呼び出しを閉じています。これで合計行のD列に数量合計の数式が設定されました。LINE 251: 商品データの有無を確認。
商品別集計に1件以上のデータがある場合のみ、次のデータバー装飾を行うための条件分岐です。商品データが空の場合は装飾処理をスキップします。LINE 252: データバー書式の設定開始。
商品別集計シートの売上金額列に対して、条件付き書式であるデータバーを設定する処理を開始します。数値の大小を視覚的に棒グラフのように表示するための準備です。LINE 253: データバーの範囲と色を指定。
売上金額列(C列)の2行目から商品数分の行までにデータバーを適用し、色を緑色(#63C384)に指定しています。これにより売上金額の大きさが一目でわかるようになります。LINE 254: conditional_format呼び出しの終了。
データバーを設定するconditional_format関数の呼び出しを閉じています。ここまでで商品別集計シートの売上金額列に視覚的な装飾が完成します。LINE 255: 商品名列の幅を設定。
商品別集計シートのA列(商品名)の幅を24文字分に設定し、商品名が読みやすく表示されるようにしています。LINE 256: カテゴリ列の幅を設定。商品別集計シートのB列(カテゴリ)の幅を12文字分に設定し、カテゴリ名が見切れないよう調整しています。
LINE 257: 数値列の幅をまとめて設定。商品別集計シートのC列からE列(売上金額・数量・構成比)の幅をまとめて14文字分に設定し、数値が読みやすく収まるようにしています。LINE 260: グラフシートの作成。
グラフを配置するための新しいワークシートを作成し、変数chart_wsに格納しています。このシートに月別と商品別の2つのグラフが挿入されます。LINE 261: グラフシートの余白列を設定。
グラフシートのA列の幅を3文字分に狭く設定し、グラフを挿入する際の左側の余白として使っています。LINE 263: 月別データの有無を確認。月別集計に1件以上のデータがある場合のみ、月別売上の縦棒グラフを作成する条件分岐です。
データが空の場合はグラフ作成をスキップします。LINE 264: 縦棒グラフオブジェクトの作成。月別売上を表示するための縦棒グラフ(column型)のグラフオブジェクトを新規作成し、変数month_chartに格納しています。
LINE 265: グラフへの系列追加開始。作成した縦棒グラフにデータ系列を追加する処理を開始します。ここで指定する情報をもとにグラフの見た目とデータ範囲が決まります。
LINE 266: 系列設定の辞書開始。グラフに追加する系列の詳細設定を辞書形式でまとめる部分の始まりです。系列名やデータ範囲、色などをこの中で指定していきます。
LINE 267: 系列名の指定。グラフの凡例に表示される系列名として「月別売上金額」という文字列を設定しています。凡例欄に表示される名称になります。
LINE 268: 横軸カテゴリの範囲指定。グラフの横軸(カテゴリ)に月別集計シートのA列(月)を、2行目から月数分まで指定しています。これによりグラフの横軸に各月が表示されます。
LINE 269: 縦軸データの範囲指定。グラフの縦軸に使う数値データとして、月別集計シートのB列(売上金額)を、2行目から月数分まで指定しています。これがグラフの棒の高さになります。
LINE 270: 棒の塗りつぶし色を指定。縦棒グラフの棒の色を紺色(#1F4E79)に指定し、見やすい配色で表示されるようにしています。LINE 271: データラベルの表示設定。
グラフの各棒に数値ラベルを表示するよう設定し、数値の表示形式を3桁区切りにしています。これにより棒グラフの上に売上金額の値が直接表示されます。LINE 272: 系列設定の辞書終了。
系列の詳細設定をまとめた辞書の記述を閉じています。ここまでの設定内容がグラフの系列としてまとめて適用されます。LINE 273: add_series呼び出しの終了。
データ系列を追加するadd_series関数の呼び出しを閉じています。これで月別売上の縦棒グラフにデータが組み込まれました。LINE 274: グラフタイトルの設定。
縦棒グラフのタイトルとして「月別売上推移」という文字列を設定し、グラフ上部に表示されるようにしています。LINE 275: 横軸タイトルの設定。縦棒グラフの横軸(X軸)に「月」というラベルを設定し、軸が何を表しているかがわかるようにしています。
LINE 276: 縦軸タイトルと表示形式の設定。縦棒グラフの縦軸(Y軸)に「売上金額(円)」というラベルを設定し、目盛りの数値表示を3桁区切りの形式にしています。LINE 277: 凡例の位置設定。
グラフの凡例をグラフの下部に配置するよう設定しています。凡例の表示位置を見やすい場所にまとめています。LINE 278: グラフサイズの設定。
縦棒グラフの表示サイズを幅720ピクセル、高さ400ピクセルに指定し、シート上で見やすい大きさになるよう調整しています。LINE 279: グラフのシートへの挿入。作成した縦棒グラフをグラフシートのB2セルの位置に挿入しています。
これで月別売上のグラフが実際にシート上に配置されます。LINE 281: 商品データの有無を確認(商品別グラフ用)。商品別集計に1件以上のデータがある場合のみ、商品別売上の横棒グラフを作成する条件分岐です。
データが空の場合はグラフ作成をスキップします。LINE 282: 横棒グラフオブジェクトの作成。商品別売上を表示するための横棒グラフ(bar型)のグラフオブジェクトを新規作成し、変数product_chartに格納しています。
LINE 283: グラフへの系列追加開始。作成した横棒グラフにデータ系列を追加する処理を開始します。ここで指定する情報をもとにグラフの見た目とデータ範囲が決まります。
LINE 284: 系列設定の辞書開始。横棒グラフに追加する系列の詳細設定を辞書形式でまとめる部分の始まりです。系列名やデータ範囲、色などをこの中で指定していきます。
LINE 285: 系列名の指定。グラフの凡例に表示される系列名として「商品別売上金額」という文字列を設定しています。LINE 286: 縦軸カテゴリの範囲指定。
横棒グラフの軸(カテゴリ)に商品別集計シートのA列(商品名)を、2行目から商品数分まで指定しています。これによりグラフに各商品名が表示されます。LINE 287: 横軸データの範囲指定。
横棒グラフの数値データとして、商品別集計シートのC列(売上金額)を、2行目から商品数分まで指定しています。これが棒の長さとして使われます。LINE 288: 棒の塗りつぶし色を指定。
横棒グラフの棒の色を赤茶色(#C0504D)に指定し、月別グラフと異なる配色で見分けやすくしています。LINE 289: 系列設定の辞書終了。商品別グラフの系列設定をまとめた辞書の記述を閉じています。
ここまでの設定内容がグラフの系列としてまとめて適用されます。LINE 290: add_series呼び出しの終了。データ系列を追加するadd_series関数の呼び出しを閉じています。
これで商品別売上の横棒グラフにデータが組み込まれました。LINE 291: グラフタイトルの設定。横棒グラフのタイトルとして「商品別売上金額」という文字列を設定し、グラフ上部に表示されるようにしています。
LINE 292: 横軸タイトルと表示形式の設定。横棒グラフの横軸に「売上金額(円)」というラベルを設定し、数値表示を3桁区切りの形式にしています。LINE 293: 縦軸タイトルと並び順の設定。
横棒グラフの縦軸に「商品名」というラベルを設定し、reverseオプションで表示順を逆にしています。これにより売上上位の商品が上側に表示されます。LINE 294: 凡例を非表示に設定。
横棒グラフの凡例を表示しないよう設定しています。系列が1つのみのためグラフをすっきり見せる目的です。LINE 295: グラフサイズの設定。
横棒グラフの表示サイズを幅720ピクセル、高さ400ピクセルに指定し、月別グラフと同じ大きさに揃えています。LINE 296: グラフのシートへの挿入。作成した横棒グラフをグラフシートのB24セルの位置に挿入しています。
これで商品別売上のグラフが月別グラフの下に並んで配置されます。LINE 298: Excelファイルの保存確定。Workbookオブジェクトのcloseメソッドを呼び出し、これまで設定してきたシートやグラフの内容をファイルとして書き出し、保存を確定させています。
LINE 300: 処理結果の辞書を返す開始。write_excel_report関数の戻り値として、処理結果をまとめた辞書を返す記述を開始します。呼び出し元でこの結果をもとに表示メッセージが作られます。
LINE 301: 出力パスを絶対パスで格納。出力したExcelファイルの絶対パスを取得し、結果辞書のpathキーに格納しています。相対パスで指定されていてもフルパスとして扱えるようにしています。
LINE 302: ファイルサイズの取得。os.path.getsizeで出力したExcelファイルのサイズをバイト単位で取得し、結果辞書のsizeキーに格納しています。LINE 303: シート名一覧を格納。
作成した4つのシート名(明細・月別集計・商品別集計・グラフ)をリストとして結果辞書のsheetsキーに格納しています。LINE 304: 明細行数を格納。明細データの行数を整数型に変換し、結果辞書のdetail_rowsキーに格納しています。
LINE 305: 月数を格納。月別集計の行数(集計対象となった月の数)を整数型に変換し、結果辞書のmonthsキーに格納しています。LINE 306: 商品数を格納。
商品別集計の行数(集計対象となった商品の数)を整数型に変換し、結果辞書のproductsキーに格納しています。LINE 307: 売上合計を格納。集計結果の売上合計金額を浮動小数点数に変換し、結果辞書のtotalキーに格納しています。
LINE 308: 戻り値の辞書を閉じる。write_excel_report関数が返す結果辞書の記述を閉じています。これで関数呼び出し元にレポート生成結果一式が渡されます。
RUN 8/9: 4シートのExcel生成を確認する。write_excel_reportが完成した状態です。CSVから集計までを通してExcelを書き出し、シート構成や明細行数、ファイルサイズを確認します。
CHECK 8/9: 途中実行に成功。write_excel_reportが完成した状態です。CSVから集計までを通してExcelを書き出し、シート構成や明細行数、ファイルサイズを確認します。
RETURN 08: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。LINE 313: パーサー構築関数の定義。
argparseを用いてコマンドライン引数を解析するArgumentParserを組み立てるbuild_parser関数を定義しています。戻り値の型はArgumentParserであることを明示しています。LINE 314: 関数の説明文。
build_parser関数の役割を説明するドキュメント文字列です。sample・summary・reportの3つのサブコマンドを組み立てることを示しています。LINE 315: ArgumentParserの作成開始。
コマンドライン引数を扱うためのArgumentParserオブジェクトを作成する処理を開始しています。ここで指定するprogやdescriptionが実行時のヘルプ表示に使われます。LINE 316: プログラム名の指定。
ヘルプ表示などに使われるプログラム名として「sales_report_cli.py」を設定しています。ユーザーがコマンドの使い方を確認する際に表示されます。LINE 317: コマンドの説明文の指定。
このCLIツール全体の説明文として「売上CSVを月別・商品別に集計し、グラフ付きExcelレポートを生成する」という文言を設定しています。ヘルプ表示の冒頭に表示されます。LINE 318: ArgumentParser作成の終了。
ArgumentParserオブジェクトを作成する記述を閉じています。ここまでの設定でCLI全体の基本情報を持つparserが完成します。LINE 319: サブコマンド機能の追加。
parserにサブコマンドを追加する機能を組み込んでいます。dest="command"により選択されたコマンド名がargs.commandに格納され、requiredを付けることで必ずいずれかのコマンドを指定させるようにしています。LINE 321: sampleサブコマンドの定義。
sampleという名前のサブコマンドを作成しています。ここで作られたsample_parserに対して、この後sample専用のオプションを追加していきます。LINE 322: 出力先CSVパスの指定。
sampleコマンド用に--outオプションを追加しています。指定しなかった場合はデフォルトでsales.csvという名前で出力されるようにしています。LINE 323: 生成件数オプションの指定。
sampleコマンド用に--rowsオプションを追加しています。type=intで整数に変換し、指定がなければ200件の明細を生成するようにしています。LINE 324: 乱数シードオプションの指定。
sampleコマンド用に--seedオプションを追加しています。同じシード値を指定すれば毎回同じ内容のサンプルデータが再現できるようにしています。LINE 326: summaryサブコマンドの定義。
summaryという名前のサブコマンドを作成しています。集計結果を画面に表示するためのコマンドとして、この後専用オプションを追加していきます。LINE 327: 入力CSVパスの必須指定。
summaryコマンド用に--inputオプションを追加しています。required=Trueとすることで、入力ファイルのパス指定を必ず求めるようにしています。LINE 329: reportサブコマンドの定義。
reportという名前のサブコマンドを作成しています。グラフ付きExcelレポートを生成するためのコマンドとして、この後専用オプションを追加していきます。LINE 330: 入力CSVパスの必須指定。
reportコマンド用に--inputオプションを追加しています。集計元となる売上CSVのパスを必ず指定させるようにしています。LINE 331: 出力Excelパスの指定。
reportコマンド用に--outオプションを追加しています。指定がなければsales_report.xlsxという名前でExcelファイルを出力するようにしています。LINE 333: 作成したparserの返却。
ここまで組み立てたparserを呼び出し元に返しています。この戻り値を使ってコマンドライン引数を解析することになります。RUN 9/9: サブコマンドの引数定義を確認する。
build_parserを書き終えた状態です。3つのサブコマンドへ引数を渡して解析し、既定値も含めてどんな値が受け取れるのかを見ます。CHECK 9/9: 途中実行に成功。
build_parserを書き終えた状態です。3つのサブコマンドへ引数を渡して解析し、既定値も含めてどんな値が受け取れるのかを見ます。RETURN 09: エディターへ戻りました。
入力済みのコードを維持したまま、次の実装へ進みます。LINE 338: main関数の定義開始。プログラム全体のエントリーポイントとなるmain関数を定義しています。
この関数の中でコマンドの判定と各処理の呼び出しをまとめて行います。LINE 339: main関数の説明文。main関数がサブコマンドに応じて処理を分岐し、結果を標準出力に表示することを説明したドキュメント文字列です。
LINE 340: コマンドライン引数の解析。build_parser関数でparserを作り、parse_argsでコマンドライン引数を解析しています。解析結果はargsという変数に格納され、この後の処理で参照します。
LINE 342: sampleコマンドの判定。args.commandがsampleかどうかを確認しています。sampleが指定されていた場合のみ、この下のサンプルCSV作成処理を実行します。
LINE 343: サンプルCSVの作成実行。write_sample_csv関数を呼び出し、指定された出力先・件数・シードでサンプルCSVを作成しています。作成結果の情報はinfoという辞書に格納されます。
LINE 344: 作成完了メッセージの表示。サンプルCSVの作成が完了したことを知らせるメッセージを画面に表示しています。LINE 345: 出力ファイルパスの表示。
infoに格納された出力ファイルの絶対パスを画面に表示しています。実際にどこにファイルが作成されたかを確認できるようにしています。LINE 346: 明細件数の表示。
infoに格納された明細件数を3桁区切りの数値として画面に表示しています。生成された行数を分かりやすく確認できます。LINE 347: 対象期間の表示。
infoに格納された最初の日付と最後の日付を画面に表示し、サンプルデータがカバーする期間を示しています。LINE 348: 商品数の表示。infoに格納された商品の種類数を画面に表示しています。
何種類の商品がサンプルデータに含まれているかを確認できます。LINE 349: CSV列構成の表示。CSV_COLUMNSをカンマ区切りの文字列に連結し、出力されたCSVの列構成を画面に表示しています。
LINE 350: sample処理の終了。sampleコマンドの処理が完了したため、returnでmain関数をここで終了させ、以降のsummaryやreport用の処理を実行させないようにしています。LINE 352: 売上CSVの読み込み。
load_sales関数を呼び出し、指定された入力CSVを読み込んで前処理済みのDataFrameを取得しています。この処理はsummaryとreportの両方で共通して使われます。LINE 353: 集計処理の実行。
aggregate_sales関数を呼び出し、読み込んだ売上データから月別集計と商品別集計をまとめて作成しています。結果はaggという辞書に格納されます。LINE 355: summaryコマンドの判定。
args.commandがsummaryかどうかを確認しています。summaryが指定されていた場合のみ、この下の集計結果表示処理を実行します。LINE 356: 集計結果テキストの表示。
format_summary関数で整形した集計結果の文字列を画面に表示しています。ファイル情報、月別集計、商品別集計がまとめて出力されます。LINE 357: summary処理の終了。
summaryコマンドの処理が完了したため、returnでmain関数をここで終了させ、この下にあるreport用の処理へ進まないようにしています。LINE 359: Excelレポートの生成実行。write_excel_report関数を呼び出し、読み込んだデータと集計結果からグラフ付きExcelファイルを生成しています。
生成結果の情報はinfoという辞書に格納されます。LINE 360: 生成完了メッセージの表示。Excelレポートの生成が完了したことを知らせるメッセージを画面に表示しています。
LINE 361: 入力CSVパスの表示。os.path.abspathで入力CSVの絶対パスを求め、どのファイルを元にレポートを作成したかを画面に表示しています。LINE 362: 出力ファイルパスの表示。
infoに格納された出力Excelファイルの絶対パスを画面に表示しています。実際にどこにファイルが作成されたかを確認できるようにしています。LINE 363: ファイルサイズの表示。
infoに格納されたファイルサイズを3桁区切りのバイト数として画面に表示しています。生成されたExcelファイルの大きさを確認できます。LINE 364: シート構成の表示。
infoに格納されたシート名一覧をスラッシュ区切りの文字列に連結し、Excel内に含まれるシートの構成を画面に表示しています。LINE 365: 明細行数の表示。infoに格納された明細シートの行数を3桁区切りの数値として画面に表示しています。
LINE 366: 集計表の行数表示。infoに格納された月別集計と商品別集計それぞれの行数を画面に表示しています。集計表の規模を確認できます。
LINE 367: 売上合計の表示。infoに格納された売上合計金額を3桁区切りの円表示で画面に表示しています。LINE 368: グラフ配置の案内表示。
グラフシートに月別の縦棒グラフと商品別の横棒グラフが配置されていることを案内するメッセージを画面に表示しています。LINE 371: スクリプト実行時の判定。このファイルが直接実行されたときにのみ、下の処理を実行するようにする定型的な条件文です。
他のファイルからインポートされた場合には実行されません。LINE 372: main関数の呼び出し。main関数を呼び出し、プログラム全体の処理を開始しています。
ここがスクリプト実行時の実質的な出発点になります。実行1/4: サンプル売上CSVを作成する。sampleコマンドを実行して、集計の入力になる売上CSVを作ります。
件数と乱数シードを指定するため、何度実行しても同じ内容のファイルができます。サンプル売上CSVを作成する。sampleコマンドを実行して、集計の入力になる売上CSVを作ります。
件数と乱数シードを指定するため、何度実行しても同じ内容のファイルができます。集計結果をターミナルで確認する。作成したCSVをsummaryコマンドへ渡し、対象期間と売上合計、月別集計、商品別集計を画面で確認します。
Excelを開かずに中身を点検できます。グラフ付きExcelレポートを出力する。reportコマンドで4シート構成のExcelを書き出します。
標準出力にはシート構成や明細行数が並び、指定したパスへファイルが作られたことも確認できます。件数と出力先を変えて作り直す。件数と乱数シードを変えたCSVから、まだ存在しないフォルダへレポートを出力します。
出力先の親フォルダが自動で用意される様子も確認できます。学習内容のまとめ。
xlsxwriter.Workbook(out_path)でブックを開くread_csvでBOM付きUTF-8のCSVを読むsampleはwrite_sample_csvでCSVを作る 単一のPythonファイルで完結する構成 小さく実行確認しながら完成状態まで段階的に組み立てる エンディング。Python研修はCodeCampでご確認ください。
目次
- XlsxWriterとpandasとは
- Python・XlsxWriterで開発する場合の環境構築
- グラフ付きExcel売上レポートCLIの要件定義
- グラフ付きExcel売上レポートCLIを作る際の重要ポイント
- Pythonでグラフ付きExcel売上レポートCLIの完成コード
- pandasのgroupbyで月別・商品別に集計する
- Pythonでグラフ付きExcel売上レポートCLIのエラー対処
- PythonのXlsxWriterで明細・集計表・売上グラフを書き出す
- グラフ付きExcel売上レポートCLIの動作確認
- グラフ付きExcel売上レポートCLIの活用例
- グラフ付きExcel売上レポートCLI開発のまとめ
- 参考にした一次情報
XlsxWriterとpandasとは
今回使用する主要なライブラリについて、役割と使い分けを順番に確認します。
Excelを生成するライブラリ「XlsxWriter」
XlsxWriterは、Pythonのコードから拡張子がxlsxのExcelブックを新しく書き出すためのライブラリです。既存ファイルの読み取りや編集には対応していない代わりに、セル書式、枠固定、オートフィルタ、条件付き書式、グラフといった見た目に関わる設定をコードで細かく指定できます。
今回のツールでは集計そのものをpandasへ任せ、XlsxWriterは4枚のシートを組み立てて保存する担当になっています。
今回のExcelレポートでXlsxWriterに任せている具体的な設定は次のとおりです
- xlsxwriter.Workbook(out_path)でブックを開く
- add_formatで金額用の¥#,##0書式を作る
- write_datetimeで売上日を日付セルとして書く
- freeze_panes(1, 0)で見出し行を固定する
- autofilterで絞り込み用の見出しを付ける
- conditional_formatのdata_barで売上の大小を棒で示す
- add_chartで縦棒グラフと横棒グラフを作る
- book.close()で保存を確定する
表形式のデータを集計するライブラリ「pandas」
pandasは、CSVのような表形式データをDataFrameという表オブジェクトとして扱えるデータ分析ライブラリです。1行ずつループを書かなくても、列単位の計算や条件による絞り込み、グループごとの合計をまとめて指定できます。今回はread_csvで売上明細を読み、型変換で扱えない行を落としてから、groupbyで月別と商品別の集計表を組み立てました。
このCLIでpandasが担当している読み込みから集計までの処理は次のとおりです
- read_csvでBOM付きUTF-8のCSVを読む
- to_datetimeで売上日を日付型へ変換する
- to_numericで単価と数量を数値へそろえる
- dropnaで変換できなかった行を落とす
- 列同士の掛け算で売上金額を作る
- groupbyで月ごとの合計と件数を出す
- sort_valuesで売上金額の降順に並べ替える
- to_stringでターミナル向けの表に整える
Python・XlsxWriterで開発する場合の環境構築
この記事のセットアップ手順と掲載コードは、Windows 11 Pro、PowerShell 5.1、Python 3.13.3で動作確認しています。仮想環境を有効化せず、その中のPythonを直接指定するため、以下のコマンドはPowerShellとコマンドプロンプト(cmd)の両方で使えます。
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install --upgrade pip
.\.venv\Scripts\python.exe -m pip install pandas XlsxWriter
macOS・Linuxでは仮想環境内のPythonパスが異なります。今回の動作確認環境とは異なるため、以下は環境差分を補う参考手順です。
python3 -m venv .venv
./.venv/bin/python -m pip install --upgrade pip
./.venv/bin/python -m pip install pandas XlsxWriter
- パッケージ名はXlsxWriterですが、コード内のimport名は小文字のxlsxwriterです。
- 生成したxlsxをExcelで開いたまま同じパスへ再出力するとPermissionErrorになるため、上書き前に閉じておきます。
- サンプルCSVはutf-8-sigで保存しているので、Excelでそのまま開いても日本語列名が文字化けしません。
グラフ付きExcel売上レポートCLIの要件定義
目的は、売上CSVを読み込み、月別と商品別の集計表とグラフを含むExcelレポートをコマンド一つで出力できるようにすることです。
対象者として、Pythonの基本文法を学び終え、pandasでの集計とExcelファイルの自動生成をコマンドラインツールとしてまとめる方法を学びたい人を想定しています。
完成物は、argparseのサブコマンドでサンプル作成・集計表示・レポート出力を切り替え、XlsxWriterで明細・月別集計・商品別集計・グラフの4シートを書き出すCLIツールです。
実装へ入る前に、機能・品質・受け入れ条件を分けて確認します。
機能要件
- sampleコマンドでサンプル売上CSVを生成する
- --rowsと--seedで件数と乱数シードを指定する
- summaryコマンドで月別集計と商品別集計を画面へ表示する
- reportコマンドでグラフ付きExcelを出力する
- 単価と数量の積を売上金額の列として計算する
- 売上日から年月の列を作る
- 商品別集計へ売上金額の構成比を付ける
- 明細シートへ枠固定とオートフィルタを設定する
- 月別集計と商品別集計へSUM関数の合計行を追加する
- 商品別売上金額の列へデータバーを表示する
- グラフシートへ月別の縦棒グラフと商品別の横棒グラフを配置する
- 出力ファイルのパスとサイズを標準出力へ表示する
非機能要件
- 単一のPythonファイルで完結する構成
- 外部依存をpandasとxlsxwriterの2つに限定する
- 標準ライブラリのargparse・os・random・datetimeを利用する
- サンプルCSVをUTF-8(BOM付き)で保存する
- 乱数シードを固定して同じサンプルデータを再現する
- 出力先の親フォルダを自動で作成する
- 入力CSVが無い場合にFileNotFoundErrorを送出する
- 必要な列が欠けている場合にValueErrorを送出する
- 日付や数値へ変換できない行を除外する
- 集計対象が0件のときにValueErrorで停止する
- --rowsが1未満のときにValueErrorで停止する
- 型ヒントと関数単位の分割で処理を整理する
実装方針
今回はXlsxWriterとpandasの基本動作を追いやすくするため、グラフ付きExcel売上レポートCLI本体を1つのPythonファイルへまとめます。
入力、判定、結果表示の役割を分け、実行結果を確認しながら機能を積み上げます。
グラフ付きExcel売上レポートCLIを安全に組み立てるための実装方針は次のとおりです。
- 単一のPythonファイルで完結する構成
- 外部依存をpandasとxlsxwriterの2つに限定する
- 標準ライブラリのargparse・os・random・datetimeを利用する
- サンプルCSVをUTF-8(BOM付き)で保存する
- 乱数シードを固定して同じサンプルデータを再現する
- 出力先の親フォルダを自動で作成する
- 入力CSVが無い場合にFileNotFoundErrorを送出する
- 必要な列が欠けている場合にValueErrorを送出する
- 日付や数値へ変換できない行を除外する
- 集計対象が0件のときにValueErrorで停止する
- --rowsが1未満のときにValueErrorで停止する
- 型ヒントと関数単位の分割で処理を整理する
完成と判断する条件
- sampleコマンドが指定件数のCSVを作成する
- 作成したCSVが6列の構成になる
- summaryコマンドが対象期間と売上合計を表示する
- 月別集計が年月の昇順で並ぶ
- 商品別集計が売上金額の降順で並ぶ
- 構成比がパーセント表示になる
- reportコマンドが4シートのExcelを出力する
- 出力Excelの明細行数が入力明細の件数と一致する
- 標準出力へシート構成とファイルサイズが並ぶ
- 3つのサブコマンドが終了コード0で完了する
グラフ付きExcel売上レポートCLIを作る際の重要ポイント
このCLIは、CSVを読み込むload_sales、集計するaggregate_sales、結果を出力するformat_summaryとwrite_excel_reportを軸に組み立てています。argparseのサブコマンドは、受け取った引数に応じてこれらの関数を順に呼ぶだけの入口です。
処理を分けておくと、集計内容を画面で確かめてからExcelを出す、といった使い方が自然にできました。
3つのサブコマンドが呼び出す関数と出力先の対応は次のとおりです
- sampleはwrite_sample_csvでCSVを作る
- summaryはformat_summaryの文字列を表示する
- reportはwrite_excel_reportでxlsxを書く
- load_salesが型変換と列追加をまとめて担う
- aggregate_salesがmonthlyとproductとtotalを辞書で返す
- ensure_parent_dirが出力先フォルダを用意する
argparseのサブコマンドで入口を分ける
add_subparsersを使うと、1つのスクリプトへgitのような複数の入口を持たせられます。dest="command"で選ばれたコマンド名を受け取り、required=Trueにしておけばコマンド未指定の実行を最初の段階で止められます。各サブコマンドは必要な引数だけを持つので、helpも短くまとまりました。
build_parserが定義している3つのサブコマンドと引数です
- sampleは--outと--rowsと--seedを受ける
- --rowsの既定値は200件
- summaryは--inputを必須にする
- reportは--inputと--outを受ける
- --outの既定値はsales_report.xlsx
関数を分けて集計と出力を切り離す
読み込み、集計、表示、書き出しを別々の関数にすると、同じ集計結果を画面表示にもExcel出力にも使い回せます。今回はaggregate_salesが辞書を返す形にしたため、summaryとreportで同じ値をそのまま渡せました。エラーは例外として投げ、判定を各関数の入口へ集めています。
mainがサブコマンドごとに呼び分けている関数の流れです
- sampleはwrite_sample_csvだけで完結する
- summaryとreportは同じload_salesを通る
- aggregate_salesの戻り値を両方で共有する
- format_summaryは文字列を返し表示はmainが行う
- write_excel_reportは書き出し結果を辞書で返す
4シート構成のExcelを組み立てる順序
シートは明細、月別集計、商品別集計、グラフの順に追加します。この実装では集計シートへ値を書き込んでから、そのセル範囲を参照するグラフを組み立てています。合計行のSUM式にはあらかじめ計算した値も添えて渡し、Excelで開き直す前でも数値を確認できる状態にしました。
write_excel_reportがシートを追加していく順番と役割です
- 明細シートへ全行と枠固定を書く
- 月別集計シートへSUM関数の合計行を足す
- 商品別集計シートへ構成比とデータバーを付ける
- グラフシートから集計シートの範囲を参照する
- write_formulaへ計算済みの値も一緒に渡す
Pythonでグラフ付きExcel売上レポートCLIの完成コード
コードは上から順に、定数とマスタ、出力先の準備、サンプル生成、読み込みと前処理、集計、Excel出力という並びにしています。関数名を見ればどの段の処理か分かるようにしたので、上から読み下すだけで流れがつかめます。
列名やシート名はすべてモジュール先頭の定数にまとめました。文字列を各所に散らすと、列名を1つ変えたときの修正漏れが起きやすくなります。
入力の不備は早めに例外で落とす方針です。CSVが無い場合や必須列が足りない場合は、その場でメッセージ付きの例外を投げます。
ここからは、押さえておけば全体が読めるようになる箇所を順に見ていきましょう。
このセクションの用語
- argparse
- Python標準のコマンドライン引数の解析ライブラリです。
--inputのようなオプションやサブコマンドを定義できます。 - DataFrame
- pandasが扱う表形式のデータ構造です。列に名前が付いた表として、行の絞り込みや集計を行えます。
- utf-8-sig
- 先頭にBOMを付けたUTF-8のことです。ExcelでCSVを直接開いたときに日本語が化けにくくなります。
- 乱数シード
- 乱数の出発点となる数値です。同じ値を指定すれば、毎回同じ疑似データを再現できます。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""売上CSVを月別・商品別に集計し、グラフ付きExcelレポートを生成するCLIツール。"""
from __future__ import annotations
import argparse
import os
import random
from datetime import date, timedelta
import pandas as pd
import xlsxwriter
# サンプルデータ用のマスタと列定義
PRODUCT_MASTER = [
("ブレンドコーヒー豆", "食品", 1200),
("有機緑茶ティーバッグ", "食品", 800),
("ステンレスボトル", "雑貨", 2800),
("陶器マグカップ", "雑貨", 1500),
("ドリップスタンド", "調理器具", 3600),
("電動コーヒーミル", "調理器具", 5400),
]
REGIONS = ["東京", "大阪", "名古屋", "福岡"]
CSV_COLUMNS = ["売上日", "商品名", "カテゴリ", "地域", "単価", "数量"]
DETAIL_COLUMNS = CSV_COLUMNS + ["売上金額"]
SAMPLE_START = date(2024, 1, 1)
SAMPLE_DAYS = 182
SHEET_DETAIL = "明細"
SHEET_MONTH = "月別集計"
SHEET_PRODUCT = "商品別集計"
SHEET_CHART = "グラフ"
# 出力先フォルダの準備
def ensure_parent_dir(path: str) -> None:
"""出力パスの親フォルダが無ければ作成する。"""
parent = os.path.dirname(os.path.abspath(path))
if parent:
os.makedirs(parent, exist_ok=True)
# サンプル売上CSVの生成
def build_sample_frame(rows: int, seed: int) -> pd.DataFrame:
"""シードを固定した擬似売上明細をDataFrameとして作る。"""
if rows < 1:
raise ValueError("--rows には1以上の件数を指定してください")
rng = random.Random(seed)
records = []
for _ in range(rows):
name, category, unit_price = rng.choice(PRODUCT_MASTER)
sold_at = SAMPLE_START + timedelta(days=rng.randrange(SAMPLE_DAYS))
records.append(
{
"売上日": sold_at.isoformat(),
"商品名": name,
"カテゴリ": category,
"地域": rng.choice(REGIONS),
"単価": unit_price,
"数量": rng.randint(1, 8),
}
)
frame = pd.DataFrame(records, columns=CSV_COLUMNS)
return frame.sort_values("売上日").reset_index(drop=True)
def write_sample_csv(path: str, rows: int, seed: int) -> dict:
"""サンプル売上CSVをUTF-8(BOM付き)で保存する。"""
frame = build_sample_frame(rows, seed)
ensure_parent_dir(path)
frame.to_csv(path, index=False, encoding="utf-8-sig")
return {
"path": os.path.abspath(path),
"rows": int(len(frame)),
"first_date": str(frame["売上日"].iloc[0]),
"last_date": str(frame["売上日"].iloc[-1]),
"products": int(frame["商品名"].nunique()),
}
# 売上CSVの読み込みと前処理
def load_sales(path: str) -> pd.DataFrame:
"""CSVを読み込み、型変換と売上金額・月の列追加までを行う。"""
if not os.path.exists(path):
raise FileNotFoundError(f"売上CSVが見つかりません: {os.path.abspath(path)}")
frame = pd.read_csv(path, encoding="utf-8-sig")
missing = [column for column in CSV_COLUMNS if column not in frame.columns]
if missing:
raise ValueError(f"必要な列がありません: {', '.join(missing)}")
frame["売上日"] = pd.to_datetime(frame["売上日"], errors="coerce")
frame["単価"] = pd.to_numeric(frame["単価"], errors="coerce")
frame["数量"] = pd.to_numeric(frame["数量"], errors="coerce")
frame = frame.dropna(subset=["売上日", "単価", "数量"]).copy()
if frame.empty:
raise ValueError("集計できる売上明細がありません")
frame["売上金額"] = frame["単価"] * frame["数量"]
frame["月"] = frame["売上日"].dt.strftime("%Y-%m")
return frame.sort_values("売上日").reset_index(drop=True)
# 月別・商品別の集計
def aggregate_sales(frame: pd.DataFrame) -> dict:
"""groupbyで月別集計と商品別集計を作り、構成比を付ける。"""
monthly = (
frame.groupby("月", as_index=False)
.agg({"売上金額": "sum", "数量": "sum", "売上日": "count"})
.rename(columns={"売上日": "件数"})
.sort_values("月")
.reset_index(drop=True)
)
product = (
frame.groupby(["商品名", "カテゴリ"], as_index=False)
.agg({"売上金額": "sum", "数量": "sum"})
.sort_values("売上金額", ascending=False)
.reset_index(drop=True)
)
total = float(frame["売上金額"].sum())
product["構成比"] = product["売上金額"] / total if total else 0.0
return {"monthly": monthly, "product": product, "total": total}
# 集計結果のテキスト整形
def format_summary(path: str, frame: pd.DataFrame, agg: dict) -> str:
"""ターミナル表示用に集計表を3桁区切り・パーセント表示へ整える。"""
monthly = agg["monthly"].copy()
monthly["売上金額"] = monthly["売上金額"].map("{:,.0f}".format)
monthly["数量"] = monthly["数量"].map("{:,.0f}".format)
monthly["件数"] = monthly["件数"].map("{:,.0f}".format)
product = agg["product"].copy()
product["売上金額"] = product["売上金額"].map("{:,.0f}".format)
product["数量"] = product["数量"].map("{:,.0f}".format)
product["構成比"] = product["構成比"].map("{:.1%}".format)
lines = [
"=== 集計対象 ===",
f"ファイル : {os.path.abspath(path)}",
f"明細件数 : {len(frame):,}件",
f"対象期間 : {frame['売上日'].min():%Y-%m-%d} 〜 {frame['売上日'].max():%Y-%m-%d}",
f"売上合計 : {agg['total']:,.0f}円",
"",
"=== 月別集計 ===",
monthly.to_string(index=False),
"",
"=== 商品別集計(売上金額の降順) ===",
product.to_string(index=False),
]
return "\n".join(lines)
# Excelレポートの生成
def write_excel_report(frame: pd.DataFrame, agg: dict, out_path: str) -> dict:
"""XlsxWriterで明細・集計表・グラフの4シートを持つExcelを書き出す。"""
monthly = agg["monthly"]
product = agg["product"]
detail = frame[DETAIL_COLUMNS]
ensure_parent_dir(out_path)
book = xlsxwriter.Workbook(out_path)
header_fmt = book.add_format(
{"bold": True, "font_color": "#FFFFFF", "bg_color": "#1F4E79", "border": 1, "align": "center"}
)
date_fmt = book.add_format({"num_format": "yyyy/mm/dd", "border": 1})
text_fmt = book.add_format({"border": 1})
int_fmt = book.add_format({"num_format": "#,##0", "border": 1})
yen_fmt = book.add_format({"num_format": "¥#,##0", "border": 1})
pct_fmt = book.add_format({"num_format": "0.0%", "border": 1})
total_text_fmt = book.add_format({"bold": True, "border": 1, "bg_color": "#DCE6F1"})
total_yen_fmt = book.add_format({"bold": True, "num_format": "¥#,##0", "border": 1, "bg_color": "#DCE6F1"})
total_int_fmt = book.add_format({"bold": True, "num_format": "#,##0", "border": 1, "bg_color": "#DCE6F1"})
# 明細シート(ヘッダー書式・枠固定・オートフィルタ)
detail_ws = book.add_worksheet(SHEET_DETAIL)
for col, name in enumerate(DETAIL_COLUMNS):
detail_ws.write(0, col, name, header_fmt)
for row, values in enumerate(detail.itertuples(index=False, name=None), start=1):
detail_ws.write_datetime(row, 0, values[0].to_pydatetime(), date_fmt)
detail_ws.write_string(row, 1, str(values[1]), text_fmt)
detail_ws.write_string(row, 2, str(values[2]), text_fmt)
detail_ws.write_string(row, 3, str(values[3]), text_fmt)
detail_ws.write_number(row, 4, float(values[4]), yen_fmt)
detail_ws.write_number(row, 5, int(values[5]), int_fmt)
detail_ws.write_number(row, 6, float(values[6]), yen_fmt)
detail_ws.freeze_panes(1, 0)
detail_ws.autofilter(0, 0, len(detail), len(DETAIL_COLUMNS) - 1)
detail_ws.set_column("A:A", 12)
detail_ws.set_column("B:B", 24)
detail_ws.set_column("C:D", 12)
detail_ws.set_column("E:G", 12)
# 月別集計シート(SUM関数の合計行付き)
month_headers = ["月", "売上金額", "数量", "件数"]
month_ws = book.add_worksheet(SHEET_MONTH)
for col, name in enumerate(month_headers):
month_ws.write(0, col, name, header_fmt)
for row, values in enumerate(monthly[month_headers].itertuples(index=False, name=None), start=1):
month_ws.write_string(row, 0, str(values[0]), text_fmt)
month_ws.write_number(row, 1, float(values[1]), yen_fmt)
month_ws.write_number(row, 2, int(values[2]), int_fmt)
month_ws.write_number(row, 3, int(values[3]), int_fmt)
month_total_row = len(monthly) + 1
month_ws.write_string(month_total_row, 0, "合計", total_text_fmt)
month_ws.write_formula(
month_total_row, 1, f"=SUM(B2:B{month_total_row})", total_yen_fmt, float(monthly["売上金額"].sum())
)
month_ws.write_formula(
month_total_row, 2, f"=SUM(C2:C{month_total_row})", total_int_fmt, int(monthly["数量"].sum())
)
month_ws.write_formula(
month_total_row, 3, f"=SUM(D2:D{month_total_row})", total_int_fmt, int(monthly["件数"].sum())
)
month_ws.set_column("A:A", 12)
month_ws.set_column("B:D", 14)
# 商品別集計シート(構成比とデータバー)
product_headers = ["商品名", "カテゴリ", "売上金額", "数量", "構成比"]
product_ws = book.add_worksheet(SHEET_PRODUCT)
for col, name in enumerate(product_headers):
product_ws.write(0, col, name, header_fmt)
for row, values in enumerate(product[product_headers].itertuples(index=False, name=None), start=1):
product_ws.write_string(row, 0, str(values[0]), text_fmt)
product_ws.write_string(row, 1, str(values[1]), text_fmt)
product_ws.write_number(row, 2, float(values[2]), yen_fmt)
product_ws.write_number(row, 3, int(values[3]), int_fmt)
product_ws.write_number(row, 4, float(values[4]), pct_fmt)
product_total_row = len(product) + 1
product_ws.write_string(product_total_row, 0, "合計", total_text_fmt)
product_ws.write_string(product_total_row, 1, "", total_text_fmt)
product_ws.write_formula(
product_total_row, 2, f"=SUM(C2:C{product_total_row})", total_yen_fmt, float(product["売上金額"].sum())
)
product_ws.write_formula(
product_total_row, 3, f"=SUM(D2:D{product_total_row})", total_int_fmt, int(product["数量"].sum())
)
if len(product) > 0:
product_ws.conditional_format(
1, 2, len(product), 2, {"type": "data_bar", "bar_color": "#63C384"}
)
product_ws.set_column("A:A", 24)
product_ws.set_column("B:B", 12)
product_ws.set_column("C:E", 14)
# グラフシート(月別の縦棒グラフと商品別の横棒グラフ)
chart_ws = book.add_worksheet(SHEET_CHART)
chart_ws.set_column("A:A", 3)
if len(monthly) > 0:
month_chart = book.add_chart({"type": "column"})
month_chart.add_series(
{
"name": "月別売上金額",
"categories": [SHEET_MONTH, 1, 0, len(monthly), 0],
"values": [SHEET_MONTH, 1, 1, len(monthly), 1],
"fill": {"color": "#1F4E79"},
"data_labels": {"value": True, "num_format": "#,##0"},
}
)
month_chart.set_title({"name": "月別売上推移"})
month_chart.set_x_axis({"name": "月"})
month_chart.set_y_axis({"name": "売上金額(円)", "num_format": "#,##0"})
month_chart.set_legend({"position": "bottom"})
month_chart.set_size({"width": 720, "height": 400})
chart_ws.insert_chart("B2", month_chart)
if len(product) > 0:
product_chart = book.add_chart({"type": "bar"})
product_chart.add_series(
{
"name": "商品別売上金額",
"categories": [SHEET_PRODUCT, 1, 0, len(product), 0],
"values": [SHEET_PRODUCT, 1, 2, len(product), 2],
"fill": {"color": "#C0504D"},
}
)
product_chart.set_title({"name": "商品別売上金額"})
product_chart.set_x_axis({"name": "売上金額(円)", "num_format": "#,##0"})
product_chart.set_y_axis({"name": "商品名", "reverse": True})
product_chart.set_legend({"none": True})
product_chart.set_size({"width": 720, "height": 400})
chart_ws.insert_chart("B24", product_chart)
book.close()
return {
"path": os.path.abspath(out_path),
"size": os.path.getsize(out_path),
"sheets": [SHEET_DETAIL, SHEET_MONTH, SHEET_PRODUCT, SHEET_CHART],
"detail_rows": int(len(detail)),
"months": int(len(monthly)),
"products": int(len(product)),
"total": float(agg["total"]),
}
# CLI引数の定義
def build_parser() -> argparse.ArgumentParser:
"""argparseのサブコマンド(sample / summary / report)を組み立てる。"""
parser = argparse.ArgumentParser(
prog="sales_report_cli.py",
description="売上CSVを月別・商品別に集計し、グラフ付きExcelレポートを生成する",
)
sub = parser.add_subparsers(dest="command", required=True)
sample_parser = sub.add_parser("sample", help="サンプル売上CSVを作成する")
sample_parser.add_argument("--out", default="sales.csv", help="出力するCSVのパス")
sample_parser.add_argument("--rows", type=int, default=200, help="生成する明細件数")
sample_parser.add_argument("--seed", type=int, default=42, help="乱数シード")
summary_parser = sub.add_parser("summary", help="月別・商品別の集計結果を表示する")
summary_parser.add_argument("--input", required=True, help="入力する売上CSVのパス")
report_parser = sub.add_parser("report", help="グラフ付きExcelレポートを生成する")
report_parser.add_argument("--input", required=True, help="入力する売上CSVのパス")
report_parser.add_argument("--out", default="sales_report.xlsx", help="出力するExcelのパス")
return parser
# エントリーポイント
def main() -> None:
"""サブコマンドに応じて処理を実行し、結果を標準出力に表示する。"""
args = build_parser().parse_args()
if args.command == "sample":
info = write_sample_csv(args.out, args.rows, args.seed)
print("サンプル売上CSVを作成しました")
print(f" 出力ファイル : {info['path']}")
print(f" 明細件数 : {info['rows']:,}件")
print(f" 対象期間 : {info['first_date']} 〜 {info['last_date']}")
print(f" 商品数 : {info['products']}種類")
print(f" 列構成 : {', '.join(CSV_COLUMNS)}")
return
frame = load_sales(args.input)
agg = aggregate_sales(frame)
if args.command == "summary":
print(format_summary(args.input, frame, agg))
return
info = write_excel_report(frame, agg, args.out)
print("Excelレポートを生成しました")
print(f" 入力CSV : {os.path.abspath(args.input)}")
print(f" 出力ファイル : {info['path']}")
print(f" ファイルサイズ : {info['size']:,}バイト")
print(f" シート構成 : {' / '.join(info['sheets'])}")
print(f" 明細行数 : {info['detail_rows']:,}行")
print(f" 集計表 : 月別{info['months']}行 / 商品別{info['products']}行")
print(f" 売上合計 : {info['total']:,.0f}円")
print(" グラフ : 月別売上の縦棒グラフと商品別売上の横棒グラフを『グラフ』シートに配置")
if __name__ == "__main__":
main()
コード全文は上の折り畳みに入れてあるので、全部を上から読む必要はありません。ここでは特に重要な部分だけを抜き出して、何をしているのか順番に見ていきます。
標準ライブラリと2つの主役の読み込み
import argparse
import os
import random
from datetime import date, timedelta
import pandas as pd
import xlsxwriter引数処理のargparse、パス操作のos、サンプル用の乱数randomは標準ライブラリなので追加インストールは不要です。集計を担うpandasとExcel生成を担うxlsxwriterだけが外部ライブラリになります。空行で標準ライブラリと外部ライブラリを分けておくと、依存関係が一目で分かります。
列名とシート名を定数にまとめる
CSV_COLUMNS = ["売上日", "商品名", "カテゴリ", "地域", "単価", "数量"]
DETAIL_COLUMNS = CSV_COLUMNS + ["売上金額"]
SAMPLE_START = date(2024, 1, 1)
SAMPLE_DAYS = 182
SHEET_DETAIL = "明細"CSV_COLUMNSが入力CSVの列、DETAIL_COLUMNSはそれに計算列の売上金額を足した明細シート用の列です。リストの足し算で作っているので、入力列を増やしても明細側が自動で追随します。サンプルデータの開始日と日数、シート名もここで一括管理しました。
ensure_parent_dirで出力先フォルダを用意する
def ensure_parent_dir(path: str) -> None:
"""出力パスの親フォルダが無ければ作成する。"""
parent = os.path.dirname(os.path.abspath(path))
if parent:
os.makedirs(parent, exist_ok=True)os.path.abspathで絶対パスに直してから親フォルダ名を取り出し、無ければ作成します。exist_ok=Trueを付けているので、すでにフォルダがある場合もエラーになりません。この関数があるおかげで、reports/sales_report.xlsxのように存在しないフォルダを出力先に指定できます。
build_sample_frameで乱数シードを固定する
rng = random.Random(seed)
records = []
for _ in range(rows):
name, category, unit_price = rng.choice(PRODUCT_MASTER)
sold_at = SAMPLE_START + timedelta(days=rng.randrange(SAMPLE_DAYS))random.Random(seed)で専用の乱数生成器を作ると、同じシードなら毎回同じデータが得られます。商品マスタから1件選んで商品名・カテゴリ・単価を同時に取り出し、開始日に日数を足して売上日を決める仕組みです。再現できるサンプルは、集計結果を見比べながら開発するときに助かります。
DataFrame化と売上日での並べ替え
frame = pd.DataFrame(records, columns=CSV_COLUMNS)
return frame.sort_values("売上日").reset_index(drop=True)辞書のリストをpd.DataFrameに渡し、columnsで列の並び順を固定しています。そのあと売上日で並べ替え、reset_index(drop=True)で行番号を0から振り直します。drop=Trueは、振り直す前の行番号を新しい列として残さないための指定です。
to_csvでBOM付きUTF-8として保存
frame.to_csv(path, index=False, encoding="utf-8-sig")
return {
"path": os.path.abspath(path),
"rows": int(len(frame)),
"first_date": str(frame["売上日"].iloc[0]),index=Falseで行番号を書き出さず、encoding="utf-8-sig"でExcelがそのまま開ける文字コードにしています。戻り値は絶対パスや件数、先頭日付をまとめた辞書で、これを使って実行後のメッセージを組み立てる仕組みです。関数の中でprintせずに値を返す形にすると、後から表示の仕方だけを変えられます。
load_salesの存在チェックと必須列の検証
if not os.path.exists(path):
raise FileNotFoundError(f"売上CSVが見つかりません: {os.path.abspath(path)}")
frame = pd.read_csv(path, encoding="utf-8-sig")
missing = [column for column in CSV_COLUMNS if column not in frame.columns]最初にファイルの有無を調べ、無ければ絶対パス付きのメッセージでFileNotFoundErrorを投げます。読み込み後はCSV_COLUMNSと実際の列を突き合わせ、足りない列をリストに集めます。ここで止めておけば、集計の途中で意味の分かりにくいエラーに悩まされません。
pandasのgroupbyで月別・商品別に集計する
集計の前段で型を整えるのが要点です。売上日はpd.to_datetimeで日付型に、単価と数量はpd.to_numericで数値型に変換してから、売上金額と月の列を追加しています。
文字列のまま掛け算しようとすると意図しない結果になりますし、日付が文字列だと月の切り出しもできません。前処理を関数load_salesに閉じておくと、集計側は型を気にせず書けます。
型を整えたあとはgroupbyの出番です。月の列でまとめれば月別の推移、商品名でまとめれば商品別の実績になり、同じ書き方で切り口だけを変えられます。
変換できない値の扱いは引数で選べるので、汚れたCSVを扱うときは公式ドキュメントの記述を確認しておくと安心です。
このセクションの用語
- groupby
- 指定した列の値が同じ行をまとめ、合計や平均などを計算するpandasの機能です。集計軸を変えるだけで別の切り口の表が作れます。
- to_datetime
- 文字列などを日付時刻型に変換するpandasの関数です。変換後は年や月の取り出しが簡単になります。
- to_numeric
- 文字列を数値型に変換するpandasの関数です。変換できない値をどう扱うかを引数で指定できます。
- NaN
- 数値として欠けている状態を表す特別な値です。集計時は多くの関数が自動的に除外して計算します。
集計を実行する前に、手元で確かめておきたい3つの確認行動は次のとおりです。
-
summaryサブコマンドを先に流し、件数と売上合計が想定どおりか目で確認する - 入力CSVの見出し行に6つの必須列がそろっているか、表計算ソフトで開いて確認する
- サンプルの先頭日付と末尾日付を見て、集計したい期間が月をまたいでいるか確かめる
さらに詳しく学べるPython研修の詳細はこちら参考:
©pandas公式ドキュメントpandas.to_numericIf 'coerce', then invalid parsing will be set as NaN.
Pythonでグラフ付きExcel売上レポートCLIのエラー対処
ここからは、この種のツールを動かすときに一般に遭遇しやすいエラーを整理します。原因の切り分け方まで分かっていれば、初回実行でつまずいても復帰は早くなります。
多いのはパスの取り違えと、Excelでファイルを開いたまま上書きしようとするケースです。エラーメッセージの末尾とファイルパスを落ち着いて読むのが近道でしょう。
このセクションの用語
- 例外
- 処理を続けられない状況をプログラムが知らせる仕組みです。名前を見れば何が起きたか大まかに判断できます。
- トレースバック
- 例外が起きるまでの呼び出し経路を示すエラー表示です。最後の行に例外名とメッセージが出ます。
| エラー例 | 原因 | 対処 |
|---|---|---|
| FileNotFoundError: 売上CSVが見つかりません |
--inputのパス違い、または実行したフォルダがCSVの場所と違う |
メッセージに出る絶対パスを確認し、正しいパスを指定して再実行する |
| ModuleNotFoundError: No module named 'xlsxwriter' | ライブラリが未インストール、または別の仮想環境で実行している | 有効化中の環境を確認し、その環境にインストールしてから実行する |
| ValueError: --rowsには1以上の件数を指定してください |
--rowsに0以下の値を渡している |
--rows 240のように1以上の件数を指定する |
| xlsxwriter.exceptions.FileCreateError: [Errno 13] Permission denied | 出力先の.xlsxをExcelで開いたまま上書きしようとしている | 対象ファイルを閉じるか、別の出力ファイル名を指定する |
| UnicodeDecodeError | 入力CSVがShift_JISなどUTF-8以外の文字コードで保存されている | CSVをUTF-8で保存し直すか、読み込み時のencodingを実データに合わせる |
グラフ付きExcel売上レポートCLIで注意したい点
最初に引っかかりやすいのは文字コードです。日本語を含むCSVをExcelで直接開くと化ける場合があり、保存時にutf-8-sigを選ぶだけで解決することが多くあります。
次に多いのが型の扱いです。CSVから読んだ値は文字列になりがちなので、pd.to_datetimeとpd.to_numericで型を整える前に計算を書くと、数値が足りない・日付が使えないという事態になります。
出力パスも落とし穴です。相対パスはコマンドを実行したフォルダを基準に解釈されるため、思った場所にファイルが出ないときはまず現在のフォルダを確認しましょう。
ポイントとしては、文字コード・型変換・出力先という3つの注意点を押さえておけば、ほとんどの初回トラブルは避けられます。
文字コード:utf-8-sigで統一
型変換:to_datetimeを先に
出力先:親フォルダを自動作成
拡張子:.xlsxで保存
PythonのXlsxWriterで明細・集計表・売上グラフを書き出す
Excel側はxlsxwriter.Workbookを直接開き、明細・月別集計・商品別集計・グラフの各シートを順に書き込む構成にしました。明細シートには見出しや金額の書式を当て、オートフィルタを設定して並べ替えや絞り込みをすぐ試せるようにしています。
集計シートには合計行をSUM関数として書き込みました。数式のまま入れておくと、受け取った人がExcel上で数字を触っても合計が追随します。
さらに商品別集計シートの金額列にはデータバーを設定し、表を見ただけで大小が分かる見た目にしています。グラフシートには集計シートのセル範囲を参照する縦棒グラフと横棒グラフを配置しました。
グラフがセル範囲を参照しているので、Excelで元の数値を直せばグラフの形も変わります。書き終えたらワークブックを閉じ、保存先のパスと件数、売上合計をprintで表示して締めています。
Excel出力で特に押さえておきたいポイントを次の3点にまとめました。
このセクションの用語
- ワークブック
- Excelファイル全体を指す単位です。XlsxWriterではワークブックを作り、その中にシートを追加していきます。
- オートフィルタ
- 見出し行に絞り込み用のボタンを付けるExcelの機能です。並べ替えや条件指定が数クリックで行えます。
- データバー
- セルの値の大きさを横棒の長さで表す条件付き書式です。表のまま大小関係を把握できます。
- SUM関数
- 指定範囲の合計を求めるExcelの関数です。数式として書き込めば、値を編集しても再計算されます。
終了処理:workbook.close
グラフ参照:セル範囲で指定
数値書式:num_formatで統一
グラフ付きExcel売上レポートCLIの動作確認
動作確認は4回のコマンドで行い、いずれも終了コード0で正常に終了しました。実行時の画面はキャプチャとして残しています。
最初にサンプル売上CSVを生成し、続けて集計内容を表示して中身を確かめました。そのうえでExcelレポートを出力し、最後に存在しないフォルダを出力先に指定するパターンも試しています。
4回目のコマンドではreports/配下を指定しましたが、ensure_parent_dirがフォルダを用意するため、事前にディレクトリを作らずに実行できました。
どのコマンドも結果を標準出力に表示する作りなので、ログを見ればどのファイルが何件から作られたのかを追えます。
このセクションの用語
- 終了コード
- コマンドが終わったときにOSへ返す数値です。0は正常終了を意味し、失敗すると0以外になります。
- 標準出力
- printの結果が流れ込む、コマンド実行画面への出力先です。ログの確認やファイルへの保存に使えます。
実際に流した4つのコマンドと、それぞれで確認したかったことは次のとおりです。
-
python sales_report_cli.py sample --out sales.csv --rows 240 --seed 42でサンプルCSVを生成 -
python sales_report_cli.py summary --input sales.csvで集計内容を画面表示 -
python sales_report_cli.py report --input sales.csv --out sales_report.xlsxでExcelレポートを生成 -
python sales_report_cli.py report --input sales.csv --out reports/sales_report_2024h1.xlsxでサブフォルダ出力を確認




グラフ付きExcel売上レポートCLIの活用例
この構成は売上に限らず、定期的に同じ形の表とグラフを配る業務に応用できます。集計軸と参照するセル範囲を差し替えるだけで、別のレポートに転用できるからです。
一般には、手作業のコピー貼り付けが毎月発生している資料ほど効果が出やすい領域だといえます。
このセクションの用語
- バッチ処理
- 人が操作せず、決めた時刻や条件でまとめて実行する処理です。CLIツールは組み込みやすい形式です。
| 使える場面 | 具体的な使い方 |
|---|---|
| 月次売上会議の資料作り | 会計システムから出したCSVをreportに渡し、明細・月別集計・グラフまで入ったブックを毎月同じ体裁で配布する |
| 店舗別・担当別実績共有 |
groupbyの集計軸を地域や担当者の列に差し替え、集計シートのデータバーで実績金額の大小を一目で見せる |
| 定期バッチでのレポート自動配信 | タスクスケジューラやcronからreportを実行し、出力先をreports/年月のフォルダにして履歴として蓄積する |
| 自作ツールの動作確認やデモ |
sampleでシードを固定した疑似CSVを作り、集計ロジックを変更した前後で同じ入力の結果を見比べる |
| 非エンジニアへの数値提供 | Excelとして渡すことで、受け取った側がSUM関数の合計行やオートフィルタを使って自分で切り口を変えられる |
グラフ付きExcel売上レポートCLI開発のまとめ
売上CSVの集計からグラフ付きExcelレポートの生成までを、sales_report_cli.pyの1ファイルにまとめました。処理を4段に分け、argparseのサブコマンドで呼び分ける構成が読みやすさに効いています。
pandas側はpd.to_datetimeとpd.to_numericで型を整えてからgroupbyにかけるのが基本形でした。XlsxWriter側はシートごとに役割を決め、グラフから集計シートのセル範囲を参照させています。
動作確認は4回のコマンドで行い、いずれも終了コード0で完了しました。出力はすべてprintで確認できるので、まずはsampleでCSVを作り、summaryで中身を眺めてからreportを実行する順番で試してみてください。
参考にした一次情報
- ^ XlsxWriter Documentation - Working with Charts. https://xlsxwriter.readthedocs.io/working_with_charts.html, (参照26-09-03).
- ^ XlsxWriter Documentation - The Worksheet Class. https://xlsxwriter.readthedocs.io/worksheet.html, (参照26-09-03).
- ^ pandas API reference - DataFrame.groupby. https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.groupby.html, (参照26-09-03).
- ^ XlsxWriter Documentation - The Exceptions Class. https://xlsxwriter.readthedocs.io/exceptions.html#exception-filecreateerror, (参照26-09-03).
- ^ Python標準ライブラリ - argparse. https://docs.python.org/ja/3/library/argparse.html, (参照26-09-03).
※内容は執筆時点のものです。ライブラリやサイトの仕様は変わる可能性があるため、公式ドキュメントもあわせてご確認ください。










