2019年9月8日日曜日

PythonでPDFの表からデータを抽出する

Python3でPDFのテキストを抽出するではPDFMinerでPDFからテキストを抽出したが、表データが含まれたPDFもよくある。PDFMinerでもテキストデータとして抽出して整形すればできないことはなさそうだが、tabula-javaのPythonラッパーであるtabula-pyを使うと簡単に表のデータを抽出できるので実際にやってみる。

抽出する表を含むPDFは、何かと話題のキャッシュレス決済によるポイント還元制度の公式サイトからダウンロードできる登録加盟店一覧のPDF。


環境


Windows10(1903)のWSL(Ubuntu 18.04)。



ポイント還元制度の登録加盟店一覧のPDF


まずは、データを抽出するPDFの内容を確認しておく。ポイント還元制度の登録加盟店一覧のPDFを以下のコマンドでダウンロード。ファイル名はkameiten_touroku_list.pdfで、サイズは約31MB。


9月7日時点でダウンロードできたPDFは、9月5日時点の登録申請状況をもとにした一覧で6360ページある。1ページ目は目次、2ページ目は加盟登録申請状況で、3ページ目から加盟店一覧の表が続く。加盟店一覧の表は② 固定店舗(EC・通信販売を除く)、③EC・通信販売(楽天市場)、④ EC・通信販売(Yahoo!ショッピング)、⑤ EC・通信販売(その他ECサイト)の4種類あるが、今回は② 固定店舗(EC・通信販売を除く)の表からデータを抽出する。対象のページは3ページから5536ページまで。

② 固定店舗(EC・通信販売を除く)の表には「No.」「都道府県」「市町村区」「事業所名(屋号)」「業種」「還元率」の6つのカラムがあるが、「業種」は2つのセルからなる。また、表のカラム名の行は各ページにある。


tabula-pyのインストール


PDFからPythonで表データを抽出するためにtabula-pyをインストールするが、先にJDKをインストールしておく。


続いてtabula-pyのインストール。



PDFから表データの抽出


tabula-pyでPDFから表データを抽出してみる。tabula-pyは読み込んだPDF内の表をPandasのデータフレームとして返してくれる。以下のコードでPDFの表をデータフレームとして抽出できる。

import pandas as pd
import tabula

PDF_PATH = 'kameiten_touroku_list.pdf'

def main():
    # lattice=Trueでテーブルの軸線でセルを判定
    df = tabula.read_pdf(PDF_PATH, lattice=True, pages = '3-5536')
    print(df.head())

    # データフレームの情報確認
    print(df.info())
if __name__ == '__main__':
    main()

以下のようにPDFの表がデータフレームとして抽出できた。最後のカラム名が「Unnamed: 6」になっているのは「業種」カラムが2セルに分かれているため。


また、データフレームの情報も確認しておく。PDFを見る限り232391行のデータがあるはずだが、各ページのカラム名の行も取り込んでいるので237924行ある。



抽出したデータのクリーニング


とりあえずデータフレームにしたが、カラム名の行や重複データが混ざっているのでクリーニングする。
import pandas as pd
import tabula

PDF_PATH = 'kameiten_touroku_list.pdf'

def cleaning(df_original):
    df = df_original.copy()

    # 「業種」カラムが2セルに分かれていてヘッダーとデータがずれるのでカラム名を再設定する
    df.columns = ['No.', '都道府県', '市町村区', '事業所名(屋号)', '業種1', '業種2', '還元率']

    # 各ページのテーブルにヘッダーがあるので、ヘッダーを除外
    df = df[df['No.']!='No.']

    # 「No.」カラムの数字にはカンマがあるので削除してint型に変換
    df['No.'] = pd.to_numeric(df['No.'].str.replace(',', ''))

    # 「%」削除してintに変換
    df['還元率'] = pd.to_numeric(df['還元率'].str.replace('%', ''))

    col_cat = ['都道府県', '市町村区', '事業所名(屋号)', '業種1', '業種2']
    # category型に変換
    df[col_cat] = df[col_cat].astype('category')

    # 重複データ削除
    df = df[~df[col_cat].duplicated()]

    # インデックス再設定
    df.reset_index(drop=True, inplace=True)

    return df

def main():
    # lattice=Trueでテーブルの軸線でセルを判定
    df = tabula.read_pdf(PDF_PATH, lattice=True, pages = '3-5536')
    #print(df.head())

    # データフレームのクリーニング
    df_cleaned = cleaning(df)

    # データフレーム情報の確認
    print(df_cleaned.info())

    # カラムごとのユニークなデータの数
    print(df_cleaned.nunique())
if __name__ == '__main__':
    main()

クリーニング後のデータフレーム情報の確認。最終的に228687行のデータになった。


カラムごとのユニークなデータの数を確認。都道府県は47だし、還元率は2%と5%だけなので大丈夫そう。



データの可視化


せっかくクリーニングもしたので、matplotlibで業種ごとの還元率を比較してみる。以下はグラフ作成部のみのコード。
# ターミナル環境用の設定
import matplotlib
matplotlib.use('Agg')

import matplotlib.pyplot as plt

# 日本語フォントの設定
font = {'family' : 'TakaoPGothic'}
matplotlib.rc('font', **font)

# 業種ごとの積み上げ棒グラフ
plt.figure()
df_sector = df_cleaned.groupby(['業種2', '還元率'])['業種2'].count().unstack('還元率').fillna(0)
df_sector.plot(kind='bar', stacked=True)
plt.tight_layout()
plt.savefig('sector_stacked.png')

結果は以下の通り。還元率5%のところが多いが、ガソリンスタンドはほとんどが2%、食料品は約半分の店舗が2%になっている。



0 件のコメント:

コメントを投稿