景況感をニュースの感情分析で雑に分析してみた

# 概要
COTOHA APIにある感情分析APIを利用して最新経済ニュースのテンションを読み取って雑に景況感を出してみました。

感情分析API
https://api.ce-cotoha.com/contents/reference/apireference.html#sentiment
> 感情分析APIは、入力として日本語で記述されたテキストを受け取り、そのテキストの書き手の感情(ネガティブ・ポジティブ)を判定します。また、テキスト中に含まれる「喜ぶ」「驚く」「不安」「安心」といった15種類の感情を分類・認識して出力します。

最近触り始めたのでNimをつかって実装します。

# ニュースのソース
NewsAPIがお手軽にニュースの概要を取得できるのでこちらを分析につかってみます。
https://newsapi.org/
こちらは会員登録してAPI Keyを取得すればGETリクエストだけで簡単に以下のようにニュースのタイトルや概要文を取得できます。

```json
{
  "status": "ok",
  "totalResults": 70,
  "articles": [
    {
      "source": {
        "id": null,
        "name": "Yahoo.co.jp"
      },
      "author": "デイリースポーツ",
      "title": "ヒカキン、必死の訴え トイレットペーパー１０００ロール購入は「４年前」 デマに「超悲しい」（デイリースポーツ） - Yahoo!ニュース",
      "description": "人気ユーチューバーのヒカキンが２日、自身のＹｏｕＴｕｂｅチャンネルで、一部でトイレットペーパーの買い占めの“元凶”などとされていることに、「誤解されている」と必死に訴えた。ヒカキンは１６年の動画で２",
      "url": "https://headlines.yahoo.co.jp/hl?a=20200304-00000035-dal-ent",
      "urlToImage": "https://lpt.c.yimg.jp/amd/20200304-00000035-dal-000-view.jpg",
      "publishedAt": "2020-03-04T01:46:22Z",
      "content": null
    }
  ]
}
```

## ニュースの取得の実装
httpclientをつかってjsonを取得するだけです。

```nim
import asyncdispatch, httpclient, json, strformat, times

const newsAPIUrl = "http://newsapi.org/v2/top-headlines"
const newsAPIKey = "[**newsAPIKey**]"

proc asyncFetchTopNews(): Future[JsonNode] {.async.} =
  try:
    let client = newAsyncHttpClient()
    let yeasterday = (datetime - 1.days).format("yyyy-MM-dd")
    let content = await client.getContent(&"{newsAPIUrl}?country=jp&category=business&sortBy=popularity&from={today}&to={today}&pageSize=100&apiKey={newsAPIKey}")
    result = parseJson(content)["articles"]
  except:
    echo "Failed: " & getCurrentExceptionMsg()
```

# 感情API
COTOHA APIの感情分析APIを利用すると以下のようなjsonが得られます。
`sentiment`が`Positive` `Negative` `Neutral`のどれかを返すのでニュースのタイトルと説明文を送ってこれをもってニュースの感情を検証していきたいと思います。
また`score`が`sentiment`の度合いになるのでこれを合算してみます。

```json
{
  "result":
    {
    "sentiment":"Positive",
    "score":0.20766788536018937,
    "emotional_phrase":[
      {
        "form":"謳歌",
        "emotion":"喜ぶ,安心"
      }
    ]
  },
  "status":0,
  "message":"OK"
}
```

# 感情分析APIリクエストの実装
ニュースの数の分だけAPIリクエストするのでasyncリクエストを並行して発行しています。

```nim
import asyncdispatch, httpclient, json, sequtils, strformat
const cotohaBaseUrl = "https://api.ce-cotoha.com/api/dev"
const cotohaAuthUrl = "https://api.ce-cotoha.com/v1/oauth/accesstokens"
const clientId = "[**clientId**]"
const clientSecret = "[**clientSecret**]"


proc fetchAccessToken(): Future[string] {.async.} =
  try:
    let client = newAsyncHttpClient()
    let headers = newHttpHeaders()
    headers["Content-Type"] = "application/json"
    let body = %*{
      "grantType": "client_credentials",
      "clientId": clientId,
      "clientSecret": clientSecret,
    }
    let response = await client.request(
      url = cotohaAuthUrl,
      headers = headers,
      httpMethod = HttpPost,
      body = $body
    )
    let content = await response.body
    result = parseJson(content)["access_token"].getStr()
  except:
    echo "Failed: " & getCurrentExceptionMsg()

proc asyncAnalizeSentiment(sentence: string, accessToken: string): Future[JsonNode] {.async.} =
  try:
    let client = newAsyncHttpClient()
    let headers = newHttpHeaders()
    headers["Content-Type"] = "application/json;charset=UTF-8"
    headers["Authorization"] = "Bearer " & accessToken
    let body = %*{
      "sentence": sentence,
      "type": "default"
    }
    let response = await client.request(
      url = cotohaBaseUrl & "/nlp/v1/sentiment",
      headers = headers,
      httpMethod = HttpPost,
      body = $body
    )
    result = parseJson(await response.body)["result"]
  except:
    echo "Failed: " & getCurrentExceptionMsg()

proc main() {.async.} =
  var 
    positiveCount, neutralCount, negativeCount = 0
  var 
    positiveScore, neutralScore, negativeScore : float = 0.0
  let accessToken = await fetchAccessToken()
  let articles = await asyncFetchTopNews()
  let responses = await all(articles.mapIt((it["title"].getStr() & "\n" & it["description"].getStr()).asyncAnalizeSentiment(access_token)))
  try:
    for res in responses:
      if not res.hasKey("sentiment") :
        continue
      let sentiment = res["sentiment"].getStr()
      case sentiment
      of "Positive":
        positiveCount += 1
        positiveScore += res["score"].getFloat()
      of "Neutral":
        neutralCount += 1
        neutralScore += res["score"].getFloat()
      of "Negative":
        negativeCount += 1
        negativeScore += res["score"].getFloat()
  except:
    echo "Failed: " & getCurrentExceptionMsg()

  echo &"Positive: {$positiveCount} ({$positiveScore})"
  echo &"Neutral: {$neutralCount} ({$neutralScore})"
  echo &"Negative: {$negativeCount} ({$negativeScore})"

waitFor main()

```

## 結果
2019-03-03のニュースで分析してみました。
新型コロナの影響でしょうか、かなりNegativeなものが多かったです。ニュースの文言なのでNeutralが多くなってしまうのは仕方ないのでしょうか。

```terminal
$ nim c -r -d:ssl news.nim
Positive: 9 (2.243333515781775)
Neutral: 33 (10.93592138261002)
Negative: 19 (7.034453934207068)
```

# まとめ
リクエストを同時に投げているためか、いくつかのリクエストが正常に返ってこないきてない問題が発生したりしました。すこしまってリクエストする工夫が必要かもしれないです。
News APIのtitle / descriptionだけなのでニュース全体の分析ができていないのが心残りですが、APIリクエストするだけで日本語の分析が簡単にできるのはCOTOHAすごいです。

あと、Nimは書きやすくてよいです！