伝票明細内容をクラス分類したい時ってどうしてます？



## はじめに
身近にはいろんな伝票があります。
データ分析を前提に、日付、品名などの種類、会社名等、属性分類されている伝票もあれば、「明細にわかるように書いておいて」というものもあります。

明細で内容を判断しなければならない伝票は、分類したり、集計したりするのは大変です。明細に書かれている内容は、句読点のある文章ではなく、注文番号、略称、現場しかわからないいわば記号であることが多いからです。

あるコンテンツ明細から「エンタメ」を抽出したいという場合、明細に書かれる内容は、「エンタメ、エンターテイメント、エンタ、ｴﾝﾀ、ｴﾝﾀﾒ、entertainment、Entertainment、娯楽…」等、大文字・小文字、カタカナ、英語、日本語、略称が入り混じっているかもしれません。
この例だけでも嫌になりますが、内容がイメージできる抽象語が書かれているものはまだマシなのかもしれません。

また、ペン、ノート、消しゴム、ホッチキス、修正テープ... → 「文房具」と区分したい場合、「文房具」に相当するものにどのようなものがあるのか？は、あらかじめわかっている訳でもありませんので、逐次対象を追加しながら網羅性を確認するということを繰り返していかないといけません。
　
ある程度納得できる分類を得るには、試行回数を稼ぐ必要がありますので、このようなケースはExcelよりもPythonのほうが手早くできるのではないかと思います。
　
このような伝票明細分類をしないといけない機会がありましたので、忘れぬうちに残しておこうというのが今回の記事となります。
　
　
## 実行条件など

>Google colabで実行
　
## ライブラリのインポート

```Python:
#@title ライブラリのインポート
import pandas as pd
import numpy as np
import warnings
warnings.filterwarnings('ignore')
import string as str
```

## CSV読込み

```Python:
#@title CSV読込み
from google.colab import files
uploaded = files.upload() #Upload
target = list (uploaded.keys()) [0]
df = pd.read_csv (target)
```

## 分類キーワード設定
以下、備品購入の伝票を想定し、3つの分類を仮想しています。
Key1は文房具、Key2はパソコン関係、Key3は図書関係です。
これは、明細内容にこの文字があればというキーワードです。文字間の「| 」はor を意味します。

```Python:
Key1 = 'pen|ペン|消しゴム' #@param {type:“raw”}
Key2 = 'パソコン|pc|モニタ|LCD|usb' #@param {type:“raw”}
Key3 = '本|書籍|電子書籍|参考書|規格|図書' #@param {type:“raw”}
```

## 明細内容をキーワードで区分し、データフレームに格納
データに「明細」というカラムがあり、この明細内容にKey1のキーワードがあれば、データフレームの「Class1」という列にTrue、なければFalseを格納するという内容です。
Class2、Class3も確認するキーワードがそれぞれ異なるだけで処理は同じです。

case=False とすると、設定したキーワードが大文字であっても小文字であっても抽出の対象となります。

```Python:
#@title キーワード区分
df['Class1'] = df[ '明細' ].str.contains(Key1, case=False)
df['Class2'] = df[ '明細' ].str.contains(Key2, case=False)
df['Class3'] = df[ '明細' ].str.contains(Key3, case=False)

df.head( )
```

## 集計
各ClassのTrueの数（キーワードに合致した数）をカウント数を求めます。
伝票明細数は、データフレームのレングス数 len(df) となりますので、len(df)から各Classのカウント数を除いた数がノーカウントClass（＝Other_count）となります。
ノーカウント数が多い場合は、キーワード設定が足りないということになりますので、ノーカウント数のチェックは大切です。
また、伝票明細に「金額カラム」がある場合、単純な件数カウントではなく、すべての伝票金額の何割をClass分類できているかという見方も大切でしょう。
100%のClass分類を目指すのではなく、総金額の70％以上のClass分類を目指す等が現実的であると思うからです。
以下はこれらを想定した、単純な四則演算の内容と結果表示です。（あくまで例です）


```Python:
#@title 集計
#Class別カウント
Class1_count = df[df.Class1 == True].Class1.sum()
Class2_count = df[df.Class2 == True].Class2.sum()
Class3_count = df[df.Class3 == True].Class3.sum()
Count_sum = len(df) 
Other_count =len(df) - Class1_count - Class2_count - Class3_count

#Class別 金額集計
Class1_cost = df[df.Class1 == True].金額カラム.sum()
Class2_cost = df[df.Class2 == True].金額カラム.sum()
Class3_cost = df[df.Class3 == True].金額カラム.sum()
Cost_sum = df['金額カラム'].values.sum()
Other_cost = Cost_sum - Class1_cost - Class2_cost - Class3_cost

#Class別 金額比率
Class1_ratio = Class1_cost/Cost_sum
Class2_ratio = Class2_cost/Cost_sum
Class3_ratio = Class3_cost/Cost_sum
Other_cost_ratio = Other_cost/Cost_sum

#Class別 金額比率表示
print('Class別金額比率(全運賃に対する比率) ')
print('\tClass1\t\t',"{:.1%}". format (Class1_ratio))
print('\tClass2\t\t',"{:.1%}". format (Class2_ratio))
print('\tClass3\t\t',"{:.1%}" .format(Class3_ratio))
print('\tOther\t\t',"{:.1%}".format(Other_cost_ratio))
print('\tTotal\t\t',"{:.1%}".format(Cost_sum/Cost_sum))
```

## csv出力
Class分類結果をcsvに出力します。
どの伝票が、どのClassに分類されたか、どのクラスにも分類されなかったノーカウントクラスデータはどのようなデータかなどは、Excelでざっと眺めた方が手っ取り早いでしょう。

```Python:
#@title CSV出力
csv_output = False #@param {type: "boolean"}

#csv出力
if csv_output True:
    df.to_csv('transport_class_data.csv',index=False, encode='utf_8_sig'
    from google.colab import files
    files.download ('transport_class_data.csv')
```
　
## 最後に
句読点を含む文章の場合、形態素分析による語彙カウントが有効だと思いますが、伝票明細はほぼ記号、とはいえ、こんな方法しかないのか？とも思います。
記号の場合、伝票記入担当にしかわからないものも多く、「3ヶ月分だけ区分してみて」等とお願いし、これを教師データとしたり、区分してもらったデータで学習させたりしてもよいでしょう。
ただ、私が試行錯誤したデータでは、これだけではうまくいきませんでした。
担当者毎に伝票の書き方に癖があった、過去データと直近データで記載傾向が異なるところがあったからです。
結局、仮設定したキーワードで試行し、実担当にノーカウントクラスとなったデータを見せ、「これ何？」と聞き、キーワードを見直して、試行するしかありませんでした。
Excelで実行するよりも早くて楽であったとは思いますが、このような場合、こんなアプローチもあるよという方がおられれば、ぜひご教示をお願いいたします。