8
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Outlookから日報メールを取得してネガポジ判定をしてみた。

Last updated at Posted at 2022-08-02

■概要

テキストデータのネガポジ判定の手法を学んだので、
試しに昨年新卒入社した時の日報でネガポジ判定を行ってみました。
書いたソースコードの紹介と共に、
「日報の提出時間が遅くなるにつれて、所感部分の文字数が減り、またPN値も下がるのでは」
という疑問を解明して参ります。

■環境

OS: Windows10
言語: Python 3.10.4
IDE: Visual Studio Code (バージョン 1.69.2)

■分析の流れ

以下の手順で日報データの分析を行いました。

  1. Outlookのフォルダから自分の送った日報のデータを取得、CSVファイルとして保存。
  2. CSVファイルを読み込んで、DataFrameの整形
  3. 極性辞書の読み込み、PN値の算出
  4. 外れ値の処理
  5. データ可視化1,2
  6. 結果と考察

■ソースコード紹介

では早速、用いたコードを紹介して参ります。

0.モジュール、オブジェクトのインポート

import win32com.client
import re
import pandas as pd
import matplotlib.pyplot as plt
import MeCab
import japanize_matplotlib
import datetime as dt
import numpy as np
import seaborn as sns
from scipy.stats import pearsonr

1.Outlookから日報データを取得

今回、WindowsアプリケーションよりOutlookのメールのデータを取得しています。

参照: 【自動化】PythonでOutlookのメールを読み込む (@konitech913 さん)

■説明
・「送信済みアイテム」のメールの件名を取得し、正規表現で自分の送った日報を指定。
・メールの受信日時、所感部分のテキスト、所感の文字数、件名を取得。
→日報の中でも所感部分が自由筆記(と言いつつ、業務内容の報告、結果、展望がメインですが)だったので、
仕事に対する自分の姿勢が表れると思い、所感を抽出することにしました。
・取得した各日の日報を1レコードとするDataFrameを作成し、CSVファイルとして保存。

■データ数を保つための工夫
Outlookのデフォルトの設定か分かりませんが、
今回、受信から1年以上経過した「送信済みアイテム」内のメールが見れなくなっていました。
そのため、(方法は調べ切れていませんが) Outlookにてメールの保存期間の設定を行うか、
初回実装時に取得したメールデータをCSVファイルとして保存しておくことをお勧めします。

'''Outlookからテキストデータの取得,データセットの作成'''
def createDataset():
    #Outlookオブジェクトの生成
    outlook = win32com.client.Dispatch("Outlook.Application").GetNamespace("MAPI")

    #アカウント情報の取得
    accounts = outlook.Folders

    #フォルダ情報の取得(日報の送受信に使用しているアカウントを指定)
    fldrs = accounts[1].Folders

    #メール情報の取得(fldrsの第3要素が「送信済みアイテム」)
    mails = fldrs[3].Items

    #件名に"【Team21日報】"と"山田太郎"を含むメールオブジェクトを辞書に格納
    mailList = []
    for mail in mails:
        #取得するメールを正規表現で抽出
        if  (mail.subject.startswith('【Team21日報】')) and ('山田太郎' in mail.subject):
            #受信日時の形式変換(yyyy/mm/dd_hh:mm:ss)
            time_str = mail.receivedtime.strftime('%Y/%m/%d_%H:%M:%S')
            #所感部分の取得
            body_str =re.findall('《所感》(.+?)以上、ご確認の程よろしくお願い致します。',''.join(mail.body.split()))[0]
            #件名の取得
            subject_str = mail.subject
            #リストに取得した値を格納(受信日時, 件名, 所感の文字数, 所感テキスト)     
            mailList.append([time_str, subject_str, len(body_str), body_str])
        else:
            pass
    
    #DataFrame内における、全角文字を加味したカラム名と値の位置調整(処理確認時の視認性向上)
    pd.set_option('display.unicode.east_asian_width', True)

    #DataFrame化
    df = pd.DataFrame(mailList, columns=['受信日時','件名','文字数', '所感'], index=None)

    #CSvファイルに保存
    fpath = r"*\test.csv"
    df.to_csv(fpath, encoding='shift-jis', index=None)

