Python ピボットテーブルによるクロス集計。また、時系列データをグラフ化する「pandas」

 pandasのpivot_tableで比較的簡単に処理できます。本記事では、データ処理の例として、愛知県の次のリンク先https://www.pref.aichi.jp/site/covid19-aichi/の中程にある新型コロナの「愛知県内の発生事例」のPDFファイル「8月まで [PDFファイル/357KB]」を使用しました。このPDFファイルは下図のような表が99ページに渡って複数あります。

f:id:HK29:20200906211527p:plain

 本コードの実施例を以降に示します。本コードを実行すると、上記pdfファイルを読み取って、下図のようにcsvファイルを作成します。これを行うためのPythonライブラリは「PyPDF2 」と「camelot」を使用しています。

f:id:HK29:20200906211750p:plain

そして、下図のように感染者数が多い市町村順に横棒グラフ化します。これにより、視覚的に比較確認できます。

f:id:HK29:20200906212148p:plain

次に、下図のようにクロス集計を行ってcsvファイルにします。これを行うには、「pandas」のpivot_tableを使用すると簡単に作成できます。

f:id:HK29:20200906212416p:plain

最後に、配列(リスト)で指定した複数の市町村を下図のように時系列でグラフ化します。これにより、感染者数の変化を可視化できて比較できます。

f:id:HK29:20200906212628p:plain

■本プログラム

# -*- coding: utf-8 -*-
# conda install -c conda-forge camelot-py
# pip install japanize-matplotlib
import warnings
from pathlib import Path
from tqdm import tqdm

from PyPDF2 import PdfFileReader
from PyPDF2 import PdfFileWriter
import pandas as pd
import camelot

import os
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
import japanize_matplotlib

import datetime
now = datetime.datetime.now()
now = now.strftime("%y%m%d")
import time

### pdfをページ毎に分割してpdfを生成する。またそのファイル名をリストで返す
### PyPDF2を使用
def my_split_pdf_func(input_pdf):
    file_name_list = []
    with open(input_pdf, 'rb') as f1:
        input = PdfFileReader(f1)
        pgnum = input.getNumPages()
        for i in range(pgnum):
            file_name = str(i+1) + "page.pdf"
            output = PdfFileWriter()
            output.addPage(input.getPage(i))
            outputfile = open(file_name, 'wb')
            output.write(outputfile) 
            outputfile.close()
            file_name_list.append(file_name)
    print(file_name_list)
    
    return file_name_list


### ランキング作成して横棒グラフ化する関数。引数Xはカウントする列名、引数Yは数えた数
def create_ranking_csv_img_func(DF, X_column, Y_column):
    df = DF.dropna(subset=[X_column])
    df = df.dropna(axis=1)
    df = df.groupby([X_column]).size()
    df = df.reset_index(name=Y_column)
    df = df.sort_values(by=Y_column, ascending=True)
    print(df)

    df_x = df[X_column]
    df_y = df[Y_column]
    
    y_np = np.array(df_y)
    plt.figure(figsize=(8,10))
    plt.barh(range(len(df_x)), df_y, tick_label=df_x, align="center", color="magenta", height=0.8)
    df_x_r = df_x.reset_index(drop=True) # インデックスをリセットして振り直す
    with open(save_name + '_ranking.csv', 'a') as f:
        for i, j in enumerate(y_np):
            plt.text(j, (i+0.5), str(int(j)), ha='left', va='top')
            mystr_list = [df_x_r[i], str(int(j)), '\n']
            mystr = ','.join(mystr_list)
            f.write(mystr)

    #plt.title()
    plt.xlabel('count', fontsize=10)
    if range_X:
        plt.xlim([range_X[0], range_X[1]])
    plt.grid(which="major", axis="x", color="black", alpha=0.8, linestyle="-", linewidth=1)
    plt.tight_layout()
    #plt.show()
    plt.savefig(save_name + '_ranking.png')
    plt.close()
    

