【Qiita x COTOHA APIプレゼント企画】の記事から Happy Comment賞 を勝手に決定する

**[【Qiita x COTOHA APIプレゼント企画】](https://zine.qiita.com/event/collaboration-cotoha-api/)** が昨日終了しました。

本キャンペーンでは

**<font color="red">C</font>ool!賞**
**<font color="red">O</font>moshiro Idea賞**
**<font color="red">T</font>echnology賞**
**f<font color="red">O</font>r Beginner賞**

が決定されますが、今回は、**[COTOHA API の 感情分析API](https://api.ce-cotoha.com/contents/reference/apireference.html#sentiment)** で、キャンペーン対象のコメントを分析し、

**<font color="red">HA</font>ppy Comment賞** を勝手に決定します。

# **<font color="red">HA</font>ppy Comment賞**

記事についた、執筆者以外のコメントを **[感情分析API](https://api.ce-cotoha.com/contents/reference/apireference.html#sentiment)** で評価し、 **感情分析の結果(sentiment) が Positive のコメントの、センチメントスコア(score) の総計** で評価します。

<details><summary>集計スクリプト</summary><div>

``` javascript
// for COTOHA API
const COTOHA_DEVELOPER_API_BASE_URL   = "https://api.ce-cotoha.com/api/dev/";
const COTOHA_ACCESS_TOKEN_PUBLISH_URL = "https://api.ce-cotoha.com/v1/oauth/accesstokens";
const CLIENT_ID     = "ココニアイデイカク";
const CLIENT_SECRET = "ココニシークレットカク";

// for QIITA_API
const QIITA_AUTHORIZATION = 'Bearer 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcd' // token

const request = require('request');
const fs = require("fs");

const sleep = delay  => new Promise(resolve => setTimeout(resolve, delay));

const getItems = page => {
  return new Promise((resolve, reject) => {
    request(
      {
        url: `https://qiita.com/api/v2/items?page=${page}&per_page=100&query=created:>2020-02-06 created:<2020-03-15 tag:COTOHA`,
        headers: { Authorization: QIITA_AUTHORIZATION },
      },
      (error, response, body) => {
        if (error) {
          console.error('ERROR!', error);
          reject(error);
        } else {
          let res = JSON.parse(body);
          if (!Array.isArray(res)) {
            console.log(`INVALID!!! response is not array!!! ${JSON.stringify(res)}`);
            reject(res);
          } else {
            resolve(res.map( r => {
              return {
                id: r.id,
                title: r.title,
                user_id: r.user.id,
                likes_count: r.likes_count,
              }
            }));
          }
        }
      }
    );
  });
}

const getComments = item_id => {
  return new Promise((resolve, reject) => {
    request(
      {
        url: `https://qiita.com/api/v2/items/${item_id}/comments?page=1&per_page=100`, // 100件以上のコメントはない想定
        headers: { Authorization: QIITA_AUTHORIZATION },
      },
      (error, response, body) => {
        if (error) {
          console.error('ERROR!', error);
          reject(error);
        } else {
          let res = JSON.parse(body);
          if (!Array.isArray(res)) {
            console.log(`INVALID!!! response is not array!!! ${JSON.stringify(res)}`);
            reject(res);
          } else {
            resolve(res.map( r => {
              return {
                body: r.body,
                user_id: r.user.id,
              }
            }));
          }
        }
      }
    );
  });
}

const getAccessToken = () => {
  return new Promise((resolve, reject) => {
    request(
      {
        url: COTOHA_ACCESS_TOKEN_PUBLISH_URL,
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        json: {
          grantType: "client_credentials",
          clientId: COTOHA_CLIENT_ID,
          clientSecret: COTOHA_CLIENT_SECRET,
        },
      },
      (error, response, body) => {
        if (!error && (response.statusCode === 200 || response.statusCode === 201)) {
          if (typeof body !== 'object') body = JSON.parse(body);
          resolve(body.access_token);
        } else {
          if (error) {
            console.log(`request fail. error: ${error}`);
          } else {
            console.log(`request fail. response.statusCode: ${response.statusCode}, ${body}`);
          }
          reject(body);
        }
      },
    );
  });
}