2.CSVファイルの読み込み

手順1で保存したCSVファイルを読み込み、後のデータ可視化に向けてDataFrameにカラムを追加していきます。

■説明
・分析の際に、日、月、曜日など、異なる単位でデータを分けるため、それぞれに相応するカラムを追加。
・受信日時でレコードをソートするために、受信日時をインデックスに指定。
・経過時間では、終業時間(18時)からの経過時間を時間単位で算出。

'''CSVファイルの読み込み'''
#引数: Outlookのメールデータを持つCSVファイルのフルパス
def readCSV(fpath):
    df =pd.read_csv(fpath, encoding='shift-jis', index_col=['受信日時'])

    #インデックスの年月のみ取得(pd.Series→pd.Dataframeに変換)
    #df['年月'] = df['受信日時'].apply(lambda x: x[0:7])
    df['年月'] = [ x[0:7] for x in df.index]
    df['年月日'] = [ x[0:10] for x in df.index]

    #受信日時カラムをto_datetimeで時系列データに変換し、インデックスに指定
    df.index = pd.to_datetime(df.index, format='%Y/%m/%d_%H:%M:%S')

    #受信日時の値について、昇順でソート
    df.sort_index(ascending=True, inplace=True)

    #曜日の取得
    df['曜日'] = df.index.day_name()

    #定時からの経過時間の取得(インデックスの18:00からの差分の算出)
    df['時間'] = df.index.values
    df['経過時間'] = df['時間'].apply(lambda x: float(x.hour - 18) + float(x.minute / 60))

    return df

3.所感のPN値の算出

「岩波国語辞書(岩波書店)」をリソースとする単語対応極性対応表(以下、極性辞書)を基に、
所感から抽出した単語に感情極性の実数値を割り当てていきます。

■説明
get_pn_dict()関数
・極性辞書から(単語:スコア)の辞書を作成。

'''極性辞書の読み込み'''
def get_pn_dict():
    dic_url = 'http://www.lr.pi.titech.ac.jp/~takamura/pubs/pn_ja.dic'
    pndf = pd.read_csv(dic_url, encoding='shift-jis', names=['word_type_score'])
    #単語とPN値を抽出して辞書型に変換(単語は':で'分割した際の第0要素、PN値は第3要素)
    pndf['splited'] = pndf['word_type_score'].str.split(':')
    pndf['word'] = pndf['splited'].str[0]
    pndf['score'] = pndf['splited'].str[3]
    keys = pndf['word'].to_list()
    values = pndf['score'].to_list()
    pn_dict = dict(zip(keys,values))
    return pn_dict

getDicList(text)関数
・MeCabを使って形態素解析を実施。
・String型のテキストを引数とし、原形、品詞のリストを戻り値とします。
→なお、今回は品詞を使った分析を行わなかったので、品詞の取得は任意でお願いします。

'''テキストを形態素解析し、結果を辞書型のリストで返す'''
def getDicList(text):
    #MeCabのインスタンス作成
    m = MeCab.Tagger('')
    #MeCabでテキストを形態素解析
    parsed = m.parse(text)
    #解析結果を1行(1語)ごとにリスト化(末尾の2要素は不要のため取得しない)
    lines = parsed.split('\n')[0:-2]
    #原形を取得するリスト, 品詞を取得するリスト
    diclist= []
    typelist = []
    for line in lines:
        #行ごとに取り出し、タブとカンマで区切られた語を取得してリスト化
        words = re.split('\t|,', line)
        #原型を取得しリストに格納
        d = {'BaseForm':words[7]}
        diclist.append(d)
        #品詞を取得しリストに格納
        t = {'type':words[1]}
        typelist.append(t)

    return diclist,typelist

add_pnvalue(diclist, pn_dict)関数
・getDicList関数で取得したテキストの原形リスト(diclist)と極性辞書(pn_dict)を引数とし、
原形にPN値を割り当てた辞書(word_pn_list)を戻り値とします。
・所感から取得した原形をkeyに、対応するスコア(PN値)を極性辞書から取得し、valueとしてword_pn_listに追加。
・所感から取り出した原形が極性辞書にない場合は、'notfound'を返します。

