歩いたら休め

なんでこんな模様をしているのですか?

【R】「20代のエンジニアの間で本当にPHPは廃れたのか?」を集計する

前回の記事で、転職ドラフトのデータをスクレイピングし、簡単な分析を行いました。

kiito.hatenablog.com

ただし、こちらの記事の時点では、転職ドラフトは終わっておらず、中途半端なデータの状態のまま集計していました。 特に最終日に多くの指名が入っていたようで、そこで傾向が変わることは大いに考えられます。

今回は、

  • 年齢と最高提示額の関係を集計する
  • 20代のエンジニアの間でPHPは本当に廃れたのか?

の2点に絞って調べてみようと思います。

なお、前回行った「年齢と最高提示額の関係を集計する」については、そもそも問題設定(「入札額が高いエンジニアに特有なスキルがある」という仮説自体)が的外れである可能性が高いように思うので、今回は実施しませんでした。

年齢と最高提示額の関係を集計する

前回同様の積み上げ棒グラフでのプロットを行いました。

f:id:takeshi0406:20170427225206p:plain

前回の記事のグラフがこちらです。比較すると、「ノーマル級」が一気に減っていることが目立ちますね。

f:id:takeshi0406:20170427224650p:plain

前回同様、上位層だけに注目したグラフをプロットしてみます。前回はここまで集計すると20代後半が減っていましたが、今では30代前半と同様まで残っていることが分かります。

f:id:takeshi0406:20170427230237p:plain

f:id:takeshi0406:20170427225641p:plain

20代のエンジニアの間でPHPは本当に廃れたのか?

こちらの記事の中で、「25〜27歳ぐらいになっている若手エンジニアにPHPの経験がない人が増えている」という記述がありました。

devblog.thebase.in

リブセンスさんが運営されている転職ドラフトという転職サイトで、全員のプロフィールを読んでいて薄々気がついていたことに改めて気がつかされたのですが、BASEの方でサーバサイドに使っているメインの技術はCakePHPというフレームワークでありPHPの技術なのですが、

新卒の就職先がRubyを使っていて、今、25〜27歳ぐらいになっている若手エンジニアにPHPの経験がない人が増えている!

しかし、われわれエンジニアはデータを見ずに判断してはなりません。

まず、30代のエンジニアの人数を調べてみましょう。次のようなコードで集計すると、

  • 30代エンジニアは145人
  • 20代エンジニアは122人

であることが分かりました。

# 年代の人数を調べるコード
df %>%
  dplyr::filter(readr::parse_number(age_name) == 30) %>% # 「20」に変更する
  dplyr::distinct(user_id) %>% 
  nrow()

次に、各年代の利用人数(count)と普及率(rate)の上位10件を取得してみます。

df %>%
  dplyr::filter(readr::parse_number(age_name) == 30) %>%
  dplyr::group_by(skills) %>%
  dplyr::summarise(count = n()) %>%
  dplyr::arrange(desc(count)) %>% 
  dplyr::mutate(rate = count/145) %>% 
  head(10)

30代の結果がこちら

skills count rate
MySQL 77 0.5310345
Java 68 0.4689655
JavaScript 67 0.4620690
PHP 53 0.3655172
AWS 43 0.2965517
Jenkins 43 0.2965517
Ruby 41 0.2827586
jQuery 41 0.2827586
Apache 36 0.2482759
Git 32 0.2206897

20代の結果がこちらです。

skills count rate
MySQL 55 0.4508197
JavaScript 54 0.4426230
AWS 44 0.3606557
PHP 39 0.3196721
Ruby 38 0.3114754
Java 35 0.2868852
jQuery 34 0.2786885
docker 29 0.2377049
Git 29 0.2377049
Jenkins 26 0.2131148

きちんとした技術の名寄せなどを行っていないラフな集計であるものの、たしかに、PHPの普及率が約37% => 約32%に減っており、Rubyの普及率は約28% => 約31%まで増えています。ただし、これが大きな問題になるほど「PHPの経験がない人が増えている」かどうかは分かりません。

もしかすると、企業側から参加者のプロフィールを見て「きちんとした(PHP|Ruby)のプロジェクトに参加したことある」と判断されるもので絞り込むと、この差はもっと広がってしまうのかもしれません。

また、今回は「一気に利用率が(上がった|下がった)技術」も集計すると面白いかもしれませんね。一時期「Smalltalk年収高いけど高齢化もすごい!」と話題になった、以下のツイートのグラフと同じ似た集計を行っても面白いかもしれません。

まとめ

以下のことは言えたと思います。

  • 転職ドラフトの終盤で、20代後半でも高額の指名が入っていた
    • 20代も安心して転職ドラフトに利用しよう!
  • PHPの経験のない人は増えているが、「そこまで極端に減ってなくない?」とも感じた

【R】転職ドラフトのデータをスクレイピングして分析(集計)する

お久しぶりです。

最近上司と「機械学習とかその辺の技術が発展したら、真っ先に自動化されて仕事なくなるのはハンパなエンジニアと中間管理職だよね〜」という話をして危機感を募らせている @takeshi0406 です。

WEBエンジニアにはご存じの方も多いと思いますが、転職ドラフトというWEBサービスがあります。

job-draft.jp

このサービスは、次のような理念や問題感から始まった作られたものだそうです。素晴らしいです。

企業による公開競争入札
これなら、自由競争でのリアルな相対価値がわかるようになる。

エンジニアだからこそ、より明確に。

誰が評価され、誰が評価されないのか。
自分の価値向上には、これから何をすべきなのか。

私も「友達を紹介してオライリー・ジャパンの本をGETしよう!」の文言につられて、友だちを3人紹介した上で登録したのですが、レジュメを丁寧に読んでダメ出ししてもらえ(一度リジェクトされましたw)、その過程で自分のキャリアの振り返りができたので良かったと思っています。

また、何件か入札もいただいて、その中にプロジェクトで苦労した点に対して共感したという(私にとっては嬉しい)コメントもあり、サービスとして好印象を受けています。

ところが、実際使って同世代のエンジニアと話していると、以下のような不満や疑念を話すこともありました。

  • 同じようなスキルのレベルでも、結局年齢で入札額が決められてるんじゃないか(友人は「全く同じレジュメで20代中盤→30代中盤で出したら年収上がるんじゃねw」と冗談半分に話していました)
  • 著名なエンジニアと思しき人が、入札されていない or 低めの額で入札されている
  • 結局、入札額多い会社に目を付けられるかどうかで決まるんじゃないのか

もう一つ、

  • どんなスキルを持っている人が、高めの入札額がつけられるのか

という部分にも興味があるようでした。

というわけで、公開されているデータで分析できそうな、以下の2点について集計してみました。

  1. 年齢と最高提示額の関係
  2. どんなスキルを持っている人が、高めの入札額がつけられているか

今回は、書き捨てコードでデータの集計を行うものなので、統計用プログラミング言語Rを使いました。 おそらくエンジニアではPython + pandasを利用している方が多いと思いますが、個人的には、データの集計目的ではRのほうが楽に書けます。

スクレイピングでデータを取得する

スクレイピングでデータを取得します。以下のようなRのコードでcsvに保存しました。

関数の前にはroxygen形式のコメントを付けています。この辺もうまくまとめたRのプログラム開発については、以下の記事を読んでください。

qiita.com

library(rvest)
library(purrr)
library(dpylr)
library(stringr)
library(readr)

SLEEPTIME = 3 # 3秒ずつSleepさせる
MAX_PAGE_NUM = 29 # 手動で確認する
RANKING_BASE_URL = 'https://job-draft.jp/festivals/6/users?page='
PROFILE_BASE_URL = 'https://job-draft.jp/users/'

#' 今回の全ての参加者のデータをクロールして取得する
#' 
fetchAllData <- function() {
  rank_df <- fetchAllRankData()
  user_df <- rank_df$user_id %>% fetchAllUserData()
  return (
    dplyr::left_join(rank_df, user_df, by = 'user_id')
  )
}

#' ランキングページのデータを取得する
#' 
#' @return ユーザーIDのベクトル
fetchAllRankData <- function() {
  return (
    1:MAX_PAGE_NUM %>% 
      purrr::map(~fetchHtml(paste0(RANKING_BASE_URL, .))) %>% 
      purrr::map(parseRankData) %>% 
      purrr::reduce(rbind)
  )
}

#' 今回の参加者の全てのIDをクロールして取得する
#' 
#' @return ユーザーIDのベクトル
fetchAllUserData <- function(user_ids) {
  return (
    user_ids %>% 
      purrr::map(~list(., fetchHtml(paste0(PROFILE_BASE_URL, .)))) %>% 
      purrr::map(purrr::lift(parseUserData)) %>% 
      purrr::reduce(rbind)
  )
}

#' URLからHTMLを取得する
#' 
#' @param url ターゲットとなるurl
#' @return HTMLオブジェクト
fetchHtml <- function(url) {
  Sys.sleep(SLEEPTIME)
  return (
    xml2::read_html(url)
  )
}

#' ランキングページのHTMLをデータフレームに変換する
#' 
#' @param rank_html ランキングページのHTML
#' @return データフレーム
parseRankData <- function(rank_html) {
  return (
    data.frame(
      user_id = parseUserIds(rank_html),
      age_name = parseRoughAges(rank_html),
      value_name = parseMaxValues(rank_html)
    )
  )
}

#' ユーザーランキングのHTMLからユーザーIDのベクトルを取得する
#' 
#' @param rank_html ユーザーランキングのHTML
#' @return ユーザーIDのベクトル
parseUserIds <- function(rank_html) {
  return (
    rank_html %>% 
      rvest::html_nodes(xpath = '//div[@class="p-users-listview__item"]//a[@class="u-font-sl f-w-bold"]') %>% 
      rvest::html_attr('href') %>% 
      readr::parse_number() 
  )
}

#' おおまかな年齢(xx代後半など)を取り出す
#' 
#' @param rank_html ユーザーランキングのHTML
#' @return 年齢のベクトル
parseRoughAges <- function(rank_html) {
  return (
    rank_html %>% 
      rvest::html_nodes(xpath = '//div[@class="p-users-listview__item"]') %>% 
      rvest::html_text() %>% 
      stringr::str_extract('\\d0代(前半|中盤|後半)')
  )
}