### ピボットテーブル処理
def create_time_series_plotgraph_func(DF, TIME, LEGEND, COUNT, Y_list):
    print(DF)
    print(TIME)

    # 日付を(文字列型でなくて)datetime型に変換する
    DF[TIME] = DF.apply(lambda a: f"{str(2020)}-{a[TIME]}", axis=1) # 西暦2020年を追記
    DF[TIME] = DF[TIME].str.replace('月', '-') # 月を-に置換
    DF[TIME] = DF[TIME].str.strip('日') # 日を削除
    DF[TIME] = pd.to_datetime(DF[TIME]) # datetime型へ変換(これで後で、日付順に並び替え出来る)
    print(DF)

    # pivot_tableで、日付をインデックスに、凡例を列に、クロス集計
    df = pd.pivot_table(DF,
                        index = TIME, # 発表日
                        columns = LEGEND, # 住居地
                        values = COUNT, # count
                        aggfunc='sum')
    print(df)

    # インデックス(日付)でソートして、csvファイルで保存
    df_sorted = df.sort_index()
    cross_data_file_name = save_name + '_' + TIME + '_' + LEGEND + '_cross.csv'
    df_sorted.to_csv(cross_data_file_name,
                     header = True,
                     index = True,
                     encoding = 'cp932',
                     sep = ',')

    # 保存したcsvファイルを読み出す(前処理不要なcsvファイルを読み込む場合はここからスタート)
    df = pd.read_csv(cross_data_file_name,
                     encoding='cp932')
    
    ### 時系列グラフを作成する
    # 並び替えた日付の型を文字列へ変換する(横軸として表記するため)
    #df = df_sorted.astype({TIME: 'object'})
    fig = plt.figure(figsize=(15,6))
    plt.style.use('bmh') # グラフの見栄え 'bmh' 'dark_background' 'fivethirtyeight'
    ax = fig.add_subplot(1,1,1)
    for i, Y in enumerate(Y_list):
        print(Y)
        try:
            df.plot(x=TIME,
                    y=Y,
                    kind='line',
                    ax=ax,
                    color=cm(i))
            ax = plt.gca()
            x_ticklabels = ax.get_xticklabels() # デフォルト目盛り表記をゲット
            plt.setp(x_ticklabels, rotation=45, fontsize=14) # 目盛り表記を45度回転
            tick_spacing = 15 # 目盛り表示する間隔
            ax.xaxis.set_major_locator(ticker.MultipleLocator(tick_spacing))
        except KeyError:
            pass
    
    y_ticklabels = ax.get_yticklabels() # デフォルト目盛り表記をゲット
    plt.setp(y_ticklabels, rotation=0, fontsize=16) # 目盛りのフォントサイズを変更
    
    ax.set_xlabel(None) # 軸ラベルを表示しない場合は,Noneを指定
    ax.set_ylabel(COUNT, fontsize=16)
    
    ax.grid(True)
    plt.legend(bbox_to_anchor=(1.01, 1), loc='upper left', borderaxespad=0, fontsize=16)
    plt.rcParams['font.size'] = 18
    plt.subplots_adjust(right=0.7)
    plt.tight_layout() # 文字はみだしを防ぎグラフレイアウトを微修正する
    fig.savefig(cross_data_file_name + '.png')
    plt.close()


def main():

    ### pdfをページ毎に分割してpdfを生成する。またそのファイル名をリストで返す
    page_pdf_path_list = my_split_pdf_func(pdf_path)

    ### csvファイルへ変換する
    # 1頁毎にDataFrame形式に変換後、リストへ追記してゆく
    for page_pdf_path in tqdm(page_pdf_path_list):
        # pdfファイルの1頁から最終頁までリストで取得する
        table = camelot.read_pdf(page_pdf_path,
                                 pages='1-end',
                                 split_text=True,
                                 strip_text='\n',
                                 encoding='cp932')
        
        # データフレーム化
        try:
            df = table[0].df
            print(df)
        except IndexError:
            print('read error of pdf2df -> pass')
        
        # csvファイルへ出力
        # cp932でファイルオブジェクトを作成する。この時、エラー文字は無視する
        with open(now + '_' + file_name + '.csv',
                  mode="a", newline='', encoding="cp932", errors="ignore") as f:
            df.to_csv(f,
                      index = False,
                      header = False,
                      mode = 'a',
                      encoding='utf-8') # 書き出す文字コードを指定 'utf-8' 'shift_jis' 'cp932'
        os.remove(page_pdf_path)
    
    # csvファイルを読み出し(pdfでなくてcsv読み出す時はここからスタート)
    df_csv = pd.read_csv(now + '_' + file_name + '.csv', header=0, index_col=0, encoding='cp932')
    
    # ランキングを作成する関数
    create_ranking_csv_img_func(df_csv, target_column, 'count')
    
    # ピボットテーブルの前処理
    df_new = df_csv.assign(count=1) # 各行に個数の列がない場合に追加
    # ピボットテーブルで処理
    create_time_series_plotgraph_func(df_new, x_time, target_column, 'count', y_list)

if __name__ == '__main__':
    pdf_path = '345284_愛知県8月まで.pdf'
    #pdf_path = '345533_愛知県9月.pdf'
    
    file_name = Path(pdf_path).stem #ファイルパスから、拡張子抜きファイル名を取得
    save_name = now + "_" + file_name
    
    # ランキング作成用
    target_column = '住居地'
    range_X = ()
    
    # ピボットテーブルでクロス集計後の時系列グラフ化用
    x_time = '発表日'
    y_list = [
        '名古屋市',
        '豊田市',
        '一宮市',
        '岡崎市',
        '刈谷市',
        '日進市',
    ]
    # グラフにプロットするデータの色マップの指定
    cm_name = 'gist_rainbow' # 'flag' 'jet' 'gist_rainbow' 'spring' 'bwr' 'cool'
    cm = plt.get_cmap(cm_name, len(y_list))
    
    # わかっている警告(表示しなくてよい警告)は非表示にする
    warnings.resetwarnings()
    warnings.simplefilter('ignore', UserWarning)
    
    start = time.time()
    main()
    print('run time: ', time.time() - start) # 処理時間を表示する
    print('finished')

●関連記事

hk29.hatenablog.jp

hk29.hatenablog.jp

以上

<広告>