'''形態素解析結果の単語ごとのdictデータにPN値を追加'''
def add_pnvalue(diclist, pn_dict):
    #{'BaseForm’:基本形, 'PN値':該当するPN値}を要素とするリスト
    word_pn_list = []
    for word in diclist:
        #個々の辞書から基本形(value部分)を取得
        base = word['BaseForm']
        #ループ中で取得するPN値を持つ変数
        pn = 0
        if base in pn_dict:
            pn = float(pn_dict[base])
        #取得した単語が極性辞書にない場合、PN値に'notfound'を返す。
        else:
            pn = 'notfound'
        word['PN'] = pn
        word_pn_list.append(word)
    return word_pn_list

get_mean(dictlist)関数
・1レコード分の単語とそのPN値の辞書データを引数とする。
・PN値を取得し、平均値を算出。PN値がない場合は、0を返す。

PN_mean(df)関数
・手順2で作成したDataFrameを引数とする。
・get_mean関数を使ってレコードごとに、所感のPN値の平均値を算出し、新カラム「PN」に追加。

'''メールごとにPN平均値を算出'''
def get_mean(dictlist):
    pn_list = []
    for word in dictlist:
        pn = word['PN']
        #PN値を持つ場合のみ、pn_listに値を追加
        if pn!='notfound':
            pn_list.append(pn)
    #pn_listに値があれば、平均値を算出。なければ、0を返す。
    if len(pn_list)>0:
        pnmean = np.mean(pn_list)
    else:
        pnmean=0
    return pnmean


'''PN値をデータセットに適用'''
def PN_mean(df):

    #極性辞書の読み込み
    pn_dict = get_pn_dict()

    '''日単位でその日の所感のPN値の平均値を算出し、新カラムに格納'''
    # 空のリストに各日ごとのPN値の平均値を追加。
    means_list = []
    for text in df['所感']:
        dl_old, typelist = getDicList(text)
        dl_new = add_pnvalue(dl_old, pn_dict)
        pnmean = get_mean(dl_new)
        means_list.append(pnmean)
    #新規カラムPNに算出した各行のPN平均値を指定。
    df['PN'] = means_list
    
    return df

4.外れ値の処理

2種類の条件で外れ値の処理を行います。

■説明
removeOutlier1(df)関数
・以下の条件を外れ値の判断で用います。
条件A: 終業時間より後に受信されている = 経過時間が0より大きい(→半日勤務や早退のケースを除外)。
条件B: 曜日が土日でない(→日報の再送、提出遅れのケースを除外)。
→条件Aと条件Bの両方を満たすレコードを今回の分析対象として抽出します。

■補足
今回、平日に終日働いた場合における、
日報を書いた時間(経過時間)と所感の文字数やPN値の関係を見たかったので、上記のような条件を設定しました。
そのため、外れ値というよりは、「分析対象のレコードを選定する」という表現の方が正しいかもしれません。

'''外れ値の処理1(経過時間,曜日で外れ値を定義)'''
def removeOutlier1(df):
    #定時より前に提出された場合と土日に提出された場合を除外(午後OFF, 再送など)
    df_new = df.query('経過時間 >= 0 & not 曜日.str.contains("Saturday") & not 曜日.str.contains("Sunday")', engine='python')
    return df_new

removeOutlier2(series)関数
・四分位数で外れ値を定義します。
参照: 1次元データの外れ値の検出 (@papi_tokeiさん)

'''外れ値の処理2(四分位数で外れ値を定義)'''
#引数: データセット中の1カラムseries
def removeOutlier2(series):

    #第一四分位範囲
    q1 = series.quantile(0.25)
    #第3四分位範囲
    q3 = series.quantile(0.75)
    #四分位範囲
    iqr = q3 - q1
    #下限値lower: iqr(四分位範囲) - q1(第一四分位範囲)*1.5
    lower_q = iqr - q1*1.5
    #上限値: iqr(四分位範囲) - q1(第一四分位範囲)*1.5
    upper_q = iqr + q3*1.5
    #下限、上限の範囲内にあるレコードのみ取得
    series_iqr = series[(lower_q <= series) & (series <= upper_q)]

    return series_iqr