const getSentiment = (accessToken, sentence) => {
  return new Promise((resolve, reject) => {
    request(
      {
        url: `${COTOHA_DEVELOPER_API_BASE_URL}nlp/v1/sentiment`,
        method: 'POST',
        headers: { 'Content-Type': 'application/json;charset=UTF-8', Authorization: `Bearer ${accessToken}`},
        json: { sentence: sentence },
      },
      (error, response, body) => {
        if (!error && (response.statusCode === 200 || response.statusCode === 201)) {
          if (typeof body !== 'object') body = JSON.parse(body);
          if (body.status === 0) {
            resolve({
              sentiment: body.result.sentiment,
              score: body.result.score,
              emotional_phrase: body.result.emotional_phrase,
            });
          } else {
            console.log(sentence);
            console.log(`request fail. error: ${body.message}`);
            reject(body);
          }
        } else {
          if (error) {
            console.log(sentence);
            console.log(`request fail. error: ${error}`);
          } else {
            msg = (typeof body !== 'object') ? body : JSON.stringify(body);
            console.log(sentence);
            console.log(`request fail. response.statusCode: ${response.statusCode}, ${msg}`);
          }
          reject(body);
        }
      }
    );
  });
}


(async () => {
  let items = [];
  for (let i = 1; i <= 3; i++) { // 記事数は138想定だが、念のため3回を上限としてループする
    let tmpItems = await getItems(i);
    if (tmpItems.length === 0) {
      break;
    } else {
      items = items.concat(tmpItems);
      sleep(1000)
    }
  }
  for (let item of items) {
    let comments = await getComments(item.id);
    item.comments = comments.map( c => { return { body: c.body, user_id: c.user_id } })
  }
  fs.writeFileSync(`comments.json`, JSON.stringify(items, null, '  '));
  // let items = JSON.parse(fs.readFileSync(`comments.json`, 'utf-8')); // 一度出力成功した場合はファイルから読み込む
  let accessToken = await getAccessToken();
  for (let item of items) {
    let comments = item.comments.filter( c => c.user_id !== item.user_id);
    for (let comment of comments) {
      let sentiment = await getSentiment(accessToken, comment.body)
      comment.sentiment = sentiment;
      sleep(1000)
    }
    item.comment_num = comments.length;
    item.happy_score = 0;
    item.happy_num = 0;
    for (let comment of comments) {
      if (comment.sentiment.sentiment === 'Positive') {
        item.happy_num += 1;
        item.happy_score += comment.sentiment.score;
      }
    }
  }
  items.sort((a, b) => (b.happy_score - a.happy_score === 0) ? b.likes_count - a.likes_count : b.happy_score - a.happy_score);
  fs.writeFileSync(`happyComments.json`, JSON.stringify(items, null, '  '));
  // let items = JSON.parse(fs.readFileSync(`happyComments.json`, 'utf-8')); // 一度出力成功した場合はファイルから読み込む
  let doc = '|順位|ユーザ|タイトル|Comments (Positive/母数)|<font color="red">Ha</font>ppy Score|LGTM|\n|---|---|---|---|---|---|\n';
  for (let i = 0; i < items.length; i++) {
    let item = items[i];
    doc += `|${i + 1}|@${item.user_id}|[${item.title}](https://qiita.com/${item.user_id}/items/${item.id})|<font color="red">${item.happy_num}</font>/${item.comment_num}|**<font color="red">${item.happy_score.toFixed(2)}</font>**|${item.likes_count}|\n`;
  }
  fs.writeFileSync(`happyCommentsItemRanking.md`, doc);

  // 平均を求める
  for (let item of items) {
    item.happy_average = item.comment_num === 0 ? 0 : item.happy_score / item.comment_num;
  }
  items.sort((a, b) => (b.happy_average - a.happy_average === 0) ? b.likes_count - a.likes_count : b.happy_average - a.happy_average);
  doc = '|順位|ユーザ|タイトル|Comments (Positive/母数)|<font color="red">Ha</font>ppy Score Avarage |LGTM|\n|---|---|---|---|---|---|\n';
  for (let i = 0; i < items.length; i++) {
    let item = items[i];
    doc += `|${i + 1}|@${item.user_id}|[${item.title}](https://qiita.com/${item.user_id}/items/${item.id})|<font color="red">${item.happy_num}</font>/${item.comment_num}|**<font color="red">${item.happy_average.toFixed(2)}</font>**|${item.likes_count}|\n`;
  }
  fs.writeFileSync(`happyCommentsAvarageItemRanking.md`, doc);

  // コメントごとの集計
  let comments = [];
  for (let item of items) {
    for (let comment of item.comments) {
      if (comment.user_id !== item.user_id) {
        comments.push({
          item_id: item.id,
          item_title: item.title,
          item_user_id: item.user_id,
          user_id: comment.user_id,
          body: comment.body,
          happy_score: comment.sentiment.sentiment === 'Positive' ? comment.sentiment.score : 0,
        })
      }
    }
  }
  comments.sort((a, b) =>  b.happy_score - a.happy_score);
  doc = '|順位|ユーザ|コメント内容|対象記事|<font color="red">Ha</font>ppy Score|\n|---|---|---|---|---|\n';
  for (let i = 0; i < comments.length; i++) {
    let comment = comments[i];
    doc += `|${i + 1}|@${comment.user_id}|${comment.body.replace(/\n/g, '')}|[${comment.item_title}](https://qiita.com/${comment.item_user_id}/items/${comment.item_id})|**<font color="red">${comment.happy_score.toFixed(2)}</font>**|\n`;
  }
  fs.writeFileSync(`happyCommentsRanking.md`, doc);
})();
```

</div>

# 結果発表

|順位|ユーザ|タイトル|Comments (Positive/母数)|<font color="red">Ha</font>ppy Score|LGTM|
|---|---|---|---|---|---|
|1|@honehoney|[「たけのこの里」を「きのこの山」に『正しく』自動で修正して差し上げるプログラム](https://qiita.com/honehoney/items/a8c7ba2c53a70c88502e)|<font color="red">12</font>/28|**<font color="red">3.47</font>**|1376|
|2|@omiita|[「募ってはいるが、募集はしていない」 人たちへ](https://qiita.com/omiita/items/0f811f15e569bf2539b8)|<font color="red">8</font>/26|**<font color="red">1.97</font>**|1166|
|3|@eigs|[忙しい MATLAB 芸人向け Qiita ふり返り（2019年版）](https://qiita.com/eigs/items/a75f816a9ad2b75466b6)|<font color="red">3</font>/3|**<font color="red">1.47</font>**|10|
|4|@ga_i_sa_i|["幸せ"の多い生涯を送って来ました。[COTOHA APIを使って「人間失格」を"幸せ"に]](https://qiita.com/ga_i_sa_i/items/972c48b769f5beaaab1c)|<font color="red">3</font>/3|**<font color="red">0.93</font>**|24|
|5|@aikawa4866|[感情分析でサボテンは踊るのをやめてしまうか](https://qiita.com/aikawa4866/items/3bb21d24e9b06041c758)|<font color="red">1</font>/2|**<font color="red">0.71</font>**|187|
|6|@tanaken0515|[[自然言語処理 初心者向け] COTOHA API を Ruby で使いたくて gem を作りました](https://qiita.com/tanaken0515/items/174e3076750891c218b9)|<font color="red">1</font>/1|**<font color="red">0.68</font>**|12|
|7|@j5c8k6m8|[キラやば～っ☆ な「COTOHA DECO」作っちゃった！？よーしっ、さっそくQiitaへ投稿だーっ☆](https://qiita.com/j5c8k6m8/items/b5df0703729a050f19ee)|<font color="red">1</font>/1|**<font color="red">0.59</font>**|15|
|8|@kishiyyyyy|[褒められて伸びる自分のためにポジティブなツイートしか表示されないエゴサーチツールを作った話](https://qiita.com/kishiyyyyy/items/35dba0d2560d797b9a2b)|<font color="red">1</font>/1|**<font color="red">0.47</font>**|11|
|9|@yossymura|[人間は、そんなに悲しい生き物じゃないけれど](https://qiita.com/yossymura/items/177610fbd58b783534f2)|<font color="red">2</font>/5|**<font color="red">0.38</font>**|183|
|10|@eigs|[【COTOHA API x MATLAB】Qiita 投稿記事の要約](https://qiita.com/eigs/items/39196afdb5f28bf3ba0f)|<font color="red">1</font>/1|**<font color="red">0.37</font>**|15|

**<font color="red">C</font>ool!賞** を受賞予定の @honehoney さんの **[「たけのこの里」を「きのこの山」に『正しく』自動で修正して差し上げるプログラム](https://qiita.com/honehoney/items/a8c7ba2c53a70c88502e)** が **見事2冠** を獲得です:trophy:
おめでとうございます:clap:

# 番外編1(平均で評価)

コメントが付いている方が、いい記事と評価したかったので、**総計** で評価しましたが、コメント数に対する **平均** でも評価してみます。

|順位|ユーザ|タイトル|Comments (Positive/母数)|<font color="red">Ha</font>ppy Score Avarage |LGTM|
|---|---|---|---|---|---|
|1|@tanaken0515|[[自然言語処理 初心者向け] COTOHA API を Ruby で使いたくて gem を作りました](https://qiita.com/tanaken0515/items/174e3076750891c218b9)|<font color="red">1</font>/1|**<font color="red">0.68</font>**|12|
|2|@j5c8k6m8|[キラやば～っ☆ な「COTOHA DECO」作っちゃった！？よーしっ、さっそくQiitaへ投稿だーっ☆](https://qiita.com/j5c8k6m8/items/b5df0703729a050f19ee)|<font color="red">1</font>/1|**<font color="red">0.59</font>**|15|
|3|@eigs|[忙しい MATLAB 芸人向け Qiita ふり返り（2019年版）](https://qiita.com/eigs/items/a75f816a9ad2b75466b6)|<font color="red">3</font>/3|**<font color="red">0.49</font>**|10|
|4|@kishiyyyyy|[褒められて伸びる自分のためにポジティブなツイートしか表示されないエゴサーチツールを作った話](https://qiita.com/kishiyyyyy/items/35dba0d2560d797b9a2b)|<font color="red">1</font>/1|**<font color="red">0.47</font>**|11|
|5|@eigs|[【COTOHA API x MATLAB】Qiita 投稿記事の要約](https://qiita.com/eigs/items/39196afdb5f28bf3ba0f)|<font color="red">1</font>/1|**<font color="red">0.37</font>**|15|
|6|@aikawa4866|[感情分析でサボテンは踊るのをやめてしまうか](https://qiita.com/aikawa4866/items/3bb21d24e9b06041c758)|<font color="red">1</font>/2|**<font color="red">0.35</font>**|187|
|7|@ga_i_sa_i|["幸せ"の多い生涯を送って来ました。[COTOHA APIを使って「人間失格」を"幸せ"に]](https://qiita.com/ga_i_sa_i/items/972c48b769f5beaaab1c)|<font color="red">3</font>/3|**<font color="red">0.31</font>**|24|
|8|@dakikd|[COTOHA APIで Pen Pineapple Apple Pen](https://qiita.com/dakikd/items/f15d4814a52ef17670cd)|<font color="red">2</font>/2|**<font color="red">0.14</font>**|19|
|9|@honehoney|[「たけのこの里」を「きのこの山」に『正しく』自動で修正して差し上げるプログラム](https://qiita.com/honehoney/items/a8c7ba2c53a70c88502e)|<font color="red">12</font>/28|**<font color="red">0.12</font>**|1376|
|10|@omiita|[「募ってはいるが、募集はしていない」 人たちへ](https://qiita.com/omiita/items/0f811f15e569bf2539b8)|<font color="red">8</font>/26|**<font color="red">0.08</font>**|1166|

平均では @tanaken0515さん の **[[自然言語処理 初心者向け] COTOHA API を Ruby で使いたくて gem を作りました](https://qiita.com/tanaken0515/items/174e3076750891c218b9)** が **1位** です:medal:
おめでとうございます:clap:

# 番外編2(コメント自体のランキング)

|順位|ユーザ|コメント内容|対象記事|<font color="red">Ha</font>ppy Score|
|---|---|---|---|---|
|1|@perpouh|ネガポジ逆にしたら「人間同士の諍いを見て喜ぶ悪魔」も作れますね₍₍⁽⁽👿🔱₎₎⁾⁾|[感情分析でサボテンは踊るのをやめてしまうか](https://qiita.com/aikawa4866/items/3bb21d24e9b06041c758)|**<font color="red">0.71</font>**|
|2|@youwht|丁寧な解説、とても勉強になりました。m(_ _)m拡張固有表現の試行も面白いですね。|[[自然言語処理 初心者向け] COTOHA API を Ruby で使いたくて gem を作りました](https://qiita.com/tanaken0515/items/174e3076750891c218b9)|**<font color="red">0.68</font>**|
|3|@youwht|素敵！キラやば～っ☆な１日に、なりますよーに！|[キラやば～っ☆ な「COTOHA DECO」作っちゃった！？よーしっ、さっそくQiitaへ投稿だーっ☆](https://qiita.com/j5c8k6m8/items/b5df0703729a050f19ee)|**<font color="red">0.59</font>**|
|4|@watawatavoltage|サーベイ記事本当に助かります．|[忙しい MATLAB 芸人向け Qiita ふり返り（2019年版）](https://qiita.com/eigs/items/a75f816a9ad2b75466b6)|**<font color="red">0.54</font>**|
|5|@watawatavoltage|確かに，一時的にバズるだけじゃだめですよね．振り返ることによって，こんなものあったなとかこれよかったななど思い出すきっかけになりますよね．|[忙しい MATLAB 芸人向け Qiita ふり返り（2019年版）](https://qiita.com/eigs/items/a75f816a9ad2b75466b6)|**<font color="red">0.50</font>**|
|6|@NIPPONPON|これは長き戦乱を終わらせる素晴らしいプログラムですね。世に平穏のあらん事を。ところで「おらがはたけのこのさといもはうまい」のようなケースも救済するでしょうか?|[「たけのこの里」を「きのこの山」に『正しく』自動で修正して差し上げるプログラム](https://qiita.com/honehoney/items/a8c7ba2c53a70c88502e)|**<font color="red">0.48</font>**|
|7|@tanaken0515|gem をご活用くださりありがとうございます！ポジティブになれて素敵ですね :clap: |[褒められて伸びる自分のためにポジティブなツイートしか表示されないエゴサーチツールを作った話](https://qiita.com/kishiyyyyy/items/35dba0d2560d797b9a2b)|**<font color="red">0.47</font>**|
|8|@kitamin|30年来続くこの国内最大の内戦に対してのスマートなアプローチですが、`きのこの里` とか `たけのこの山` とか双方巻き込む無差別テロ誤り発言が出てきたらどう対処できるんでしょうね？(*´-`)|[「たけのこの里」を「きのこの山」に『正しく』自動で修正して差し上げるプログラム](https://qiita.com/honehoney/items/a8c7ba2c53a70c88502e)|**<font color="red">0.47</font>**|
|9|@Yasha_Wedyue|非常に面白い記事でした。ただこの方法ですと、きのこの山とたけのこの里両方を好き（どちらかというとたけのこの里派なテロリストも含む）な場合、異分子として排除できないのでは？と思いました。|[「たけのこの里」を「きのこの山」に『正しく』自動で修正して差し上げるプログラム](https://qiita.com/honehoney/items/a8c7ba2c53a70c88502e)|**<font color="red">0.46</font>**|
|10|@Taro_man|「人間勝者」でも名作になりそうです。|["幸せ"の多い生涯を送って来ました。[COTOHA APIを使って「人間失格」を"幸せ"に]](https://qiita.com/ga_i_sa_i/items/972c48b769f5beaaab1c)|**<font color="red">0.45</font>**|

なんと、 @perpouhさん の **ネガポジ逆にしたら「人間同士の諍いを見て喜ぶ悪魔」も作れますね₍₍⁽⁽👿🔱₎₎⁾⁾** が **最もトップの <font color="red">0.71</font> をマーク** しました！
おめでとうございます:clap:

**コメントが、執筆者のモチベーションを保ち、Qiitaを支えているといっても過言ありません。**

みなさんの ポジティブなコメントに、感謝いたします:blush: