Twitterの過去ツイートからベストポジティブツイートを見つけよう(golang)

2021-05-14

Twitterの過去ツイートからベストポジティブツイートを見つけよう

ゾンビランドサガを見始めました。 これ、アイドルアニメだったんですね。最高ですわ。昭和アイドルと平成のアイドルが考え方の違いをぶつける話とか熱かったですね。

何をしたのか

Twitterにはアニメを見た感想とか、一時の感情にがツラツラとつぶやかれています。 自分を振り返るため、このツイート全文取得を行いの文書から感情分析をやっていきます。 結果として、個人のツイートデータとしては比較的大容量データとなる10万ツイートの中からベストネガポジツイートを取得するのが今回の趣旨になります。

前回記事JumanppをGolangから扱いやすくするラッパライブラリを書いた話のライブラリを拡張しました。

本来感情推定を行う場合に取られる手法/今回実施する手法

形態素解析した結果と、単語感情極性対応表に当てはめてポジティブ、ネガティブの度合いを出しています。単語の意味だけで判断してます。 例えば、二重否定の言葉も「したくないことも無い」ネガティブに判定されてしまします。 ※上の分だと、したくないよりのニュアンスも有るから普通にネガティブかもしれないですが…

本来であれば文章の意味を把握して、ポジティブ・ネガティブを判定する必要がありますが今回は単語に応じてスコアを決めていく方式のためそこまでの精度は出ないです。 精度を求めるのであれば、IBM等がAI解析サービスやっていますのでそちらを利用すると良いでしょう。

動作環境の準備

動作させるためには下記ライブラリとファイルが必要です。

パッケージインストール

go get github.com/cheggaaa/pb/v3
go get github.com/kenpos/JumanppGo
go get github.com/rocketlaunchr/dataframe-go

github.com/cheggaaa/pb/v3

github.com/kenpos/JumanppGo

github.com/rocketlaunchr/dataframe-go

単語感情極性対応表

単語感情極性対応表

東工大の高村教授が公開しています。

-1~+1の範囲でネガティブとポジティブが割り当てられています。

「ここからダウンロードしてください」の日本語を開き、pn_ja.dic取得してください。 取得したpn_jp.dicを使いたいコードと同じパスにおけばとりあえず動きます。

Jumanppのビルド

過去記事を参考にビルドしてもらえると助かります。 このビルドしたjumanppと辞書データがあるフォルダに環境パスを通しておいてください。 Windows10にJUMAN++をインストールし、Golangから実行結果を受け取る方法

Twitterの全文解析

今回の記事の本題に入ります。 実装コードを記載します。今回も目的達成のためにとりあえず動くものを用意してます。 きっともっと良い方法があるので修正案お待ちしております。

全文コード

package main

import (
    "bufio"
    "bytes"
    "context"
    "fmt"
    "os"
    "strconv"
    "strings"
    "time"

    "github.com/cheggaaa/pb/v3"
    JumanppGo "github.com/kenpos/JumanppGo"
    "github.com/rocketlaunchr/dataframe-go"
    "github.com/rocketlaunchr/dataframe-go/exports"
)

type Tweet struct {
    retweeted          bool
    source             string
    entities           Entities
    urls               Urls
    display_text_range string
    favorite_count     string
    id_str             string
    truncated          bool
    retweet_count      string
    id                 string
    created_at         string
    favorited          bool
    full_text          string
    lang               string
}

type Entities struct {
    hashtags      string
    symbols       string
    user_mentions Usermentions
}

type Usermentions struct {
    name        string
    screen_name string
    indices     []int
    id_str      int
    id          int
}

type Urls struct {
    url string
}

func timeToString(t time.Time) string {
    str := t.Format("2006-01-02 15:04:05")
    return str
}

func stringToTime(tweetTime string) string {
    tweets := strings.TrimSpace(tweetTime)
    tweets = strings.Replace(tweets, "\"", "", -1)
    tweets = strings.Replace(tweets, ",", "", -1)
    layout := "Mon Jan 2 15:04:05 +0000 2006"
    t, _ := time.Parse(layout, tweets)

    return timeToString(t)
}

