歩いたら休め

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

【Python3.6】AWS Lambdaを再現するDocker Imageのdocker-lambdaを使ってみた

この間AWS LambdaでPython 3.6がサポートされたので、レンタルサーバーで動かしているTwitterの自動投稿やクローラーを移行しようと画策しています。

AWS LambdaがPython3.6に対応したので使ってみた | Developers.IO

外部ライブラリ(twitterライブラリやBeautifulSoupなど)を利用使いたいので、zipで固めてデプロイする方法でいきたいのですが、いろいろ面倒そうなのでまずはその辺の煩雑な作業を自動化する手段でやってみました。

いろいろ調べていくうちに、以下の記事に紹介されている、「手元の環境で擬似的にLambdaを実行する」というDockerイメージが良さそうだったので使ってみます。

AWS Lambdaの開発環境を構築~docker-lambdaの紹介~ – クリエイティブ - ブログ - 株式会社テレビ朝日メディアプレックス

今回は Lambdaのデバッグ効率を上げるdocker-lambdaの設定手順を紹介します。

名前からイメージ出来る通り、Dockerで疑似的にLambdaを実行するという物になります。

github.com

ただし、Docker Imageが2GB弱ほどあるので、多少ハードディスクの容量は食ってしまうので、もしローカル環境で使うならば注意が必要かもしれません。

私はMac OS Xを利用しているので、公式サイトの"Docker for Mac"からダウンロードしました。

実は未だにちゃんとDockerを使ったことが無かったのですが、次のサイトを参考に簡単に勉強すれば、問題なく使うことができました。

www.atmarkit.co.jp

docker-lambdaを試しに実行する

lambda_function.pyという名前で現在のこのようなファイルを用意します。

def lambda_handler(event, context):   
    print("value1 = " + event['key1'])
    return event['key1']

公式のExampleにもある通り、引数にeventの値となるjsonを渡して実行します。

$ ls
lambda_function.py
$ docker run -v "$PWD":/var/task lambci/lambda:python3.6 lambda_function.lambda_handler '{"key1": "key2"}'
START RequestId: bcc19516-283b-485d-b728-69497756be19 Version: $LATEST
value1 = key2
END RequestId: bcc19516-283b-485d-b728-69497756be19
REPORT RequestId: bcc19516-283b-485d-b728-69497756be19 Duration: 32 ms Billed Duration: 132.0 ms Memory Size: 1536 MB Max Memory Used: 18 MB
"key2"

docker-lambdaでデプロイパッケージを作成する

次は、デプロイパッケージをdockerを使って作成します。

ローカルの環境(Mac OS X)でも同じことはできるのですが、特にPure Pythonでないライブラリを利用する際にエラーが出てしまうそうなので、できるだけ実際のAWSに近い環境でライブラリが用意できるので変なエラーに詰まることが少なくなることが期待されます。

docs.aws.amazon.com

Lambda 関数を作成するには、最初に Lambda 関数デプロイパッケージ (コードと依存関係で構成される .zip ファイル) を作成します。

以下の記事と、docker-lambdaリポジトリの「Create your own Docker image for finer control:」の項目を参考にして簡単にPython3.6をビルドできるdocker imageを作ります。

qiita.com

デプロイ用のDocker Imageを用意する

以下のようなDockerfileを用意します。

FROM lambci/lambda:build-python3.6

ENV AWS_DEFAULT_REGION us-east-1

ADD . .

CMD pip3 install -r requirements.txt -t /var/task && \
  zip -9 deploy_package.zip lambda_function.py && \
  zip -r9 deploy_package.zip *

mylambdaという名前でイメージを作ります。

docker build -t mylambda .

コードを用意する

作業ディレクトリで、lambda_function.pyとrequirements.txtを用意します(というか、この名前でファイルが存在していることを前提にDockerfileを書いています)。

$ ls
lambda_function.py requirements.txt

ひとまず、著名な外部ライブラリであるrequestsを使ったコードを書いてみます。

ひとまず、lambda_function.pyという名前で、こちらの記事で紹介されているグローバルIPを確認するAPIを呼び出しています。

import requests


def lambda_handler(event, context):
    res = requests.get('http://httpbin.org/ip')
    return res.json()

requirements.txtはこのような内容です。具体的なバージョンは特に指定していません。

requests

Dockerの環境内でzipで固める

次のコードを実行することで、Dockerfileの"RUN"の項目で指定されているシェルスクリプトが動き、deploy_package.zipが吐き出されます。

docker run -v "$PWD":/var/task mylambda

このdeploy_package.zipをアップロードすると、awscli等でアップロードすればLambda上で動かすことができます。

f:id:takeshi0406:20170506144610p:plain

まとめ

docker-lambdaを使ってAWS Lambdaの開発が便利になることを簡単に確認しました。

また、zipファイルとdevを同じディレクトリに保存しているので、何度も試行錯誤を行う際は微妙かもしれないので、適宜書き換えつつ利用しようと思います。

手軽に環境を捨てられるのもDockerの利点だと思うので。

【本】プログラマーは『ものづくりの数学のすすめ』を読んで、のんびり数学を勉強しよう

ものづくりの数学のすすめ 技術革新をリードする現代数学活用法』を読んでみたところ、とても面白かったのでおすすめしますという記事です。