#' おおまか最高指名額を取り出す
#' 
#' @param rank_html ユーザーランキングのHTML
#' @return 最高指名額のベクトル
parseMaxValues <- function(rank_html) {
  return (
    rank_html %>% 
      rvest::html_nodes(xpath = '//div[@class="p-users-listview__item"]') %>% 
      rvest::html_text() %>% 
      stringr::str_extract('(レジェンド|ゴッド|ウィザード|スター|プチリッチ|ノーマル|ノービス)級')
  )
}

#' ユーザーが持つスキルを取得する
#'
#' @param user_id ユーザーid
#' @param user_html ユーザーページのHTML
#' @return ユーザーについてのデータフレーム
parseUserData <- function(user_id, user_html) {
  return (
    data.frame(
      user_id = user_id,
      skills = parseUserSkills(user_html)
    )
  )
}

#' ユーザーが持つスキルを取得する
#' 
#' @param user_html ユーザーページのHTML
#' @return そのユーザーのスキルのベクトル
parseUserSkills <- function(user_html) {
  return (
    user_html %>% 
      rvest::html_nodes(xpath = '//li[@class="c-tag c-tag--s c-tag--gray c-tag--border-gray3 c-tag--s-rounded"]') %>% 
      rvest::html_text() %>% 
      stringr::str_trim()
  )
}

# users.csvに保存する
readr::write_csv(fetchAllData(), './users.csv')

以下のようなcsvが出来上がります。ログイン状態でスクレイピングを行えば、「○○級」より細かいデータが見られるのですが、他のユーザーの不利益になるおそれがあるのでやめました。

user_id,age_name,value_name,skills
xxxx,x0代前半,xxx級,Python
xxxx,x0代前半,xxx級,Ruby
yyyy,y0代中盤,yyy級,PHP
(以下略)

データの持ち方は、Hadley Wickham氏の提唱する整然データ(tidy data)を参考にしました。…といっても、DBの第一正規形のようなもので、「skillsカラムを一行に持たせず、ユーザーを複数行にしている」程度です。

id.fnshr.info

年齢と最高提示額の関係を集計する

まず、先程のスクレイピング結果のデータを読み込んで前処理する関数と、積み上げ棒グラフでプロットする関数を用意します。

library(dpylr)
library(stringr)
library(readr)
library(ggplot2)

AGE_DF <- readr::read_csv('./age.csv')
VALUE_DF <- readr::read_csv('./value.csv')

#' スクレイピングで取得したデータを読み込み、前処理を行う
#' ※ わざわざmax_incomeというカラムを追加しているのは、積み上げ棒グラフの順番を操作する方法が分からなかったからです
#' 
#' @return 年齢・最高額ごとに人数を集計したデータ
readAgeIncomeDf <- function() {
  return (
    readr::read_csv('./users.csv') %>%
      dplyr::distinct(user_id, value_name, age_rank, age_name) %>%
      dplyr::arrange(age_rank) %>%  
      dplyr::group_by(value_name, age_name) %>%
      dplyr::summarise(count = n()) %>% 
      dplyr::left_join(VALUE_DF, by = 'value_name') %>%
      dplyr::mutate(max_income = stringr::str_c(value_rank, '_', value_name))
  )
}

#' 年齢・最高額を積み上げ棒グラフでプロットする
#' 
#' @param df 元データ
plotAgeIncomeGrapth <- function(df) {
  ggplot2::ggplot(df, ggplot2::aes(x = age_name, y = count)) +
    ggplot2::geom_bar(stat="identity", aes(fill = max_income)) +
    ggplot2::scale_x_discrete(limits = rev(AGE_DF$age_name)) +
    ggplot2::theme(text = element_text(family = "HiraKakuPro-W3"))
}

また、AGE_DFとVALUE_DFの中身はこんな感じです。

> # Rのコンソール画面です
> AGE_DF %>% head
# A tibble: 6 × 2
  age_name age_rank
     <chr>    <int>
1 40代後半       12
2 40代中盤       11
3 40代前半       10
4 30代後半        9
5 30代中盤        8
6 30代前半        7
> VALUE_DF %>% head
# A tibble: 6 × 2
    value_name value_rank
         <chr>      <int>
1 レジェンド級          1
2     ゴッド級          2
3 ウィザード級          3
4     スター級          4
5 プチリッチ級          5
6   ノーマル級          6

まずは、すべての参加者のデータをプロットしてみましょう。

readAgeIncomeDf() %>% plotAgeIncomeGrapth()

f:id:takeshi0406:20170422185449p:plain

次に、高めの評価のところを見るために、「プチリッチ級」以上で集計してみます。

意外と山の形は、全員の場合と変わっていないように見えます。

readAgeIncomeDf() %>% dplyr::filter(value_rank %in% 1:5) %>% plotAgeIncomeGrapth()

f:id:takeshi0406:20170422190035p:plain

ところが、「ウィザード級」以上に注目すると、やはり30代以上が多くなります。

f:id:takeshi0406:20170422190217p:plain

ひとまず、今のところの傾向として

  • 20代はプチリッチ級ではよく入札されている
  • ウィザード級以上は30代以上でないと入札されづらい

ことは集計できました。

ただし、これが本当に年齢が低いと評価されづらいのか、年齢を経ることで本当にスキル(もしくはレジュメでの説得力)が上がっているのかは、さすがに情報量不足でわかりませんが。

また、今のところ高い額を提示しているのが、Speeeさんなどの数社しかないので、これからの期間で入札されて傾向が変わるのは充分考えられます。

どんなスキルを持っている人が、高めの入札額がつけられるのか

どんなスキルを身につければ(どんな技術が必要なプロジェクトを進めれば)、高い入札額の人たちの仲間入りできるかを考えてみましょう。

これには同様の集計を行った方がいますが、スター級以上しか集計していないために、「高い入札額を提示された人が持っているスキルなのか、それとも入札されていない人も持っているスキルなのか分からない」という問題があります。

qiita.com

つまり、「各入札額を特徴づけているスキル」を集計すればよく、これは自然言語処理でよく使われる指標であるTF-IDF(にあたるもの)を集計してあげれば良さそうです。

TF-IDFで文書内の単語の重み付け | takuti.me

『いくつかの文書があったとき、それぞれの文書を特徴付ける単語はどれだろう?』こんなときに使われるのがTF-IDFという値。

TFはTerm Frequencyで、それぞれの単語の文書内での出現頻度を表します。たくさん出てくる単語ほど重要!

IDFはInverse Document Frequencyで、それぞれの単語がいくつの文書内で共通して使われているかを表します。いくつもの文書で横断的に使われている単語はそんなに重要じゃない!

というわけで、TF-IDFを計算(データフレームに追加)する関数を用意します。…こういうのを自前で実装すると、プログラムのミスが怖いこと限りないですね。 もし見つけた方がいたら教えてください。

library(dpylr)
library(readr)

VALUE_DF <- readr::read_csv('./value.csv')

#' スクレイピングで取得したデータを読み込み、前処理を行う
#' 
#' @return スキル・最高額ごとに人数を集計したデータ
readSkillsIncomeDf <- function() {
  return (
    readr::read_csv('./users.csv') %>%
      dplyr::select(user_id, value_name, skills) %>% 
      dplyr::left_join(VALUE_DF, by = 'value_name') %>% 
      dplyr::group_by(value_rank, skills) %>%
      dplyr::summarise(count = n()) %>%
      dplyr::arrange(desc(count))
  )
}

#' TF-IDFを計算したカラムを追加する
#' 
#' @param df スキル・最高額ごとに人数を集計したデータ
#' @return TF-IDFの結果を追加したデータフレーム
mutateTfIdf <- function(df) {
  return (
    dplyr::inner_join(df, mutateTf(df), by = c('value_rank', 'skills')) %>% 
      dplyr::inner_join(mutateIdf(df), by = 'skills') %>% 
      dplyr::mutate(tfidf = tf * idf) %>% 
      dplyr::select(value_rank, skills, tfidf, count)
  )
}

#' TFを計算したカラムを追加する
#' 
#' @param df スキル・最高額ごとに人数を集計したデータ
#' @return TFの結果を追加したデータフレーム
mutateTf <- function(df) {
  sigma_df <- df %>% 
    dplyr::group_by(value_rank) %>% 
    dplyr::summarise(denominator = sum(count))
  return (
    df %>%
      dplyr::inner_join(sigma_df, by = 'value_rank') %>% 
      dplyr::mutate(tf = count / denominator) %>% 
      dplyr::select(value_rank, skills, tf)
  )
}

#' IDFを計算したカラムを追加する
#' 
#' @param df スキル・最高額ごとに人数を集計したデータ
#' @return IDFの結果を追加したデータフレーム
mutateIdf <- function(df) {
  N <- df %>% dplyr::distinct(value_rank) %>% nrow()
  return (
    df %>% 
      dplyr::group_by(skills) %>%
      dplyr::summarise(df = n()) %>% 
      dplyr::mutate(idf = log(N/df) + 1) %>% 
      dplyr::select(skills, idf)
  )
}

「レジェンド級」は現時点で1人しかいらっしゃらなかったので、「ゴッド級」から集計してみます。これも5人なので、有益な分析ができるかどうかはやや疑問ですが。

# ゴッド級 (value_rank == 2) のTF-IDF上位10件のスキルを表示する
readSkillsIncomeDf() %>%
  mutateTfIdf() %>%
  dplyr::filter(value_rank == 2) %>%
  dplyr::arrange(desc(tfidf)) %>% 
  head(10)

その結果がこちらです。

   value_rank               skills      tfidf count
        <int>                <chr>      <dbl> <int>
1           2                  AWS 0.05584600     3
2           2         DigitalOcean 0.04751468     1
3           2 elasticsearch-kibana 0.04751468     1
4           2            ExoPlayer 0.04751468     1
5           2                  kms 0.04751468     1
6           2              Posgres 0.04751468     1
7           2               Presto 0.04751468     1
8           2              reactjs 0.04751468     1
9           2       マネージメント 0.04751468     1
10          2                 採用 0.04751468     1

どうやら、他の方が書いていないスキルを持った方がいて、人数(count)が1人のスキルが上位に表示されているようです(ただし、この結果も有益だと思います)。「人数2人以上」の条件を足して集計し直しましょう。

# count >= 2の条件を足した集計
readSkillsIncomeDf() %>%
  mutateTfIdf() %>%
  dplyr::filter(value_rank == 2, count >= 2) %>%
  dplyr::arrange(desc(tfidf)) %>% 
  head(10)

こうなりました。

  value_rank        skills      tfidf count
       <int>         <chr>      <dbl> <int>
1          2           AWS 0.05584600     3
2          2     AngualrJS 0.04311201     2
3          2      CircleCI 0.03723067     2
4          2       fluentd 0.03723067     2
5          2          Java 0.03723067     2
6          2         nginx 0.03723067     2
7          2         Redis 0.03723067     2
8          2 Ruby on Rails 0.03723067     2
9          2          Ruby 0.03225806     2

同様に、ウィザード級、スター級、プチリッチ級、ノーマル級、未入札の結果も出しておきます。ただし、未入札の場合は「スキルが評価されていないケース」の他に、少なくとも「(WEB系の企業が多いために)スキルがマッチしていないケース」も含まれているように思います。

# ウィザード級に特有なスキル
   value_rank        skills      tfidf count
        <int>         <chr>      <dbl> <int>
1           3         MySQL 0.03394561    11
2           3    JavaScript 0.03216110     9
3           3       Jenkins 0.02468772     8
4           3           AWS 0.02160175     7
5           3          Java 0.02160175     7
6           3 Ruby on Rails 0.02160175     7
7           3      newrelic 0.02085048     5
8           3         Redis 0.01851579     6
9           3    capistrano 0.01786728     5
10          3       Datadog 0.01481790     3

# スター級に特有なスキル
   value_rank     skills      tfidf count
        <int>      <chr>      <dbl> <int>
1           4 JavaScript 0.02994896    16
2           4       Java 0.02424686    15
3           4      MySQL 0.02263041    14
4           4        PHP 0.01939749    12
5           4        AWS 0.01778103    11
6           4        C++ 0.01684629     9
7           4    Android 0.01497448     8
8           4     jQuery 0.01497448     8
9           4      React 0.01497448     8
10          4     docker 0.01454812     9

# プチリッチ級に特有な特有なスキル
   value_rank      skills      tfidf count
        <int>       <chr>      <dbl> <int>
1           5       MySQL 0.02870529    29
2           5  JavaScript 0.02865507    25
3           5        Java 0.02078659    21
4           5         PHP 0.01979675    20
5           5        Ruby 0.01886792    22
6           5         AWS 0.01682724    17
7           5     Jenkins 0.01583740    16
8           5 Objective-C 0.01490063    13
9           5      docker 0.01385773    14
10          5      Apache 0.01375443    12

# ノーマル級に特有なスキル
   value_rank     skills      tfidf count
        <int>      <chr>      <dbl> <int>
1           6 JavaScript 0.03572748    27
2           6      MySQL 0.03085353    27
3           6        PHP 0.02513992    22
4           6        Git 0.02381832    18
5           6     jQuery 0.02381832    18
6           6       Java 0.02056902    18
7           6        AWS 0.01942630    17
8           6    Jenkins 0.01714085    15
9           6     docker 0.01599813    14
10          6       Ruby 0.01584158    16

# 未入札に特有なスキル
   value_rank     skills      tfidf count
        <int>      <chr>      <dbl> <int>
1          NA      MySQL 0.02686086    62
2          NA JavaScript 0.02458226    49
3          NA       Java 0.02166199    50
4          NA     jQuery 0.02006715    40
5          NA        PHP 0.01819607    42
6          NA        AWS 0.01646311    38
7          NA     Apache 0.01555204    31
8          NA        Git 0.01454868    29
9          NA         C# 0.01354533    27
10         NA    Jenkins 0.01213071    28

いかがでしょうか?個人的には「がんばった割に大して面白い結果が出なかったな」という感想です。

入札上位の方のスキルセットを見る限り、ここで出ている技術がその人のコア技術である例は少ないように思います。

そもそも、「入札額が高いエンジニアに特有なスキルがある」という仮説自体がまちがっており、「それなりに評価されているエンジニアは、他にないスキルを持っている」もしくはもう少しエンジニアの区分をつけた上で分析すべき(例えばフロントエンドエンジニアとインフラエンジニアでは求められるスキルは全く違う)なのかもしれません。

または、「ざっくりと入札額の高いエンジニア」と「それ以外」くらいの粗い集計でも良かったのかもしれません。

まとめ

ひとまず、以下の2点は言えると思います。

  1. (今のところ)高い入札額を提示されているのは30代中盤
    • ただし、「年齢が低いと評価されづらい」のか、「年齢を経ることで本当にスキル(もしくはレジュメでの説得力)が上がる」のか、その他の原因なのかは分からない
  2. 「これを身につければ年収上がるべw」という技術は分からなかった

以上です。

【雑記】Haskell(で|と)数学を割と真面目に勉強しようと思います

Python3の型注釈が目指す世界をきちんと理解したいと思い、 2年越しくらいですごいHaskellたのしく学ぼう!をようやく読み終わりました。

型クラスの話あたりから、動的型言語(まともに書ける言語がPython, Ruby, R)にとっては感覚をつかみにくかったのですが、 「型クラスは型がもつ共通の性質をモデリングするためのもの(そしてそれは代数学の概念が元になっているらしい)」という感覚をつかむとと受け入れられました。

Elixirの本に「(関数型)プログラミングはデータの変換をするものだ」という、 関数型プログラミングの特徴を分かりやすく説明した記述があります。 Haskellはそこから一歩進んで、「関数の引数や戻り値がどんな性質を持っているか、つまりどんな型や型クラスなのかを意識しろ」と言っているようです。

話はやや逸れるのですが、数理最適化が好きな同僚(なんかもう後輩って呼べないですw)が「問題を関係者にヒアリングして、プログラミング可能な状態に定式化する」という羨ましい能力を持っているのですが、Haskellを学んでいるうちに、それが数学の鍛錬(もっと具体的には論理と集合で問題を表現すること)によって得られたものなんじゃないかと感じ始めました。

www.orecoli.com

数学にもともと興味があって数学科に進学した学生でも、大学以降で学ぶ数学についていけなくて挫折してしまう人は多いようです。

(中略)

具体的には以下の能力、

  • 証明するべきことを正しく論理式として表現する能力
  • 許された演繹規則のみを用いて、正しい証明を構成する能力

の二つが身についていないと、今からあげる書籍を100回読んでも理解できるようにはならないと思います。

というわけで今は「論理と集合」を学ぶために、本屋で見かけて良さそうだったこの本を読んでいます。 写像の記述方法がHaskellの関数と型注釈そのものでビックリだし、Haskellを学んだ後だと数式がプログラミング言語にも見えます。

論理と集合から始める数学の基礎

論理と集合から始める数学の基礎

また、Haskellで実用的なプログラムも書いてみたいです。この記事とか参考になりそうですが、過去の資産もあるので今はPythonで書いてしまっています。

syocy.hatenablog.com

純粋関数型データ構造

純粋関数型データ構造

「何の役に立つのか」という応用先を考えずに勉強しちゃってますが、仕事で必要な勉強とは別に身につけようと思います。 少なくとも、汎用的に役に立つ分野ではあるはずなので。

とりあえず、『達人プログラマー』的には2017年は数学の基礎とHaskellを学ぶ年にします。

【R】Yコンビネータ(不動点コンビネータ)をRで写経する

最近関数型言語(主にHaskell)で遊んでいます。

その中「ラムダ式の中で再帰を行うことができる」Yコンビネータという概念が出てきたのですが、ちょっと理解できなかったので、Rで実装してみたという記事です。

Pythonによる実装は以下の記事に詳しく解説されているので、これを基に「Rを使ったYコンビネータによる階乗の実装」をRで再実装(写経)してみました。

時間城年代記:PythonによるYコンビネータの仕組みの(多分)わかりやすい説明

解説もしようと思ったのですが、思ったよりエグいのと、元記事が素晴らしいのでそれで済ませます…。

yconv <- function(f) {
  (function(x) {
    function(m) {
      f(x(x))(m)
    }
  })(
    function(x) {
      function(m) {
        f(x(x))(m)
      }
    }
  )
}

func <- function(f) {
  function(n) {
    if(n < 2) {
      n
    } else {
      f(n - 1) * n
    }
  }
}

yconv(func)(5)
# => 120

ところで、Rでは当然関数での再帰は使えますし、再帰専用のRecallという関数も用意されているので、Yコンビネータを使う必要はありません。

Wikipediaの不動点コンビネータの記事にも、

不動点コンビネータにより、第一級関数をサポートしているプログラミング言語において、明示的に再帰を書かずに再帰を実現する為に用いる事ができる。なお、一般にそういった言語では普通に再帰が使えるので、プログラミングにおいてはパズル的なテクニック以上の意味は無い。

とあります。ラムダ計算の理論では重要らしいですが。

【雑記】データエンジニア・サバイバルガイド

この間、久しぶりにある優秀な先輩に会い、いろいろと話をする機会がありました。

以前短い期間でしたが一緒に仕事をする機会があり、その際に基本的な統計プログラミングや仕事で気をつけるべき点(闇)についていろいろ教えていただいた方です。「お前はもう少しいろいろできるはずだから気張れ」とケツを叩かれつつ、今の仕事について考えていることを共有していただけました。

(そのとき食べていたのがおいしい中華料理で、妙に辛い肉と紹興酒のせいで次の日お腹を壊してしまいました。)

特にデータ分析者やエンジニアに限らない話も多いですが、そこで話した内容を自分の解釈でまとめておきます。

自分のアウトプットに責任を持たない人を信用するな

企業で働くエンジニアは「どれだけビジネスの役に立ったか」で価値が決まります。

インフラエンジニアなら「可能な限りサービスを止めないこと」、アプリケーションエンジニアなら「新しいサービスを使ってもらうこと」が価値であるのと同じように、データに関わるエンジニアなら「データを正しく活用してビジネスを加速させること」が価値であって、「データ集計・分析を行うこと」で終わりはありません。

先輩は「コンサルティング会社に依頼した日次の売上予測のコードが、(精度はそれなりに良いけど)実行に数時間かかって運用できないから結局自分で全部作り直したよHAHAHA」って言ってました。

(逆に言うと、ある人が信頼できるかどうかを判断するには、エンジニア以外でも、「その人のコード(成果)がちゃんと運用されて役に立っているか」を見ればいい気がしています。また、他人からそれを見られていることも逆に意識しないといけないでしょう。)

自分の成果に責任を持たない人(例えばコードを渡してハイ終わりという立場の人)は、そのコードが今後も運用され続けることを考えないため、とりあえず動かすために平気で例外処理を握りつぶしたり、プログラムの可読性や汎用性を考えません。

コードを自分で書け

自分のアウトプットに責任を持つようにすると、BIツールより、汎用的なプログラミング言語やシステムも使いたくなります。

例えば、もっと多くのデータに適用したり、バッチ処理として自動化したくなると、あらかじめパッケージングされているものより、自分で作って応用先を変えられるもののほうが、結果的には細かいニーズに応えられるでしょう。

ただし、それを行うには、重回帰分析を行う際に多重共線性に気をつけつつ、データ抽出のために負荷の小さいSQLを書くよう気を遣いながら、微妙なR言語のプログラムをメモリを食いつぶさないようにリファクタリングしているうちに、なぜかデータ分析用のサーバー運用まで任されるようになってしまうため、より多くの分野を学び続ける必要があります。

そのため、「評価する(上司や会社)側の立場が、エンジニアのスキルの有無や成果を正しく評価するのは難しいだろうな」と思っています。

会社の外と繋がりを持て

それは、この分野のほとんどのエンジニアや分析者にとって今のところ「ロールモデルとなる人がいない」からです(少なくとも日本では)。

ある優秀な同僚は「論文や最新の記事を読めば必要な情報が集まるから外の繋がりはいいや」と言っていましたが、自分が力を入れるべき分野や、自分自身のキャリアを考えるとき、自分に近い分野の人の考え方を知っておく必要があると思います。

また、会社によって前提になっている技術や文化が違う場合も多いです。 (少し)私の今の仕事では、ほとんどの箇所をRubyで書いている(汎用的な一つの言語で統一しようという思想)ので、場所によっては静的型の言語で厳密に書きたいなあと思うこともあります。

ただ、同じような仕事でも、「得意なものを得意な言語に任せる」という思想もありえます(例えば基本的にはJavaで、分析的なコードだけPythonで書くなど)。そういう話を会社の外の人とすると、気付かされることも多いです。

縄張り意識に気をつけろ

「もうちょっとこういうデータの分析や活用ができるんじゃないか」と思うと、どうしても組織の壁や、既にその仕事をやっている人がいるがイケてないもの(例えば自動化できる仕事を自動化できていない、など)にぶつかります。

まずは、ワインバーグの『スーパーエンジニアへの道』に書いてある「問題ない症候群 (No Problem Syndorome)」に陥らないようにしましょう。 これは「対応する問題を深く理解することなく、××という技術さえ導入すれば問題ない」と主張してしまうことです。多かれ少なかれ(自分でも他人でも)身に覚えがあると思います。

要するに、関係部署にヒアリングや、データの調査(例えばログデータが数日間途切れていたのに、誰も気づいていないということも多々あります)の手間を惜しんではいけません。

スーパーエンジニアへの道―技術リーダーシップの人間学

スーパーエンジニアへの道―技術リーダーシップの人間学

たぶん、ほとんどの人は「自分は無能だ/悪人だ」と認識するのを嫌がります。私も嫌で「自分は有能で、他人の役に立っている」と思いたいです。

そのため「××さえ導入すればものすごく楽になるのに、なんでやらないの?バカじゃね?」という態度でいるのはやめましょう。 大抵はどこか間違ってますし、万が一その通りだとしても相手は反発し、自分の仕事の成果が出なくなるでしょう。

組織構造も重要である

基本的に、データ分析を行う人は「ある程度規模が大きくデータが揃っている組織」に必要とされているのに「複数のスキルや複数の部署の仕事に跨った仕事をするベンチャー精神(?)」が必要であるため、ある程度大変なことはすぐにわかると思います。

そのため、組織構造も(おそらく)重要なはずなのですが、「どういうふうにデータ分析チームをマネジメントすればいいか」というノウハウも、ここ1~2年でようやく少しずつ見かけるようになった程度です。(例えば@tokorotenさんの以下の資料などです)

また、その先輩は「いろいろな会社を見たが、データ分析者の立場や仕事のやり方は、最初に分析の基盤やノウハウを作った人の思想を受け継いでいる」と言っていました。

www.slideshare.net

これからの世代は自分より優秀だ

実は、日本の大学に「統計学」専門の学部がなく、データサイエンス(と呼べそうな)分野を専門的に学んだ人はそう多くはありません。 しかし、日本国内でもそういう機関を充実させる動きはあり、例えば滋賀大学が2017年4月から「データサイエンス学部」が産まれます。

(私自身は、コンピュータサイエンスやデータサイエンス(と呼べる分野)を専門的に学んだことがありません。私のように、WEB企業で働いていて、近い仕事をしている人の中にも、今はそういう人が多い気がします。)

diamond.jp

統計学といえば日本では長らく、経済学部や工学部の一部(学科ですらない!)という位置づけにありました。しかし遂に日本で初めて、独立した“統計学部”が登場します。その名も「データサイエンス学部」。

ということを私から見ると一枚も二枚も上手な先輩でも話していました。「海外のエンジニアと比べると、モデル自体の精度みたいなところで勝負するのは難しいから、他の強みを持っておいたほうがいい」とも。

5年後や10年後の未来には、おそらく自分より遥かに優秀な後輩と仕事をすることになるでしょう。そのときにどこで勝負すべきか考えておく必要がありそうです。

【雑記】なぜ「人工知能」という言葉が使われてしまうのか

というバズりそうなタイトルですが、「人工知能」とか「機械学習」のことはあまり書きません。 私も詳しくないので。

そういうのを期待している人は、こっちを読みましょう。

bohemia.hatenablog.com

ある会のとき、上司が同僚2人の成果について「人工知能」という言葉が使われて説明していました。本当は、数学好きな方が使っているものは「数理最適化」で、分析が得意で業界ニュースをひたすら追っている方が使っているものは「統計学」の「重回帰」で、「知能」と呼べる(ぶべき)ものでは無いよなと私は思っていました。

(ただし、その上司も「本人が人工知能だと思っているかは別だけど」という注釈を付けて、よく聞かれる言葉で説明するために、敢えて「人工知能」という言葉を使っているようでした。)

同じようなことが何度かあり、そこでいろいろモヤモヤしていたのですが、根本的に、ビジネス側の人にとっては「(実装者にとって重要な)何を使って/どうやって解決するか」は重要ではなく、「何を解決するか」のほうを重視しているだからだろうと思うようになりました。

フェルディナン・ド・ソシュール - Wikipedia

一方、音韻だけではなく、概念も言語によって区切られている。たとえば、「イヌ」という言葉の概念は、「イヌ」以外のすべての概念(ネコ、ネズミ、太陽、工場、川、地球……)との差異で存立している。このように、人間は、「シーニュ」という「概念の単位」によって、現実世界を切り分けているのである。そして、その切り分け方は、普遍的ではない。たとえば、日本語では虹の色を「七色」に切り分けているが、それを「三色」に切り分ける言語もある。

かっこつけてソシュールを出しときましたが、実装者でなければ、裏で動いている仕組みが重回帰でもディープラーニングでも変わらないわけです。しかし、私も会計と財務と経理の違いが分かっておらず、それらは抽象化して他の部署に任せているので同じようなものです。

要するに、「数値計算を使った難しい技術が発達しているらしい」ということは分かっていて、「それらを包括して任せられる人が欲しい」と考えているようです。ただし、「人工知能」という言葉は「何を解決するか」という部分も曖昧なので、そっちの視点でも充分な言葉じゃないなと思ってしまいますが…。

また、最近飲み会で他部署の後輩と「インフラ苦手なんでどう勉強したらいいのか分からないんですよ」という話を少ししていたのですが、 よくよく聞くと「データベースやログ収集・解析の仕組み」のことを「インフラ」と言っていたので、ちょっとビックリしてしまいました。

私はデータ収集のプログラムを作っていて、私はそういう部分までアプリケーション側の(DevOpsの考え方が普及してきた今、アプリケーション/インフラの対比も既に古臭いかもしれませんが)責務だと考えていたのですが、 彼の触っている会社のメイン事業のコードベースが巨大すぎるので、必要以上に難しくブラックボックスに見えてしまっているのかもしれません。

最近ローレンス・レッシグの本を読んでいて、「アーキテクチャによる規制(要するに、ユーザーに行動を取りやすい/取りづらい仕組みを作ってふるまいを制御すること)」という考え方が書かれていたのですが、同じような現象が古いコードや巨大なシステムのせいで意図せず起こっているのかもしれません。

CODE VERSION2.0

CODE VERSION2.0

結局よくわからない記事になってしまいましたが、

  • 相手の使っている言葉が、何を差しているのか(どの範囲を差しているのか)
  • それは多分、立場とか経歴とか、触れてきたアーキテクチャ(?)によって変わるから、それを確認してちゃんと考えたほうがいい

ってことを考えています。

【R/Python】rChatworkとpychatworkをチャットワークAPIのバージョンアップに対応させました

RとPythonのチャットワーク用ライブラリを、APIのバージョンアップに早めに対応しました。 ライブラリの実装練習に作った割に、なんだかんだ自分でいろいろと利用しているのでビックリです。

help.chatwork.com

v1からv2への移行期間として、下記の停止日まではv1を引き続きご利用いただけます。
停止日:2017年5月上旬予定 ※具体的な停止日は2017年3月下旬頃にご案内予定です。

上記の停止日以降は、v1でのアクセスはできなくなりますので、 チャットワークAPIをご利用中のお客様は、お早めにv2へ移行いただきますよう、お願いいたします。

といっても、個人的に利用している「メッセージの投稿」と「メッセージの取得」しか実装していないので、エンドポイントのURLを変更しただけなんですけどね。

github.com

github.com