func convertTweet(tweetString string, tweet *Tweet) bool {
    tweets := strings.TrimSpace(tweetString)
    arrtweet := strings.Split(tweets, ":")

    arrtweet[0] = strings.Replace(arrtweet[0], "\"", "", -1)
    arrtweet[0] = strings.Replace(arrtweet[0], "\n", "", -1)
    arrtweet[0] = strings.TrimSpace(arrtweet[0])

    if len(arrtweet) > 1 {
        switch string(arrtweet[0]) {
        case "tweet":
            // fmt.Println("Tweet OK")
            return true
        case "retweeted":
            tweet.retweeted = false

        case "source":
            tweet.source = arrtweet[1]

        case "hashtags":
            tweet.entities.hashtags = arrtweet[1]

        case "symbols":
            tweet.entities.symbols = arrtweet[1]

        // case "user_mentions":

        case "urls":
            tweet.urls.url = arrtweet[1]

        case "display_text_range":
            tweet.display_text_range = arrtweet[1]

        case "favorite_count":
            tweet.favorite_count = arrtweet[1]

        case "id_str":
            tweet.id_str = arrtweet[1]

        case "truncated":
            tweet.truncated = false

        case "retweet_count":
            tweet.retweet_count = arrtweet[1]

        case "id":
            tweet.id = arrtweet[1]

        case "created_at":
            strtime := stringToTime(arrtweet[1] + ":" + arrtweet[2] + ":" + arrtweet[3])
            tweet.created_at = strtime

        case "favorited":
            tweet.favorited = false

        case "full_text":
            arrtweet[1] = strings.Replace(arrtweet[1], "\",", "", -1)
            arrtweet[1] = strings.Replace(arrtweet[1], "\"", "", 1)
            tweet.full_text = arrtweet[1]

        case "lang":
            tweet.lang = arrtweet[1]

        default:
            // fmt.Println("Nonm")
        }
    }
    return false
}

type Emotiontweet struct {
    EmoDateTime   string
    EmoTweet      string
    EmoEmoAverage float64
    EmoEmoSum     float64
}

func main() {
    data, _ := os.Open("./tweet.js")
    defer data.Close()
    scanner := bufio.NewScanner(data)

    var alltweet []Tweet
    var tweet Tweet
    for scanner.Scan() {
        flag := convertTweet(scanner.Text(), &tweet)
        if flag == true {
            alltweet = append(alltweet, tweet)
            tweet = Tweet{}
        }
    }

    count := len(alltweet)
    bar := pb.Simple.Start(count)
    bar.SetMaxWidth(120)

    s1 := dataframe.NewSeriesString("create", nil)
    s2 := dataframe.NewSeriesString("tweet", nil)
    s3 := dataframe.NewSeriesString("sum", nil)
    s4 := dataframe.NewSeriesString("average", nil)
    df := dataframe.NewDataFrame(s1, s2, s3, s4)

    for _, v := range alltweet {
        bar.Increment()
        dics := JumanppGo.JumanDic(v.full_text)
        eval, sum := JumanppGo.AverageVolume(dics)
        if v.full_text == "" {
            continue
        }
        df.Append(nil, v.created_at, v.full_text, strconv.FormatFloat(eval, 'f', 10, 64), strconv.FormatFloat(sum, 'f', 10, 64))
    }

    fmt.Println(df)

    // 空のContextを作成
    ctx := context.Background()
    sks := []dataframe.SortKey{
        {Key: "sum", Desc: true},
    }
    df.Sort(ctx, sks)

    var buf bytes.Buffer
    exports.ExportToCSV(ctx, &buf, df)
    fmt.Println("---------------------------------------------------")
    fmt.Println("Sum")
    fmt.Println(buf.String())

    sks = []dataframe.SortKey{
        {Key: "average", Desc: true},
    }
    df.Sort(ctx, sks)

    fmt.Println("---------------------------------------------------")
    fmt.Println("Ave")
    exports.ExportToCSV(ctx, &buf, df)
    fmt.Println(buf.String())

}

ツイートデータの取得

tweet.jsに含まれているツイートデータを一行ずつスキャニングして ツイート用構造体に詰め込んでいます。 Tweet.jsはJson形式に近いのだからという意見も有りますが、tweet.jsに手を加える必要があります。それが嫌なのでこの方式になりました。

  • 冒頭の変数名を削除
  • JSON形式として読み込めない「,」などの修正
  • ツイート数が多いとゴミデータの処理。
    ※GolangでサポートされているJson形式として読み込む際にエラーがでます。
data, _ := os.Open("./tweet.js")
defer data.Close()
scanner := bufio.NewScanner(data)

var alltweet []Tweet
var tweet Tweet
for scanner.Scan() {
    flag := convertTweet(scanner.Text(), &tweet)
    if flag == true {
        alltweet = append(alltweet, tweet)
        tweet = Tweet{}
    }
}

なお詰め込む部分のコードはこうなります。 見て分かる通り、:で分割して[0]番目の配列を取り出します。 取り出したtweet.jsの各行から無駄な「"」などを削除しそれぞれの情報を構造体に詰め込んでいます。

