cucumber flesh

Rを中心としたデータ分析・統計解析らへんの話題をしていくだけ

本日発表!ほくぽえむ大賞2017 俳句の部

f:id:u_ribo:20171207065706p:plain

ホクソエムといえばポエムです(要出典)。今日はTwitterでのホクソエム氏の投稿から、俳句を探してこようと思います。そして、今年のベスト俳句を独断と偏見により決めます。戦略としては、Twitterから投稿を取得、日本語形態素解析システム JUMAN++により形態素解析、単語の読みの文字数から俳句の定型を判定、というものです。

それでは早速データを取得するところから始めましょう。データの利用に際して、ホクソエム氏から許可をいただいております。

言葉にするのって難しいですね。

データ取得と前処理

ツイッターAPIを利用するため、rtweetパッケージを用います。認証を済ませ、タイムラインから投稿を取得します。

library(magrittr)
library(tidyverse)
library(rtweet)

get_timeline()でユーザを指定し、取得件数を決定します。他の引数からAPIのパラメータを変更可能なようですが、下記の例ではうまくいかなかったです。

df_hoxom <- get_timeline("hoxo_m", n = 3000, 
    exclude_replies = TRUE, include_rts = FALSE)

取得したデータから、今年の投稿を抽出します。また、RTやリプライ、URLを含んだ投稿等を除外します。加えて今回の俳句判定には、アルファベットを含むとカウントを正常に行えないという課題がありましたのでこれも除外します。その他の空白や記号の除去も次のコードで実行します。

df_hoxom$created_at %>% range()
## [1] "2016-09-24 14:21:10 UTC" "2017-12-06 13:15:42 UTC"
df_hoxom %<>% # 今年の投稿、RTやメンションを取り除く
filter(between(created_at, lubridate::ymd_hms("20170101 00:00:00"), 
    lubridate::ymd_hms("20171231 23:59:59")), 
    is_retweet == FALSE, is.na(reply_to_status_id), 
    is.na(urls_url), !grepl("^@", text)) %>% 
    # 記号、空白の除去
mutate(text = str_replace_all(text, "[:punct:]", 
    "") %>% str_replace_all(., "[:space:]", 
    "")) %>% filter(!grepl("[A-Za-z]", text)) %>% 
    # 余分な列を削除
# 投稿のID、投稿日時、投稿文に制限
select(status_id, created_at, text)

俳句の判定

肝となる俳句の判定は、次の条件で行います。

  • 投稿文を平仮名読みに直し、文字数を数える
  • 単語の文字数で5,7,5のリズム(17文字)になるものを「俳句」とする
"古池や蛙飛び込む水の音" %>% str_count()
## [1] 11
"ふるいけやかわずとびこむみずのおと" %>% 
    str_count()
## [1] 17
df_hoxom$text[1]
## [1] "謙虚さと学び"
df_hoxom$text[1] %>% str_count()
## [1] 6

柿食えば鐘が鳴るなり法隆寺」のように17文字ではないものも俳句になりますが、今回は厳密に17文字のものを俳句とみなしています。

肝心の自然言語処理の部分はJUMAN++にやってもらいます。JUMAN++のRラッパパッケージrjumanppid:songcunyouzai (y__mattu) が開発してくれているのでそれを使います。サンキューマッツ!

[https://github.com/ymattu/rjumanpp:embed:cite]

[http://y-mattu.hatenablog.com/entry/2017/08/19/230432:embed:cite]

text_wakati <- rjumanpp::jum_text("かずたんがフリー素材になっている")
text_wakati[[1]]
##  [1] "かず"                     "かず"                    
##  [3] "かず"                     "名詞"                    
##  [5] "6"                        "普通名詞"                
##  [7] "1"                        "*"                       
##  [9] "0"                        "*"                       
## [11] "0"                        "\"代表表記:下図/かず"    
## [13] "自動獲得:EN_Wiktionary\""
text_wakati[[5]]
##  [1] "素材"                     "そざい"                  
##  [3] "素材"                     "名詞"                    
##  [5] "6"                        "普通名詞"                
##  [7] "1"                        "*"                       
##  [9] "0"                        "*"                       
## [11] "0"                        "\"代表表記:素材/そざい"  
## [13] "〜を〜に構成語"           "カテゴリ:人工物-その他\""

「かずたん」(もしかして: @kazutan)がうまく分かち書きできていないのが気になりますが、読みや品詞の区分が行われた結果が得られました。

この結果をデータフレームにしておくと色々捗るので関数を用意しましょう。

jum_text_separate <- function(text) {
    x <- rjumanpp::jum_text(text)
    res <- x %>% purrr::map_df(~tibble::data_frame(.[2], 
        .[4], .[6])) %>% purrr::set_names(c("yomi", 
        "hinsi_bunrui_dai", "hinsi_bunrui_sai"))
    return(res)
}

次に今作成した関数を利用し、読みの文字数が17文字となるものを抽出します。これが条件1をクリアした「俳句」の候補となります。そして、条件をクリアした投稿文に対して、単語の読みの文字数を数えます。これを足し合わせ、5,7(12),5(17)となっているものを最終的な「俳句」とみなします。

"かずたんがフリー素材になっている" %>% 
    jum_text_separate(text = .) %>% # 17文字となるものに制限
filter_at(vars("yomi"), any_vars(str_count(paste(., 
    collapse = "")) == 17)) %>% # 単語の文字数を累積
mutate(haiku = cumsum(str_count(yomi)))
## # A tibble: 8 x 4
##     yomi hinsi_bunrui_dai hinsi_bunrui_sai haiku
##    <chr>            <chr>            <chr> <int>
## 1   かず             名詞         普通名詞     2
## 2   たん             名詞         普通名詞     4
## 3     が             助詞           格助詞     5
## 4 ふりー           形容詞                *     8
## 5 そざい             名詞         普通名詞    11
## 6     に             助詞           格助詞    12
## 7 なって             動詞                *    15
## 8   いる           接尾辞     動詞性接尾辞    17

この「かずたん〜」は5,7,5になっているので条件をクリアしていることになります。ということでこれも関数化しておきましょう。先ほど書いた関数を追加して、一度に全ての処理を完結するようにします。

is_haiku <- function(text, ...) {
    x <- jum_text_separate(text) %>% filter_at(vars("yomi"), 
        any_vars(str_count(paste(., collapse = "")) == 
            17)) %>% mutate(haiku = cumsum(str_count(yomi))) %>% 
        filter(haiku %in% c(5, 12, 17))
    res <- if_else(nrow(x) == 3, TRUE, FALSE)
    return(res)
}

次のような判定結果を得ます。

c("かずたんがフリー素材になっている", 
    "謙虚さと学び", "なつくさやつわものどもがゆめのあと") %>% 
    map_lgl(is_haiku)
## [1]  TRUE FALSE  TRUE

is_haiku()の結果を投稿データに適用し、いよいよ「ほくぽえむ大賞2017 俳句の部」の作品発表です!

df_hoxom %<>% filter(pmap_lgl(., ~is_haiku(text, 
    ...)) == TRUE)

ほくぽえむ大賞2017 俳句の部

入選作品の発表です。厳しい審査の結果、今年は4件の発表が選ばれました。

df_hoxom %>% knitr::kable(format = "markdown")
status_id created_at text
917606569378017280 2017-10-10 04:23:49 ぞうさんのおすすめテーマにしてみた
874064391338999808 2017-06-12 00:42:44 エレベタやおっさんどもが臭の跡
852532562635313152 2017-04-13 14:42:56 多様体いろんなとこに潜んでる
832896107428536320 2017-02-18 10:14:41 かずたんがフリー素材になっている

佳作

どういう状況かわかりませんが、声に出して読みたい感が高評価につながりました。おめでとうございます!また、「かずたん」は入選作品に2回も登場することから俳句に適したポエットな単語であることがわかりました。

データサイエンティストとしての意識からなのか、ふとしたつぶやきに才能を感じるこの投稿が佳作です。わたしはこの作品から、星新一ショートショートを連想しました。皆さんはどうでしょう。

大賞

こちらの俳句は初夏に詠まれたものです。「エレベタ」と「おっさん」の組み合わせ、そして「息の跡」とポエム度が高いです!スペースも入れて、詠み人自身の俳句としての意識が強いこの投稿が大賞となります!おめでとうございます!!

審査委員の言葉

いかがだったでしょう。思ったりもポエム(俳句)が少ないですね。17文字という自由度の低さが原因だったのか、この辺は改良が必要そうです。また、来年は「俳句」で肝心なキレも採点基準に加えたいと思います。

来年はより多くのポエムが投稿されることをねがいます。ホクソエムアドベントカレンダー、明日は未定です!誰か〜

Enjoy!

ある日tidyと一緒に: tidyverseは厳しいがとても優しい

この記事はtidyポエムAdvent Calendarの4日目の記事となります。タイトルは釣りです。釣られた人は乙でした。

本当のタイトルは「tidyverseは厳しいがとても優しい」です。某ホクソエム氏のようです(個人の意見です)。

uribo.hatenablog.com

tidyverseな世界に慣れてきて、つまづきやすいかなという点をまとめました。小ネタ的なものですが、少しでも皆さんの参考になればと思います。言いたいことは、データ型に気をつけろ、ということです。

tibble

はじめに言っておくと、私はtibble大好きです。界隈ではtibbleのせいで、tibbleってなんだよ、と言った声も聞こえてくるわけですが、tibbleにはメリットしか感じません。tibble::as_tibble()、みんなtibbleになるといいよ(というのは言い過ぎか)。

まず、tibbleをご存知でない方に説明しておくと、tibbleというのはRのデータフレームオブジェクトを拡張したオブジェクト形式の一種です。tibbleの特徴をまとめると次のようになります。

  1. 入力された値(文字列)をそのまま評価する(文字列型として扱う。因子型に変換しない。)
  2. row.namesを与えない
  3. 列名の空白や記号を入力の通りに評価する
  4. 変数の遅延評価を行える
  5. リストクラスのオブジェクトを内包することが可能
  6. 長さが1しかないベクトルの値が再利用される
  7. 引数に与えた列のデータを別の列に利用できる(変数の再利用)
  8. 出力の際にデータフレーム全体を表示するのではなく、データフレームのデータフレ0.ムのソースと大きさ、変数のデータ型を表示する
  9. 変数を参照する際、正確な名称を指定しない限りNULLを返す

個人的に大事だなと思うのが、5と8です。5については階層構造のあるデータフレーム、として大事な概念です。ここで述べると長くなるので以前書いた記事へのリンクを貼ります。このおかげでdplyr::dopurrr::dmap()がいい感じで機能します。ここでは8の特徴について説明します。

suryu.me

Hadley Ecosystem 2016 by Uryu Shinya

通常のデータフレームは、オブジェクトを出力するとデータの全体が返ってきます。データ数が少なければ良いですが、これって実は見にくいですよね。行数が多いと、変数名とデータを照らし合わせるのにコンソールをスクロールをしなければならない。でもtibbleでは、出力は先頭の数行だけです(オプションで変更可能)。また、データがどのような型をしているのかも表示されるので、因子型と文字列型の違いによるトラブルを避けやすくなります。全体のサイズも表示してくれるのも良いですね。優しい!

iris %>% class()

## [1] "data.frame"

iris %>% tibble::as_tibble()

## # A tibble: 150 x 5
##    Sepal.Length Sepal.Width Petal.Length Petal.Width Species
##           <dbl>       <dbl>        <dbl>       <dbl>  <fctr>
##  1          5.1         3.5          1.4         0.2  setosa
##  2          4.9         3.0          1.4         0.2  setosa
##  3          4.7         3.2          1.3         0.2  setosa
##  4          4.6         3.1          1.5         0.2  setosa
##  5          5.0         3.6          1.4         0.2  setosa
##  6          5.4         3.9          1.7         0.4  setosa
##  7          4.6         3.4          1.4         0.3  setosa
##  8          5.0         3.4          1.5         0.2  setosa
##  9          4.4         2.9          1.4         0.2  setosa
## 10          4.9         3.1          1.5         0.1  setosa
## # ... with 140 more rows

データ型

tidyverseに慣れてくると、特に、先ほどのtibbleで出力されるデータ型を意識するようになります。一見するとハマりどころです。

if_elseの例

この話はすでにyutannihilationさんブログに書かれていることですが、アンサーソングへのアンサーソングという感じで。

Rのifelse()はちょっとゆるいという話です。例えば次のように条件分岐で返り値が変化する処理を書いた場合、結果のデータ型が複数定義されていても問題とはなりません。

# 文字列と数値を含める
ifelse(10 > 1, "a", FALSE)

## [1] "a"

ifelse(10 > 1, TRUE, "b")

## [1] TRUE

しかし条件によってデータ型が変わってしまうのはトラブルの元になりかねません。ifelse()をより厳密にしたdplyr::if_else()では上の処理はエラーとなります。

dplyr::if_else(10 > 1, "a", FALSE)
# Error: `false` must be type string, not double
dplyr::if_else(10 > 1, TRUE, "b")
# Error: `false` must be type logical, not character

dplyr::if_else()の中での返り値は、複数の型を混ぜることはできません。上の処理を実行するには文字列で揃える必要があります。

# 文字列にしたいなら明示的に文字列としておく
dplyr::if_else(10 > 1, as.character(TRUE), "b")

## [1] "TRUE"

特にハマるポイントなのが欠損値の処理です。次のように入力値が10未満である場合に入力値をそのまま返す、という処理を組んだ際、NAをNAとして返そうとするとエラーです。

hoge <- function(x) {
  dplyr::if_else(10 > x, NA, "10以上")
}

hoge(5)
# Error: `false` must be type string, not logical

欠損値にもデータ型に応じて複数の種類があります。この場合はFALSEの値が文字列ですので、TRUEの際も文字列としての欠損値が返るようにしなくてはいけません。

# NAのデータ型を文字列由来にする
fuga <- function(x) {
  dplyr::if_else(10 > x, NA_character_, "10以上")
}

# 返り値はNA
fuga(5)

## [1] NA

join, bindの例

データの結合にjoin、bind群の関数を使いますが、ここでもデータ型に注意が必要です。次の例はエラーになります。なぜでしょうか。

dplyr::full_join(iris[, "Species"],
                 iris[, "Species"] %>% tibble::as_tibble(),
                 by = "Species")
# Error in UseMethod("full_join") : 
#   no applicable method for 'full_join' applied to an object of class "factor"

これは因子型であるiris$Speciesと、tibble化され文字列となったiris$Speciesの列がデータ型が異なるがためのエラーです。もう一つ例を見ましょう。次は正解例から示します。

d <- data.frame(
  var = c(1, 3, 5),
  var2 = letters[1:3]
)
d2 <- data.frame(
  var = c(1, 2, 3)
)

dplyr::bind_rows(d, d2)

##   var var2
## 1   1    a
## 2   3    b
## 3   5    c
## 4   1 <NA>
## 5   2 <NA>
## 6   3 <NA>

dplyr::left_join(d, d2, by = "var")

##   var var2
## 1   1    a
## 2   3    b
## 3   5    c

こちらが失敗する例です。d3というデータフレームでは文字列var変数を持っています。

d3 <- data.frame(
  var = c("1", "2", "3")
)

dplyr::bind_rows(d, d3)
# Error in bind_rows_(x, .id) : 
#   Column `var` can't be converted from numeric to factor

dplyr::left_join(d, d3, by = "var")
# Error in left_join_impl(x, y, by$x, by$y, suffix$x, suffix$y, check_na_matches(na_matches)) : 
#   Can't join on 'var' x 'var' because of incompatible types (factor / numeric)

このように、変数名やデータ構造が一致していても、データ型が異なる場合には処理を停止してくれる(今後のトラブルを防ぐ)処理が施されています。優しい!

purrr::mapの例

purrrパッケージのmap()の返り値はリストオブジェクトですが、map()の返り値をオブジェクトの種類を関数名を変更することで指定できます。例えば文字列として返したいならばpurrr::map_chr()を使います。

letters[1:4] %>% 
  purrr::map(toupper)

## [[1]]
## [1] "A"
## 
## [[2]]
## [1] "B"
## 
## [[3]]
## [1] "C"
## 
## [[4]]
## [1] "D"

letters[1:4] %>% 
  purrr::map_chr(toupper)

## [1] "A" "B" "C" "D"

一方で、指定した型へ変換できない際は怒られます。当然ですね。データ型の変換は、「論理型、整数型、倍精度小数点型、文字列」の順で行われます。なので、数値を文字列にすることはできてもその逆はできません。気をつけましょう。

letters[1:4] %>% 
  purrr::map_dbl(toupper)
# Error: Can't coerce element 1 from a character to a double

この他にもdplyrselect_ifmutate_ifなどを利用する際にデータ型によって処理を適用する、ということがあるかと思います。データ型の確認は重要。繰り返しておきます。

tidyverseのコンフリクトやバージョン

さて、tidyverseはRでtidyデータを扱うための概念とも捉えられるわけですが、実態としてのtidyverseも存在します。ズバリtidyverseパッケージを読み込みます。

library(tidyverse)

f:id:u_ribo:20171204102850p:plain

するとこのような出力がでます。上から、tidyverseパッケージ自体のバージョンおよびアタッチされた(利用可能となった)パッケージとそのバージョン、名前空間の衝突が発生している関数名の一覧です。多数のパッケージを読み込み、標準実装されている関数名とも衝突するtidyverseパッケージ、その情報を出力してくれるなんて、優しさに溢れています!

これはRStudioでの実行結果ですが、コンソールでもR.appでもカラーリングは実行されます。これにはcrayonパッケージが一役買っています。色をつけることで、情報の次元が広がりますね。優しい!

いかがだったでしょう。tidyverseの優しさと厳しさ、Hadleyの愛、伝わりましたでしょうか。今日のポエムはここで締めます。また会いましょう。

Enjoy!

私とホクソエム

この記事は「HOXO-M Advent Calendar 2017」の2日めです。昨日は id:yutannihilationさんの「出ない順ホクソエム語彙集(その1)」でした。ホクソエムってなんなんでしょうね笑

さて、2日目は私が担当します。それでは聞いてください「私とホクソエム」。


現在の日本のRコミュニティで、もっとも長い歴史とたくさんの発表者・参加者がいるTokyo.R。その中で私が最初にスゲーと思ったのがこのアドベントカレンダーの主役である「ホクソエム(hoxo_m)」氏である。某ECサイトのロゴを連想させるアイコンと可愛げのある「h(o x o )m」という表現での顔文字(?)もまた可愛いのがにくい。

そんな彼との最初の出会いは、もちろんネットである。私がどっぷりと統計言語Rにハマりだした2014年から2015年にかけて、ホクソエム氏もまたブログやQiitaにRの記事を書いていた。いや、ホクソエム氏の記事があったから私はここまでRが好きになれたのかもしれない。

本物の「ホクソエム」をみたのは2014年のBUGS/stan勉強会 #3である。相関係数という身近な物を題材にStanを使った解説をしてくれた。

d.hatena.ne.jp

で、それに触発されて私も参加記事を書いた。

qiita.com

だがしかし、その時はまだホクソエム氏に認識されていなかったようである。

Oh... まあ、仕方ないよね。

以降、ホクソエム氏に認識されてもらうべく、私のQiitaでの活動は熱を増していった(かどうかは記憶が定かではないが、記事をめっちゃ書いている)。

そんな活動が認められたのか、運命の時は訪れる。2015年2月のTokyo.R#46だ。自分はLTをした。発表後、ホクソエムさんがやってこられて名刺を頂いた。ホクソエム氏のブログの最初の記事には

2年ほど前にバイオインフォマティクス系の会社に入社

http://d.hatena.ne.jp/hoxo_m/20100727/1280236673

とあり、その会社にいる頃に名刺を頂いた(だと思う)。当時大学院生だった私は交換する名刺を持ち合わせていなかったが、憧れの「ホクソエム」名刺を手に入れて名刺文化も悪くないなと思った記憶がある。まだ匿名知的集団ホクソエムも株式会社ホクソエムも誕生していなかったが、当時からホクソエムというブランドがあったのは確かである。

これでホクソエム氏とお近づきになったぞ!と思っていたのもつかの間、今度は雷を落とされる。ホクソエム氏は2015年10月に転職をしているのだが、前職の退職記念に飲み会が開催されることになっていて、幸運なことにそれに参加させて頂いた。そこで事件はおこる。

ホクソエム「うりさんは研究者になりたいならRやっている場合じゃないんじゃないの?
Rで食っていくの?? そんなことできるの???」

私「え、その...(今それいう??)」

だいたいそんな感じの会話があった。Rで記事を書いて、Qiitaなんかでちょっともてはやされて浮かれていた私にはマイティー・ソーの放つ雷よりも強烈な言葉だった。

その言葉は私が大学院を退学するまで、いや今も心の中で響いている(ホクソエムさんの言葉が引き金になったとか最後の一撃になったとかではないです)。

ホクソエムさんはこのように、厳しい。Twitterでも自然な発言が発端となってどこかに流れ弾が飛んでいることもあるように思う。私も何度か食らった。だが私はその厳しさを見習いたいと思う。きっとホクソエム氏は自分の息子(界隈ではきーたと呼んでいる)にも厳格な父であるのだろう。そんなホクソエムを尊敬する。

といいつつも、褒めるときは褒めてくれるし、メンションで気楽に絡んでくれる時もあり、優しい。

と振り返ってみたが、期間は長いがそこまで深い付き合いとは言えないのが現実だ。ベロベロさんを交えて進捗カフェへ行くこともあったが、二人で話した機会はそんなにない気がする。 にも関わらず、ホクソエムが会社を立ち上げた際には、追加メンバー?として誘ってくれたし、ホクソエム経由で仕事も回してもらったこともある。大変感謝している。 最近では、私が転職で悩んでいる際も相談に乗ってくれた。ホクソエム氏は裏表のない言葉をかけてくれる(と思っている)ので、なんだかありがたい。

「うりさんの文章はどこかに欠陥があるんだよ」と言った数日後に手渡してくれたことも忘れない。上司にしたいホクソエムの鑑だ。

思えばホクソエムは、いつも私の先にいる。統計モデリング、DB, Shiny, ウェブスクレイピング, ラムダ式... これらはホクソエムに教えてもらったと言っても過言ではない。少なくとも一度は彼の記事を参考にしている。Qiitaも未だホクソエムを超えられないでいる(今年ほんの一瞬抜いたがすぐに再び追い越された)。日本を代表するRユーザである彼を超えるのが、私の目標だ。

f:id:u_ribo:20171201234532p:plain

最後に、ホクソエム氏の盟友 ナギ=テラモさんの言葉をお借りしたい。

偉そうであるが、これからも親分を応援している。良い父であり、良いデータサイエンティストであることを願う。寿司、行っていないんでいきましょうね笑(あ、北海道でいってたw) こっぱずかしくて直面してこんなポエムは語れないので、このアドベントカレンダーがあってよかったです。

明日は R_Linux さんが書きます。ホクソエムの誓いならぬアキバの誓い、なんなんでしょう。