5-1.データ可視化1

ここから手順1~4で作成したDataFrameのデータを可視化していきます。
■グラフの概要

概要 説明
1 曜日ごとの文字数 観測値のばらつきを確認するため箱ひげ図(boxplot)を採用。
2 月ごとの文字数 こちらも同様に箱ひげ図を採用。
3 9月の文字数の推移 ある月のなかで所感の文字数がどう変化していったのかを確認するべく線グラフ(lineplot)を採用。
4 月ごとのPN値 図1,2と同様に箱ひげ図を採用。
5 経過時間と文字数の関係 相関係数を算出する前に、データの分布を確認するべく散布図(scatterplot)を採用。
6 経過時間とPN値の関係 こちらも同様に散布図を採用。

実際に出力したグラフは後程掲載しますので、ここではソースコードのみ紹介します。

'''データの可視化/分析1'''
def visualizeData1(df):

    #月ごとの所感平均文字数: line chart
    fig = plt.figure(constrained_layout=True)

    ax1 = fig.add_subplot(2,3,1)
    ax1= sns.boxplot(x='曜日', y='文字数',data=df, showmeans=True)
    ax1.set_xlabel('曜日')
    ax1.set_ylabel('文字数')
    ax1.set_title('図1: 曜日ごとの文字数のばらつき')
    plt.setp(ax1.get_xticklabels(), rotation=30, ha="right")

    #箱ひげ図のプロット(seaborn)
    ax2 = fig.add_subplot(2,3,2)
    ax2= sns.boxplot(x='年月', y='文字数',data=df, showmeans=True, color='.75')
    ax2.set_xlabel('年月')
    ax2.set_ylabel('文字数')
    ax2.set_title('図2: 月ごとの文字数のばらつき')
    plt.setp(ax2.get_xticklabels(), rotation=30, ha="right")

    #9月の文字数の遷移
    ax3 = fig.add_subplot(2,3,3)
    ax3 = sns.lineplot(x='年月日', y='文字数', data=df.query('年月.str.contains("2021/09")', engine='python'))
    ax3.set_xlabel('日時')
    ax3.set_ylabel('文字数')
    ax3.set_title('図3: 文字数の推移')
    plt.setp(ax3.get_xticklabels(), rotation=30, ha="right")

    #年間のPN値の遷移
    ax4 = fig.add_subplot(2,3,4)
    ax4 = sns.boxplot(x='年月', y='PN',data=df, showmeans=True, color='.75')
    ax4.set_xlabel('年月')
    ax4.set_ylabel('PN値')
    ax4.set_title('図4: 月ごとのPN値のばらつき')
    plt.setp(ax4.get_xticklabels(), rotation=30, ha="right")

    #文字数と経過時間の関係
    ax5 = fig.add_subplot(2,3,5)
    #比較したい2種のデータのみのデータセットを作成
    ax5 = sns.scatterplot(data=df, x='経過時間', y='文字数', hue='曜日')
    ax5.set_title('図5: 終業時間からの経過時間と文字数の分布')
    
    #経過時間とPN値の関係
    ax6 = fig.add_subplot(2,3,6)
    ax6 = sns.scatterplot(data=df, x='経過時間', y='PN', hue='曜日')
    ax6.set_title('図6: 終業時間からの経過時間とPN値の分布')
    plt.show()

5-2.データ可視化(深堀り)

ここでは、データ可視化1で気になった「日報の提出時間(終業時間からの経過時間)」と
「所感の文字数、所感のネガポジスコア(PN値)」の相関関係を見ていきます。
■説明
dfmaker(df,colName1, colName2)関数
・引数: 手順1~4までで用意したDataFrame(df)、経過時間と共に相関係数を算出する対象カラム名(colNmame1)、時系列単位カラム名(colName2)
・戻り値: 相関係数とp値を持つDataFrame(df_corr)
・曜日別、また月別に上記の相関係数、そのp値を算出し、可視化しています。
・相関係数、p値は、scipy.statsのpeasonrを用いて算出しています。
参照: 【Python入門】pandasとscipyで相関係数とp値を算出し統計分析する方法【実践】 (ログのわ。)

'''データ可視化/分析2(曜日ごと、月ごとに可視化1の図5、図6の相関係数, P値を算出し、ヒートマップを作成'''
def dfmaker(df,colName1, colName2):

    #DataFrameの生成
    dfObj = df.reset_index()[['経過時間', colName1, colName2]
    #相関係数出力対象カラムのユニーク値を取得
    objList = dfObj[colName2].unique()    
    
    #曜日ごとに相関係数、p値を算出
    corrList = []

    for dname in objList:
        #曜日ごとにdfを生成
        df_day = dfObj[dfObj[colName2] ==dname]
        #x:経過時間, y:文字数
        x = df_day['経過時間']
        y = df_day[colName1]
        #a:相関係数, b:p値(ValueErrorが出たとき)
        try:
            a,b = pearsonr(np.ravel(x), np.ravel(y))
        except  ValueError as e:
            #今回、ValueErrorが発生し、技量不足で原因が特定できず一旦try-exceptで回避させていただきました。
            print(e)
            continue
        #リストに曜日, 相関係数, p値(小数第4位を四捨五入)
        corrList.append([dname, format(a, '3f'), format(b, '3f')])
        
  
    #相関係数, p値の算出結果をDataFrame化
    df_corr = pd.DataFrame(corrList, columns=[colName2, '相関係数', 'p値'])
    #データ型の変換(object→float)
    df_corr['相関係数'] = df_corr['相関係数'].astype('float')
    df_corr['p値'] = df_corr['p値'].astype('float')
    print(df_corr.info())
    print(df_corr.head())

    return df_corr

visualizeData2(df)関数
・dfmaker関数を使って、「経過時間と文字数」、「経過時間とPN値」の相関係数とp値を
曜日ごと、年月ごとに算出し、 各パターンをDataFrameに変換。
・曜日、年月の単位をループ処理で切り替え、各DataFramをグラフ化。
・相関係数の統計的有意性を見るべく、p値の基準線 p=0.05 を描画。

def visualizeData2(df):

    #経過時間との相関係数を算出する対象カラム名リスト
    colNameList = ['文字数','PN']

    for cname in colNameList:
        #DataFrameの生成
        df_corr_dayName = dfmaker(df,cname,'曜日')
        df_corr_ym = dfmaker(df,cname,'年月')
            
        #以下、データ可視化
        fig = plt.figure(constrained_layout=True)
        
        #経過時間と文字数の相関係数の可視化(曜日単位)
        ax1 = fig.add_subplot(2,2,1)
        ax1 = sns.barplot(x='曜日', y='相関係数', data=df_corr_dayName)
        ax1.set_title('図7: 経過時間と{}の相関係数(曜日単位)'.format(cname))

        #相関係数の可視化(年月単位)
        ax2 = fig.add_subplot(2,2,2)
        ax2 = sns.barplot(x='年月', y='相関係数', data=df_corr_ym)
        ax2.set_title('図8: 経過時間と{}の相関係数(年月単位)'.format(cname))

        #相関係数のp値の可視化(曜日単位)
        ax3 = fig.add_subplot(2,2,3)
        ax3 = sns.barplot(x='曜日', y='p値', data=df_corr_dayName)
        ax3.axhline(y=0.1, linestyle='--', color='k')
        ax3.set_title('図9: 経過時間と{}の相関係数のp値(曜日単位)'.format(cname))

        #相関係数のp値の可視化(年月単位)
        ax4 = fig.add_subplot(2,2,4)
        ax4 = sns.barplot(x='年月', y='p値', data=df_corr_ym)
        ax4.axhline(y=0.1, linestyle='--', color='k')
        ax4.set_title('図10: 経過時間と{}の相関係数のp値(年月単位)'.format(cname))

        plt.show()

実行

手順1~5を実行します。

'''実行'''
#1.データの取得、データセットの作成(初回のみ実行)
#createDataset()

#2.CSVファイルの読み込み
fpath = fpath = r"*\test.csv"
df = readCSV(fpath)

#3.データセットに各レコードの所感のPN値の平均値カラム(PN)を追加
df = PN_mean(df)

#4.外れ値の除外1
df = removeOutlier1(df)
#外れ値の除外2
df['文字数'] = removeOutlier2(df['文字数'])

#5-1.データの可視化1
visualizeData1(df)

#5-2.データの可視化2
visualizeData2(df)

6-1. 出力結果

【文字数に外れ値の処理を適用する前後での差分比較】
・「外れ値の除外2」をコメントアウトして比較。
・経過時間0~3時間, 文字数400~500の間に位置する2つのレコードが除外され、
文字数、経過時間の目盛りの範囲が縮小し、データのばらつきが若干軽減されるのが分かります。

【適用前】
image.png

【適用後】
image.png

【5-1.データ可視化1の出力例】
■図1~6からわかること(さっと)
・曜日ごと、また月ごとに所感部分の文字数のばらつき度合いは異なりました。
・所感部分には業務の報告や振り返り、展望を端的に記載していたつもりでしたが、
その中でもPN値が上下することがありました。

■補足
・2021年9月は個人的に業務のモチベーションが大きく変動した月であり、
日報の文字数も変動しているかと思い、可視化の対象として取り上げました。
日報データ_可視化_曜日.png

【5-2.データ可視化2の出力例】

  1. 経過時間とPN値の相関係数(曜日、年月単位)
    日報データ_経過時間とPN値の相関係数とp値.png
    2.経過時間と文字数の相関係数(曜日、年月単位)
    日報データ_経過時間と文字数の相関係数とp値.png
    ■図7~10(単位: 曜日, 月)からわかること(さっと)
    ・曜日の相関係数、p値を算出する際にValueErrorが発生し、エラーなく処理を完了したMonday, Wednesday, Thursdayのみが出力。
    ・今回取得したデータから算出した相関係数はどれもp値が0.05(破線部分)を上回り、
     統計的に有意であるとは言えず。
    参照: p値と有意水準 (統計WEB)
    ・ただ、図9のWednesdayにおいては、相関係数が-0.3を下回り、p値も0.1を切り、
     比較的0.05に近くなりました。

下表: 相関係数と相関の強さの目安

相関係数の絶対値 相関の強さの目安
0.7~ 強い相関
0.4~0.7 中程度の相関
0.2~0.4 弱相関
~0.2 ほとんど無相関

参照: P39-43 2-6相関分析・回帰分析 (新潟県)

6-2.簡単な考察

「日報の提出時間が遅くなるにつれて、所感部分の文字数が減り、またPN値も下がるのでは」
と期待して、今回のネガポジ判定、相関係数の出力を行いました。
しかし、「データ可視化1」からもわかる通り、p値が基準値0.05を下回る相関係数は見られず、
有意な結果は得られませんでした。
逆に、所感部分は基本的にネガティブな表現がメインであることや、
終業時間からの経過時間について文字数やPN値とはあまり有意な関係がないことが分かりました。
その背景として、業務内容や労働時間(外的要因)が月日が経つごとに変化し、
また「何に対して成長したか」という自分のアクションに対するフィードバックの観点が時間と共に変わっていったこと(内的要因)が
影響しているのかと(明確には証明できませんが、、)。
他方、「有意そう」な結果で言うと、Wednesdayにおける経過時間と文字数の相関関係(coef>-0.3, 0.5<p<0.1)については、
もう少しサンプル数が多ければより高いp値が得られたかもしれません。

■まとめ

ということで、Outlookから取得したメールデータに対して極性辞書を用いたネガポジ判定を行い、
普段の生活で感じた仮説を検証するという事実発見的な分析を行ってみました。

実際、グラフ出力時に出力した全ての相関係数の統計的有意性がないと分かった時点でそもそも記事にするか迷いましたが、
今後、自分と似たような疑問を持った方がデータ分析を行う際に少しでも役に立てればと思い、
拙い文章ではございますが、ここに残させていただきます。
なお上記のコードを利用されたい場合は、途中ValueErrorが生じる箇所(5-2.データ可視化)があったので、
その点ご注意ください。
他に修正、追加依頼がございましたら、極力対応させていただきますので是非ともコメントお願い致します。

以上になります。最後までご覧いただきありがとうございました。

8
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?