そもそも私がこの本を読んだのは、

  • 著者の方のツイートを見かけて「面白い立場の方だな」と思ったこと
  • 最近私が関数型言語を勉強しており、集合論の言葉を学んだ後に、プログラミングの仕様を簡潔に理解することができるようになったこと

が主な理由です。

特に「ニュートンは議論は現代の物理学者でも難解だが、ライプニッツは運動という現象をシンプルに記述できる微分積分記号を発明した。これによって後世の研究者が、明快に議論できるようになった。企業の研究者はライプニッツを目指そう」という提言が印象的でした。

さらに、「微分という記号や概念を使わずに、放物運動を記述するのはとても困難だ」とまで突っ込んで書かれていました。(私はプログラマーなので、ここでDDD(ドメイン駆動設計)を連想しました。)

また「数学者は論理だけで話を進めがちだが、グラフ化して問題のおおまかな傾向を知ることも大事。それぞれの軸が加算(+)される変数だった場合は線型軸を使うし、乗算(*)される変数だったときは対数軸を使うはずだ」というようなことが書かれていて、ハッとさせられました。今までなんとなく軸を使い分けていたのですが、代数学的な発想で考えると、「この変数にとって本質的な操作は何だ」というようなことまで掴めます。

SOFT SKILLS』的な要素もあり、例えば他にも長いスパン・短いスパンの勉強の方法なども参考になりました。ITエンジニア向けの本だと、どうしても短いスパンの勉強が強調されてしまいがちなので。

素晴らしい本なのですが、人によって刺さる部分は違う気がします。本屋で見かけたときはちょっと立ち読みしてみてください。

ものづくりの数学のすすめ 技術革新をリードする現代数学活用法

ものづくりの数学のすすめ 技術革新をリードする現代数学活用法

余談ですが、最近は『集合・位相入門』を読んで集合論を勉強しています。今のところ「型や合成関数への感覚が深まって、コードがちょっときれいになった気がする」くらいの効果しか現れていませんが…。

また、このペースでいくと、線型代数まで学び直すのに2年くらいかかるんじゃないかと思ってます😩

集合・位相入門

集合・位相入門

以前も書いたとおり、こちらの記事に従って、少なくとも大学の学部生レベルまでは数学の能力を付けてみようと思います。

www.orecoli.com

また、『ものづくりの数学のすすめ 技術革新をリードする現代数学活用法』と同じ著者の前著も買ってみました。

線型代数学周遊―応用をめざして

線型代数学周遊―応用をめざして

まだ全然読めていませんが、第1章には「数学は言葉」であり、非数学な研究は「言葉にならないものを言葉にしてゆく科学の話」で、数学は「言葉になった後の科学の話」で、発想が全く異なるということが書かれていました。「ルールを守ってルール内のゲームを楽しむ」「定義を信じて進むうちにその定義の自然さを体感する」ということが書かれていました。

最近、ようやくそれが分かってきた気がします(が、数学を学んでも即座に役に立つ分野ではないので、目先で何を勉強すればいいのかということも同時に悩んでいます)。

【ニュース】「ブロックチェーン技術と不動産」について、有識者に質問してきてほしい

最近、「bitFlyerと積水ハウスがブロックチェーン技術を利用した不動産情報管理システムの構築を開始する」といったニュースが話題になっています。

jp.techcrunch.com

「ブロックチェーンを不動産に適用するとイケるんじゃないか」ということは以前から言われているようで、例えば以下のようなブログが見つかりました。

www.bankerfintech.com

冒頭の記事は、ブロックチェーン技術を用いれば、取引の透明性を高め、詐欺のリスクを減らし、売買に伴う煩雑なプロセスが簡素化され、取引のスピードがアップするというような内容が書かれています。

ちなみに、所有権の移転もブロックチェーン上に刻み込まれるようになるので、法務局も不要になるのかもしれません。

ついでに、こんなツイートも見つけました。「音楽ストリーミング」は確かに相性良さそうですね。

ところが、この話を上司としたところ、

「ノードを保有してるのって、企業なの?自治体なの?良からぬノードが過半数を超えてると改竄できちゃうはずだから、その辺がどう対応してるのか知りたい」

というコメントが返ってきました。しかし、私もブロックチェーン技術を「なんか分散してるもの」くらいしか理解していないため、答えられませんでした。

ブロックチェーン技術は、自社オリジナルのシステムに依存することなく、情報管理フォーマットが共通化し易いことが特性の一つであり、賃貸管理を行う不動産関連企業や自主管理の事業主が、当該プラットフォームへの相乗りが容易となる技術です。このたび構築を開始する業界初のブロックチェーン技術を 活用した不動産情報管理システムが、将来的には日本の不動産業界のネットワークを繋げる標準プラットフォームとなり、確立した不動産業界コンソーシアムとなるよう、住宅業界のリーディングカンパニーとして推進して参ります。

少なくとも、「他の会社が相乗りしやすい」ということまでしか書かれていません。

積水ハウスのIR情報も確認し、「ブロックチェーン技術を活用した不動産情報管理システムの構築を開始」という項目はあったのですが、その辺の定義がどうなっているのか分かりません。

ところで、こんなセミナーが無料で開催されるので気になる(特に上記の件を講師の方に質問してみたい)のですが、他に用事があって行けそうにありません。

peatix.com

もし興味ある知り合いがいたら、参加してどんな内容だったかを教えてほしいです。

【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の不動点コンビネータの記事にも、

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

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