func convertTweet(tweetString string, tweet *Tweet) bool {
    tweets := strings.TrimSpace(tweetString)
    arrtweet := strings.Split(tweets, ":")

    arrtweet[0] = strings.Replace(arrtweet[0], "\"", "", -1)
    arrtweet[0] = strings.Replace(arrtweet[0], "\n", "", -1)
    arrtweet[0] = strings.TrimSpace(arrtweet[0])

    if len(arrtweet) > 1 {
        switch string(arrtweet[0]) {
        case "tweet":
            // fmt.Println("Tweet OK")
            return true
        case "retweeted":
            tweet.retweeted = false

        case "source":
            tweet.source = arrtweet[1]

        case "hashtags":
            tweet.entities.hashtags = arrtweet[1]

        case "symbols":
            tweet.entities.symbols = arrtweet[1]

        // case "user_mentions":

        case "urls":
            tweet.urls.url = arrtweet[1]

        case "display_text_range":
            tweet.display_text_range = arrtweet[1]

        case "favorite_count":
            tweet.favorite_count = arrtweet[1]

        case "id_str":
            tweet.id_str = arrtweet[1]

        case "truncated":
            tweet.truncated = false

        case "retweet_count":
            tweet.retweet_count = arrtweet[1]

        case "id":
            tweet.id = arrtweet[1]

        case "created_at":
            strtime := stringToTime(arrtweet[1] + ":" + arrtweet[2] + ":" + arrtweet[3])
            tweet.created_at = strtime

        case "favorited":
            tweet.favorited = false

        case "full_text":
            arrtweet[1] = strings.Replace(arrtweet[1], "\",", "", -1)
            arrtweet[1] = strings.Replace(arrtweet[1], "\"", "", 1)
            tweet.full_text = arrtweet[1]

        case "lang":
            tweet.lang = arrtweet[1]

        default:
            // fmt.Println("Nonm")
        }
    }
    return false
}

ツイートデータの形態素分析と、感情推定

このfor文で全ツイートに対し、形態素解析と感情推定スコアを取得しています。 最後、データフレームに必要な情報を詰め込む処理を行っています。

for _, v := range alltweet {
    bar.Increment()
    dics := JumanppGo.JumanDic(v.full_text)
    eval, sum := JumanppGo.AverageVolume(dics)
    if v.full_text == "" {
        continue
    }
    df.Append(nil, v.created_at, v.full_text, strconv.FormatFloat(eval, 'f', 10, 64), strconv.FormatFloat(sum, 'f', 10, 64))
}

ソーティング処理

ツイートデータをスコア順に並び替えて、CSVとして出力します。

ctx := context.Background()
sks := []dataframe.SortKey{
    {Key: "sum", Desc: true},
}
df.Sort(ctx, sks)

var buf bytes.Buffer
exports.ExportToCSV(ctx, &buf, df)
fmt.Println("---------------------------------------------------")
fmt.Println("Sum")
fmt.Println(buf.String())

sks = []dataframe.SortKey{
    {Key: "average", Desc: true},
}
df.Sort(ctx, sks)

fmt.Println("---------------------------------------------------")
fmt.Println("Ave")
exports.ExportToCSV(ctx, &buf, df)
fmt.Println(buf.String())

実行結果

dataframe-goを用いて、全ツイートをデータフレームに詰め込みます。 詰め込むとまずは、下記のようなテーブルが取得されます。

ベストポジティブツイートを一覧で表示します。 可愛いとかしか言ってないのですね。この手法だとこういう繰り返しポジティブワードを述べるのが強いようです。

ネガティブ側のツイートも出します。 ワイは基本的にはポジティブだったらしいですね。 社会人になってから転職を決意する時期のツイートが抜けているのも要因にはありそうですが… いいですねワイ。

気になったツイートです。

日時:2016-06-12 06:04:36
感情スコア0.0006263889,-0.0112750000
「 ソルティライチの原液はまさに神の飲み物.カルピス原液と同等かそれ以上の価値がある.炭酸水で割っても美味しい,お酒と割っても美味しい.ヨーグルトに混ぜても美味しい.かき氷にかけても煌く.夏の新定番.ソルティライチ原液.最高",- 」

まとめ

こうやってツイートを読み込みそれぞれのスコアに感情値を当てはめソートすることで 何となくつぶやいたときの感情と合わせて見返すことができるので楽しいですね。

社会人になって強く感じるのは学生の頃は元気だった。 ツイート見てても楽しそうだなと思いますね。 今も十分楽しいのでなんともですが、自由な時間ってのは随分減りましたね。 学生のみんなは今がどれだけ貴重な時間なのかよく考えて、今しかできないことをやろうな。 会社員を演る人は仕事を辞めたときか 老後までそんな時間が来ないと肝に銘じておくんやで