競馬必勝本は本当に当たるのかを検証!〜Pythonで実装する馬券自動選択ツール〜

こんにちは、サーバーサイドエンジニアの竹本です。 この記事は Enigmo Advent Calendar 2020 の3日目の記事です。

みなさまは2020年に買った中でよかったものはなんでしょう?

私はiPadです。

最新 Apple iPad Pro (12.9インチ, Wi-Fi, 128GB) - シルバー (第4世代)

最新 Apple iPad Pro (12.9インチ, Wi-Fi, 128GB) - シルバー (第4世代)

  • 発売日: 2020/03/25
  • メディア: Personal Computers

主にkindleを見開きで読むことに活用しています。

エニグモの福利厚生の一つ「エンジニアサポート」で5万円の補助を受けました。わーい。 https://enigmo.co.jp/recruit/culture/

そしてみなさまは馬券、買っていますか?
馬券は競馬に賭ける際に購入する投票券です。
1口100円から、ネットでも気軽に購入することができます。(競馬は20歳から)
弊社にも数名競馬好きが在籍しており、時折競馬トークで盛り上がることもあります。

「競馬必勝本」は本当に当たるのか?

「競馬必勝本」というものが巷にはあります。
こういった本では勝った時の馬券と払戻金だけセンセーショナルに紹介されていることが多く、 実際どのくらい賭けてどのくらい当たっているのかわからないことが多いです。
なので実際に本の通りに馬券を購入していたらどのくらい勝つのかを検証してみます。

今回参考にするのは「競馬力を上げる馬券統計学の教科書」です。

競馬力を上げる馬券統計学の教科書

競馬力を上げる馬券統計学の教科書

世にある競馬必勝本の中でも、オッズのデータから勝ち馬を選ぶという、タイトルの通り統計的な要素の多い本になっています。
また馬やジョッキーに関係なくオッズのみを参考にしているので、さまざまなレースがあるなかで汎用的に活用できるという利点があります。

この本に書いてあることをざっくりまとめると以下のようになります。

  • 万馬券を狙え
  • 的中率ではなく回収率にこだわれ

3000円分の馬券を書い続けて3回に1回10000円が当たれば1000円プラスということですね。つまり当たれば万馬券になるようなレース、「穴馬」が勝ち馬にいる馬券を買う必要があります。

馬連オッズの壁」の法則で穴馬を選ぼう

本書の中で最もシンプルな穴馬選択方法として紹介されているのが「馬連オッズの壁」の法則です。 (馬連とは上位2頭を当てる馬券のこと)

この法則を簡単に紹介すると

  • レース
    • 14頭以上出走する
    • 馬連1位人気オッズが9倍以上
    • 単勝オッズ30倍以内の馬が10頭以上
    • 馬連オッズ1位人気馬に単勝1位人気馬が含まれる
  • 穴馬
    • 単勝1位人気馬の馬連オッズを人気順に並べた時1.8倍以上の壁がある時、その壁の前2頭を選ぶ
  • 馬券の組み立て方

本書ではさまざまな条件を組み合わせて馬券を選択していますが、 今回は実装の簡便さのため条件も簡略化していることをご了承ください。

実際のコード

今回はGoogle Colabratoryを利用しました。 Googleアカウントがあれば環境構築もなしにpythonが使えて便利ですね。 https://colab.research.google.com/

2020/11/27現在動くことを確認しているのでみなさまもぜひ使ってみてください。

まず必要なライブラリのインストールします

!apt-get update
!apt install chromium-chromedriver
!cp /usr/lib/chromium-browser/chromedriver /usr/bin
!pip install selenium
!pip install lxml

importします

import pandas as pd
from bs4 import BeautifulSoup
import urllib.request as req
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import numpy as np
import urllib.parse

今回用意した関数

def set_selenium():
  options = webdriver.ChromeOptions()
  options.add_argument('--headless')
  options.add_argument('--no-sandbox')
  options.add_argument('--disable-dev-shm-usage')
  driver = webdriver.Chrome('chromedriver',options=options)
  driver.implicitly_wait(15)
  return driver

def get_raceids(date):
  url = "https://race.netkeiba.com/top/race_list_sub.html?kaisai_date=" + date
  res = req.urlopen(url)
  racesoup = BeautifulSoup(res, "html.parser")
  racelist = racesoup.select("#RaceTopRace > div > dl > dd > ul > li > a:nth-of-type(1)")
  raceids = [ urllib.parse.parse_qs(urllib.parse.urlparse(race.get('href')).query)['race_id'][0] for race in racelist ]
  return raceids

def get_tansho_ichiban(raceid):
  driver = set_selenium()
  driver.get("https://race.netkeiba.com/odds/index.html?type=b1&race_id="+raceid+"&rf=shutuba_submenu")
  html = driver.page_source.encode('utf-8')
  tanhukusoup = BeautifulSoup(html, "html.parser")
  tanhukudfs = pd.read_html(str(tanhukusoup.html))
  tansho_ichiban = tanhukudfs[0][tanhukudfs[0]['オッズ'] == tanhukudfs[0]['オッズ'].min()]['馬番'].values[0]
  return tansho_ichiban, tanhukudfs[0]

def get_umarenodds(raceid):
  driver = set_selenium()
  driver.get("https://race.netkeiba.com/odds/index.html?type=b4&race_id="+raceid+"&housiki=c0&rf=shutuba_submenu")
  html = driver.page_source.encode('utf-8')
  soup = BeautifulSoup(html, "html.parser")
  dfs = pd.read_html(str(soup.html))
  umarendf = pd.DataFrame(index=[1])

  for i, df in enumerate(dfs):
    umarendf = pd.concat([umarendf, df.set_index(str(i+1)).dropna(how='all', axis=1)], axis=1)

  if umarendf.isin(['取消']).values.any() | umarendf.isin(['除外']).values.any():
    return False

  umarendf[umarendf.index.max()]=0
  umarenodds = pd.DataFrame(umarendf.fillna(0).astype('float64').values + umarendf.astype('float64').fillna(0).values.T, columns=list(map(int,map(float,umarendf.columns))), index=umarendf.index).replace(0,np.nan)
  return umarenodds

def get_baken(raceid):
  tansho_ichiban, tanhukudf = get_tansho_ichiban(raceid)
  umarenodds = get_umarenodds(raceid)
  if umarenodds is False:
    return False
  
  umarenninki = umarenodds.min()
  umaren_ichiban = umarenninki[umarenninki == umarenninki.min()].index

  if umarenninki.min() >= 9 and any(umaren_ichiban == tansho_ichiban) and umarenninki.index.max() >= 14 and sum(tanhukudf["オッズ"]<=30)>=10:
    ninkiuma = umarenodds[tansho_ichiban].sort_values()

    anaumalist = []
    for idx in np.where((ninkiuma/ninkiuma.shift(1) > 1.8).values == True)[0]:
      two = idx -1
      anaumalist = anaumalist + (ninkiuma/ninkiuma.shift(1) > 1.8).index.values[two:two+2].tolist()
    if not anaumalist:
      return False

    formation1 = ninkiuma.fillna(0).sort_values()[0:4].index.values
    formation2 = ninkiuma.fillna(0).sort_values()[4:8].index.values
    return {'anauma':anaumalist, 'formation1':formation1, 'formation2':formation2}

  return False

def get_dayresult(date):
  kakekin = 0
  modorikin = 0

  raceids = get_raceids(date)
  for raceid in raceids:
    baken = get_baken(raceid)
    if not baken:
      continue
    result = pd.read_html("https://race.netkeiba.com/race/result.html?race_id="+raceid)
    sanrenpuku = list(map(int,result[2].set_index(0)[1]['3連複'].split()))
    money = int(result[2].set_index(0)[2]['3連複'].replace(',','').replace('円',''))

    if bool(set(sanrenpuku) & set(baken['formation1'])) & bool(set(sanrenpuku) & set(baken['formation2'])) & bool(set(sanrenpuku) & set(baken['anauma'])):
      kakekin += 100*len(baken['formation1'])*len(baken['formation2'])*len(baken['anauma'])
      modorikin += money
    else:
      kakekin += 100*len(baken['formation1'])*len(baken['formation2'])*len(baken['anauma'])
  
  cols = ["賭け金","払戻金"]
  return pd.Series([kakekin,modorikin],index=cols,name=date)

netkeibaからSelenium + BeautifulSoupでオッズのデータをスクレイピングします。
その結果をpandasで前処理し、上記の馬連オッズの壁」の法則から馬券を選択
選択した馬券が当たっているのかを検証します。

回収率にこだわるのが本書の方針なので、検証項目として

  • 条件に合致した全ての三連複馬券を100円で購入 = 賭け金
  • 当たったレースの実際の払戻金

以上の差額を見ることにします。

今回は11月の1~23日までのレースを検証します。

# 開催日のリスト
datelist = [
            '20201101',
            '20201107',
            '20201108',
            '20201114',
            '20201115',
            '20201121',
            '20201122',
            '20201123'
]

moukaridf = pd.DataFrame()
for date in datelist:
  onedaydf = get_dayresult(date)
  moukaridf = moukaridf.append(onedaydf)

時間かかりますが待ちましょう

結果

moukaridf.sum()['払戻金'] - moukaridf.sum()['賭け金']
# 出力
-14630.0

金額としては14630円負けてしまいました。

moukaridf
払戻金 賭け金
20201101 33500.0 12800.0
20201107 0.0 9600.0
20201108 5560.0 16000.0
20201114 0.0 25600.0
20201115 45510.0 25600.0
20201121 0.0 0.0
20201122 0.0 9600.0
20201123 0.0 0.0

日別に見ると勝っている日もありますね。

本書では回収率を上げるための馬券選択方法がさらに細かく紹介されていたので、その通りに実装すればもっと良い結果となるかもしれません。

t検定をすると、「賭け金、払戻金の差額の平均が0ではない(正負どちらかに傾く)」という帰無仮説が棄却されます。(p > 0.05 平均 -1828.8 ± 14776 円)

sagaku =  moukaridf[['払戻金']].values - moukaridf[['賭け金']].values
# 平均
print(sagaku.mean())
# 標準偏差
print(sagaku.std())
# 出力
-1828.75
14776.743076114573

よって結論は「勝つこともあれば負けることもある!」

馬券を買おう

ではせっかく作ったので実際に賭けてみようと思います!
検証では最終オッズから馬券を選択していましたが、当日は10:30のオッズを元に馬券を選択します。
予算の関係で条件に合致した全ての馬券ではなく、1レース選択してフォーメーション3連複馬券を購入します。
11/29の10:30時点で候補が4レースありました。

date = "20201129"
raceids = get_raceids(date)
for raceid in raceids:
  baken = get_baken(raceid)
  if baken:
    print('https://race.netkeiba.com/race/shutuba.html?race_id='+raceid)
    print(baken)
    print("=======================")

 

# 出力
# 条件に合致したレースのURLと購入すべき馬券がプリントされる
https://race.netkeiba.com/race/shutuba.html?race_id=202005050907
{'anauma': [4, 16], 'formation1': array([7, 6, 3, 2]), 'formation2': array([10, 14, 15,  8])}
============
https://race.netkeiba.com/race/shutuba.html?race_id=202005050911
{'anauma': [11, 6, 1, 16], 'formation1': array([ 4, 14,  9, 13]), 'formation2': array([12,  5,  2,  3])}
============
https://race.netkeiba.com/race/shutuba.html?race_id=202009050904
{'anauma': [3, 7], 'formation1': array([ 8, 15,  5,  1]), 'formation2': array([16,  4, 13, 12])}
============
https://race.netkeiba.com/race/shutuba.html?race_id=202009050912
{'anauma': [7, 6], 'formation1': array([13,  3, 10, 11]), 'formation2': array([ 2,  9, 15,  5])}
============

今回は東京7Rに賭けます! (出力で一番上のレース)

穴馬が「4,16」1~4位が「7, 6, 3, 2」,5~8位が「10, 14, 15, 8」です。

f:id:enigmo7:20201130123855p:plain 2020年11月29日アクセス
https://www.ipat.jra.go.jp/

頼むぞ〜!

結果は…

1位 2番
2位 10番
3位 8番

3歳以上2勝クラス 結果・払戻 | 2020年11月29日 東京7R レース情報(JRA) - netkeiba.com

残念!穴馬候補だった4番と16番が3位以内に入りませんでした。惜しかったですね。
それではみなさまも良い競馬ライフをお送りください。

スクレイピング、クローリングはアクセス先に配慮してやりましょう。

参考資料

競馬力を上げる馬券統計学の教科書

増補改訂Pythonによるスクレイピング&機械学習 開発テクニック

ColaboratoryでSeleniumが使えた:JavaScriptで生成されるページも簡単スクレイピング - Qiita

明日の記事の担当はサーバーサイドエンジニアの寺田さんです。お楽しみに。


株式会社エニグモ 正社員の求人一覧

hrmos.co

デザインツールをXd→Figmaへした話 / プロトタイプ作るようになった話

こんにちは、デザイナーの本田です。こちらは Enigmo Advent Calendar 2020 の2日目の記事です。

今年でエニグモへ入社して3年目。昨年末からUXデザイングループへ異動し、BUYMAのアプリケーション UIデザインがメインになったので、考えることややることが大きく変わった1年だったと感じています。 業務の幅も広がって何を書こうか迷うのですが、今年を振り返ると今までで1番デザインツールと向き合っていたし、学んだことも多かったので

  1. デザインツールをXd→Figmaへした話
  2. プロトタイプ作るようになった話

の2つについて書こうと思います。

1. デザインツールをXd→Figmaへした話

デザインデータ引き継ぎの関係上、Adobe Xdをメインで使っていたのですが、今年の7月頃からFigmaへ移行しました。 Xdも使いやすいですし、何よりAdobe Creative Cloud ライブラリが使えるので既に登録しているUIパーツはそのまま引っ張ってこれるのでよかったのですが 半年ほどXdを使ってみて、オンラインでの共有がやっぱり使いづらかったのと、社内でFigmaを使っている方が何人かいて、使いやすいよ!と言っていただけたのが移行するきっかけになりました。

使いづらかった点を軽くまとめると以下の3つになります。

  • プロジェクトメンバーで無料プランの方がいるときに共有の仕方を気をつけなければならない
  • クラウドドキュメントとオフラインドキュメントのファイル管理がどこに置いたかごちゃごちゃになる
  • チーム全体のクラウドドキュメントが見づらい

頑張れば全然使える範囲ではあったのですが、Figmaに移行してこの3つの課題はすべて解消されたので、移行してよかったなと思っています。

秋頃から有料プランのProfessional Teamにしていただいたので、アプリのUIどうなってたかな?と思ったときにすぐ確認できるようになったし、 Figma上でコメントしたらプロジェクトごとにslackへ通知がいくように設定したので、エンジニアやPMの方にもFigmaの機能使っていただいています。(使いこなしていただいてありがとうございます!)

今後もリモートワーク が続くことを考えると、私個人はFigmaをメインにしていくことになりそうです。 Xdのデザインデータが残っているプロジェクトのときはXdを使う、など今後も臨機応変に対応できたらなと思っております。

※でも2年前はSketch使ってたし、1年前はXdでいくぞ!!と思っていたので1年ごとに変わる説はあります。またFigmaに代わるサービスが出たときは時代の流れに合わせていきます。 ※SketchとZeplinは解約していただきました。

2. プロトタイプ作るようになった話

昨年までは簡単なLPやUIを作ることが多かったので、プロトタイプをあまり作っていなかったのですが、 プロジェクトメンバーが多いときや長期のプロジェクトになると、認識合わせや動きの確認がイメージしづらいので、プロジェクトによってはプロトタイプをつくるようになりました。

とりあえずすべてプロトタイプまで作ることにすると時間がもったいないなと感じたので、以下の4つに当てはまるときはプロトタイプを作成するようにしています。

  1. モーダルやトーストが表示されるとき
  2. ステータスごとにボタンの色やエラー文が変わるとき
  3. 3画面以上 遷移するとき
  4. スクロールしても画面上部や下部で固定するエリアがあるとき
  5. アプリでもWebViewで表示するとき

1.2.3. はチームメンバーとの認識合わせがしやすくなるのと、実機で確認したときにユーザーのことをイメージしやすくなる気がしてます。 4.5. はスマホのスクロールエリアが確保できているかだったり、不具合がないか確認するためにやっています。

基本的にツールはFigmaで作りますが、細かな動きも考慮してデザインしたいときはProtoPieを使っています。 今年はスマホハンバーガーメニュー内の動きを作ったのと、トーストの表示タイミングの認識すり合わせをしたいときの計2回ほど作成しました。

f:id:enigmo7:20201201225832p:plain 10月にリリースしたスマホハンバーガーメニューのProtoPie制作画面

Figmaだと細かい動きはできないことがあるので、ProtoPieも併用していけたらなと思っています。

最後に

今思えば異動してすぐにデザインツールをFigmaにしてもよかったし、すべてのプロジェクトでとりあえずプロトタイプ作ってもよかったな〜というのが正直な感想です。 ただ、リモートワーク 下においても半年ほど現状のツールでなんとかできないか試行錯誤した上で、どうしても譲れないポイントを自分の中で見つけられたのは大きな収穫だったかなと思っています。

あとはFigmaもProtoPieもアプリデザイナーの先輩方が勉強会を開いてくださって、わからないことがあったらいつでも質問していいよ〜とやさしく声をかけていただいたのもうれしかったです。ありがとうございます! 来年も今年学んだことをベースにさらにいろんなことに挑戦できたらいいな。

最後まで読んでいただきありがとうございました。

明日の記事の担当は、エンジニアの竹本さんです。お楽しみに。


株式会社エニグモ 正社員の求人一覧

hrmos.co

Cloud Run 使ってみた

こんにちは。サーバーサイドエンジニアの平井です。

今年もあと1ヶ月ですね。リモートワーク中心の生活スタイルに変わり、より一層時が過ぎるのを速く感じています。

もう年末ということで、弊社では今年もAdvent Calendarを開催します!!

題して、Enigmo Advent Calendar 2020です!!

記念すべき1日目は、私、平井の「Cloud Run 使ってみた」になります。 プロジェクトで簡単なAPIをCloud Run(フルマネージド)上に実装したので、それについて話したいと思います。

構成

会員毎にパーソナライズされたコンテンツ情報を返すAPIをCloud Runを使って実装しました。 とてもシンプルですが、構成図は以下のようになります。

f:id:enigmo7:20201124163920p:plain Cloud Run上にRuby on RailsAPIを実装し、Datastoreに格納されている情報を取得します。 Datastoreには、会員毎のコンテンツ情報が格納されていて、この情報はBigquery上に格納されている機械学習のデータを元に生成されています。 そして、クライアントからAPIにアクセスして情報を表示させています。

Cloud Run(フルマネージド)について

Knativeを基盤として構築されたGCPのサーバーレスサービスの一つで、コンテナをサーバーレスで実行してくれます。 スケーリングなども自動で行われるため、開発者はアプリケーションの開発に専念することができます。

Cloud Runには二種類あり、Googleが管理するKubernetes環境で動作するCloud Runと自身の管理するGKEで動作するCloud Run for Anthosがあります。 今回は、より簡単に利用できるCloud Runを使いました。

準備

Dockerfile

Cloud Run上で動かすコンテナを用意する必要があるので、 Dockerfileを用意します。

cloudbuild yaml

Cloud Runへのデプロイ関連のタスクはcloudbuild.yamlで管理しました。 公式ドキュメントにも記載されているやり方と同じです。 ファイルの中身は、コンテナイメージのbuildとCloud Runへのデプロイなどのタスクが記載されていて、 実際の内容とは違いますが、以下のような感じになります。

steps:
# コンテナイメージのビルド
- name: 'gcr.io/cloud-builders/docker'
  args: ['build', '-t', 'gcr.io/PROJECT_ID/IMAGE', '.']
# コンテナレジストリーにpush
- name: 'gcr.io/cloud-builders/docker'
  args: ['push', 'gcr.io/PROJECT_ID/IMAGE']
# コンテナレジストリー上のイメージを使って、Cloud Runにコンテナをデプロイ。
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
  entrypoint: gcloud
  args: ['run', 'deploy', 'SERVICE-NAME', '--image', 'gcr.io/PROJECT_ID/IMAGE', '--region', 'REGION', '--platform', 'managed']
images:
- gcr.io/PROJECT_ID/IMAGE

cloudbuild.yamlを作成したディレクトリによりますが、

gcloud builds submit

を実行するとファイルに書かれたタスクが実行され、Cloud Run上にサービスがデプロイされます。

基本的には、Cloud Run上でコンテナを起動するために必要な準備は以上になります。

その他

ドメイン

Cloud Runでサービスを作成すると自動でデフォルトのドメインが発行されます。 ただ、実際のサービスで利用する際にはカスタムのドメインを設定したくなると思います。 Cloud Runにはカスタムドメインのマッピング機能が実装されているので、使いたいドメインマッピングすることができます。 実際にカスタムドメインを設定してみましたが、ドキュメントを読んで簡単に設定することができました。

GCPサービスとの連携

APIのデータ取得先として、Datastoreを使用しています。 今回は、Cloud Runのサービス とDatastoreのデータが同じプロジェクト上で管理されていたので、二つのサービスが連携する際に、認証の設定は必要ありませんでした。

感想

実際にCloud Runを使ってみた感想は以下です。

  • インフラ関連を意識せず開発できて、アプリケーションの実装に集中できた。
  • デフォルトのメトリクスが充実していた。
    • リクエスト数、コンテナのメモリ使用率
  • ログも充実していた。
    • キーワード検索、 重要度によるフィルタリング
  • GCPサービスとの連携が楽だった。

最後に

今回のプロジェクトの方針で、なるべく早くユーザーの行動をみたいという背景もあり、Cloud Run上にAPIだけを実装するという最低限の選択をしました。 今後の長期運用を考えて、機械学習のデータからAPI用のデータを生成する部分もCloud Run上に乗っけていこうと考えています。

最後まで読んで頂き誠にありがとうございました。

明日の記事の担当は、UXDグループの本田さんです!!


株式会社エニグモ 正社員の求人一覧

hrmos.co

男性インフラエンジニアが育児休業を取得した話

こんにちは。インフラグループの夏目です。

前回のエントリーより3ヶ月、エントリー投稿から2週間ほどで無事子供が誕生し育休期間を経て先日復職しました。

今回のエントリーでは実際に育休を取得してみてどうだったのか?といった内容を書いていきます。開発者ブログというお題目にも関わらず育休話が続いてしまい恐縮ですがお付き合いください。

f:id:enigmo7:20201001224429p:plain

育休っていつから取るの?

いざ育休を取るぞ!と意気込んで最初に感じたのが「いつから育休を取ればいいの?」という、開始時期についての疑問でした。

女性の場合出産予定日の1ヶ月少々前から産前休業に入り、予定日が多少前後しても産後休業を経て育児休業、という流れになるのでさほど開始時期について悩む部分は少ないと思うのですが、男性は産前休業がありません。

予定日から育休取得開始ということで社内調整はしていたものの、いざ予定日になっても兆候がなかったためなし崩しに「生まれた翌日から休みます」という本人も周囲もどうしたら良いのかわからない宣言後、1週間ほど後ろ倒して生まれたので育休開始、というスケジュールになってしまいました。

実のところ育児休業の法令上は予定日から取得可能なため、気兼ねなく育休開始としてもまったく問題ないのですが、生まれていないのに休業して日がな一日何をしていたらいいんだ…?という戸惑いがあり、育休に踏み出せませんでした。

ただ、人事総務的な観点からはいたずらに後ろ倒しをすると給与計算などで面倒な調整が発生するような(というか発生した)ので、やることがあろうがなかろうが当初予定日から変更するべきではなかったかな、と反省しています。

なお予定日から育児休業は取得可能なものの、育児休業給付金は出産日から起算となるようなので(詳しくは育児休業に関する各種法律をご参照ください)、そのあたりも踏まえて調整するのがベターだと思われます。

育休ってどれくらい取るの?

いざ育休が始まり、体力がまだ本調子でない妻のサポートをしつつ日々の育児におおわらわになって1ヶ月が経過したあたりで、次は「これ育休期間2ヶ月の予定だったけど全然足りないんじゃないか…?」という疑問が生じました。

新生児期は授乳やおむつの回数が多く手がかかるから、大変な時期を乗り越えれば…と思ったのですが、乳児期になっても手がかかることには変わりなく、大変じゃない時期など存在しません。

たとえば「首が座るまで」といった身体的な発達状況を目安にするのもわかりやすくて良いかなとは思ったのですが、いつ頃にできるようになるかはまちまちで、「復職時期は子供次第です」という状態は自分も周囲もどうしたら良いのかわからなくなってしまいます。

自分の場合は結局1ヶ月半頃に不安を感じた妻から「もっと伸ばせないの?柔軟に調整できるんでしょ?」と聞かれたものの「いや2ヶ月って一応決めてたし仕事の勘も忘れちゃうから…」とやや強引に押し切って当初予定通り2ヶ月で育休終了としました。

これは振り返ってみれば良い判断ではなかったと反省しています。生まれた時期や家庭環境によってまちまちになるかとは思いますが、極力調整して取れる限り取るのが家庭内平和のためにも最善です。

育休取ってみてどうだった?

当初育休を取るぞ!と決意したときの自分のスタンスは「妻には育児に専念してもらって、他の家事は全部自分が巻き取る」という、振り返ってみれば寝言のような方針だったのですが、当然ながら育児を1人でやりおおせるわけがなく。

2人がかりで育児の合間にそれぞれできる範囲で家事をするという状態がしばらく続き、育休終了1週間前くらいからワンオペ状態を意識して徐々に分担して、という形で慣らし運転をして育休を終えたものの、生活のスタイルについては現在も模索しているところです。

近隣にサポートしてくれる家族や友人がいたり、里帰り出産をするのであれば負担も軽減できるかと思いますが、核家族な我が家の事情と現在の社会状況を鑑みると、育児休業を取って(取れて)本当に良かったなと感じています。

人によっては出産後数日や1週間程度ちょっと手伝う程度というパターンも少なくないとは思うのですが、自分の場合は子供と接している期間が短いといざというときに勝手がわからず、足手まといになったであろうことは想像に難くありません。

また、取得前に予期していた通り…というか予期していた以上に生活のすべてが子供を中心として回るようになり、そのことに慣れるためにも育休期間は大変有用でした。

業務面ではどうだった?

育休取得前のエントリーではカオスエンジニアリングが云々と言っていましたが、メンバーが一人抜けた程度で大きな支障が出るわけもなく、引き継ぎが十分だったかどうかはさておき概ね問題はなかったという認識です。

自身の関わっているプロジェクトがある程度峠を越えたタイミングだったこともありますが、それ以上にタスクを引き継いでくれたチームメンバーがよしなに捌いてくれた部分が大きく、あらためて環境に恵まれているなと思いました。

他方で、リモートワークが定着したことと育児の疲弊からついつい社内Slackを眺めてちょっかいを出してしまうことが二度三度あり、これについては労務管理の観点からは褒められた行為ではないため、業務への未練をすっぱり断ち切る強い心が必要だという学びがありました。

準備は足りた?

以前のエントリでは必要なものをSpreadSheetで管理、TodoをTrelloで管理してこれで十分…と思っていたのですが、準備はまったく足りていなかったため現在進行系で色々と公的な申込み手続きをしたり育児グッズを買い足したりしています。

育児ブログではないので仔細には書きませんが、大雑把に導入して良かったものを以下に挙げます。

導入して良かったもの

  • ベビーモニター
    • 大きな戸建て住宅に住んでいるわけでもなし、狭小マンションで目の届くところにいるから必要ないかと思っていました。しかし、逆に生活圏内に子供がいるからこそ生活音や照明に気を使う日々に疲れ果て、適切な距離を保ちつつ様子を見るためにRaspberryPiでベビーモニターを作りました。
    • RPi Cam Web InterfaceというWebインタフェースを導入したため、市販のベビーモニターと違って専用のモニターユニット不要でPCやスマートフォンでどこからでも子供の様子が見れる、という完璧なソリューションが実現しました。
  • 育児記録アプリ:ぴよログ
    • 授乳時間や睡眠時間などありとあらゆる育児項目を夫婦間で記録共有・グラフで可視化できるのはもちろん、Apple Watchウィジェットからも記録ができるといった便利さで、このアプリなしでの育児は考えられないほどです。

f:id:enigmo7:20201001225224j:plainf:id:enigmo7:20201001225327j:plain
ベビーベッド側面から俯瞰する形でカメラを取り付けています。カメラとは別の位置から赤外線LEDも照射しているため、夜間でも様子が見えて安心です

気軽に育児休業を取ろう

家庭によっていろいろと事情はあるかと思いますが、上に挙げたように社会的な潮流や時勢からもこれまで以上に育休を取ることが推奨され、社会制度も整備されて金銭面の不安も軽減されていくことと思われます。

デメリットよりもメリットが多く、家族で濃密な期間を過ごせるのは間違いないので、ぜひ気軽に育児休業を取得されてみてはいかがでしょうか。

ちなみにこれはまったくの余談なのですが育休の2ヶ月間で3kg痩せました。日々成長するダンベルを使っていると強制的に鍛えられるので健康面でも育休はおすすめです。


株式会社エニグモ 正社員の求人一覧 hrmos.co

男性インフラエンジニアが育児休業取得までにやったこと

こんにちは。インフラグループの夏目です。

今回は技術的な話ではなく、ちょっとプライベートに込み入った話というかエンジニアのワークライフバランス的なお話です。

まったくの私事なんですがこのたび子供を授かることになりまして、ついては育児休業を取ってみようかな?ということで、育休取得までの流れと業務面でどういった対応をしていったのか、といったことを書いていきます。

育児休業を取ってみよう

f:id:enigmo7:20200616134043p:plain

今回育休の取得に至った経緯は色々とあるのですが、ひとつにワークライフバランスを見直す良いきっかけになるかな、という意図があります。

時短勤務やリモートワークではなく一旦完全に業務から離れてみて、日常生活における育児の占める割合はどの程度のものなのか(全部だよという意見はごもっともです)ということを理解しておくべきだろう、と。これを把握せずに、妻が産休・育休で子供の面倒を見るのに対して自分だけこれまでどおりの働き方をしていくのは、先々を考えると良いスタンスではないように思えたため、第一子のタイミングで育休を取ることにしました。

エニグモでは裁量労働のメンバーが多いこともあり、育児都合で勤務時間を調整する様子が日常的に見受けられます。このため、いざ自分に子供が生まれますよということになっても、育休を取得すること自体は自然と受け入れてもらうことができました。

受け入れるも何も労働者の権利の行使なので、本来拒否権はないはずでしょうという話なのですが、そうもいかない会社が多くあることもまた事実です。この点についてはエニグモの風土に助けられた部分が大きいなと感じています。

取得までの流れ

さて、では実際に育休を取りましょうとなってからどういった行動を取ったか、時系列に沿って大雑把ですが書いてみます。

2019年10月に妊娠確定となり、ここから家庭内協議に入りました。自分の稼働状況など鑑みて育休が果たして取れるのか、取れるのであればどれほどの期間で収入面で問題がないのか、などについて妻と喧々諤々やり合いました。

法制度の話をするのであれば1年以上取得することも可能ですが、現実問題として夫婦共に育休となった場合の収入への影響は無視することができず、第一子で不安を抱える妻に多少折れてもらいつつも2ヶ月取得してみましょう、ということになりました。

結論が出たのが11月末、ひとまずチームリーダーへ相談です。自分がアサインされているプロジェクトの今後の見通しや、その他担当タスクのチームメンバーへの委譲可否などを考慮のうえ、積極的に育休を取得してほしいとの言葉を頂きました。チーム内の調整は大丈夫(な状態にするしかない)となったので、後は部長や人事総務の方に掛け合うだけ、というところまで持っていきました。

年が明けて2020年の2月、自身のタスクを育休までにどう調整するかといった大まかな線表を作成して部長へ共有し、社内の具体的な育休取得手続きに入ります。

というタイミングで、全世界的に新型コロナウィルス感染症が広がり、エニグモでもリモートワークに突入し手続きはいったいどうすれば…?という状態になってしまいました。幸い事前手続きはほぼないことと、書類のためにわざわざ出社する必要なく郵送してねということで柔軟な対応をして頂けました。ありがたい限りです。

タスク調整どうしたの

タスク調整は実際問題どのようにしたかという点については特筆すべきことはなく、予定日までに大きなタスクを終わらせつつドキュメントを整理したり、自動化可能なタスクは自動化、間に合わない場合は手順書を作るといった形で粛々と対応を進めました。不穏当な表現ですが、トラック係数やバス係数といったものをあらためて意識することができました。

また、カオスエンジニアリングとまではいきませんが、どれだけ自分が準備をして引き継ぎをしたところで不測の事態は発生しうるので、そのあたりはチームメンバーになんとか拾い上げてもらうしかないかな、と自分勝手なことを考えています。

私生活で準備したこと

育休とはあまり関係なく、プライベートで準備したことについては基本的に世間の皆さんと同じです。強いて言えば、以下のようなエンジニアチックな手法で試行錯誤をしています。

  • 書籍や雑誌などから育児に関する必要なものリストをピックアップしてGoogle SpreadSheetへ書き出し、購入期限や数量、消耗品か否かといったメタ情報を付与して購入状況を管理
  • 公的な手続きなど、忘れがちなものはTrelloのカンバンを作って進捗管理

f:id:enigmo7:20200616134313p:plain
Trelloのボードイメージ

おそらくこういったことに対応するための専用アプリやサービスもあるかと思うのですが、それらの使い方を覚えている暇があったら使い慣れたツールを使ったほうが早いだろう、と横着をしている状況です。

今後の見通し

前述したように雪崩式にリモートワークとなり、当初想定していた育休へのロードマップとは少々異なる現状なのですが、反面リモートワークでも業務への支障はほぼゼロに近いということがわかりました。2ヶ月間の育休取得後に世間がどういった状態になっているのか(おそらくあまり変化はないでしょうが)推測できませんが、リモートワークへの敷居が下がったこともあるので、たとえば保育園に入れるまではセミリモートワークや半育休といった形態で、柔軟な働き方を選ぶこともできれば良いなと思っています。

このエントリーを書いてる今現在、出産まであと数日といった状況です。2ヶ月後にこのエントリーを読み返して「全然準備できてへんやんけ!なに余裕ぶっとるんじゃ!」となりそうな気配が漂っていますが、2ヶ月後に改めて育児休業の間に起きたことや感想についてのエントリー投稿を予定していますので、そちらをお楽しみに。

結びに

少々自己矛盾してしまうのですが、冷静に考えるとこのエントリーのタイトルは筋が悪いなという印象があります。こんなタイトルのエントリーが企業ブログのバリューとならず、掃いて捨てるようなエントリーとして扱われる時代にしていきたいものですね。


株式会社エニグモ 正社員の求人一覧 hrmos.co

GitLabのプロジェクトラベルとグループラベルを重複させてしまって困った話

こんにちは。インフラグループの夏目です。

エニグモではメインのGitサービスとしてGitLabを使ってソースコードを管理しています。 GitLabはGitHubと同様に、IssueやMR(PR)にラベルを付与して作業の優先度やステータスを表すことができるのですが、このラベルの運用でちょっと困ったことが発生して泥臭く解消するはめになったので、経緯と顛末含めてご紹介します。

プロジェクトラベルとグループラベル

GitLabのラベルは、プロジェクト(リポジトリ)とプロジェクトを束ねたグループとでそれぞれ個別に定義できます。 グループラベルとして定義したものは配下のプロジェクトでも使用できるため、基本的にはグループ側で汎用的なラベルを設定して、プロジェクト固有のメタ情報としてプロジェクト名の省略形(例:Enigmo Greatest ProjectEGP)や、リリースバージョン(例:v1.4.3)などをプロジェクトラベルで定義するというのがベターな運用方法だと思われます。

グループラベルを設定しよう

ところがこのグループラベル、エニグモではあまり認識されていなかったため

  1. 各プロジェクトで汎用的な名前のプロジェクトラベルを個別に設定
  2. プロジェクトラベルを使ってIssueやMRを管理
  3. プロジェクトごとに同じラベルを定義するのが手間だったので、プロジェクトラベルと同じ名前のグループラベルを設定

という流れで最近になってようやくグループラベルが設定されました。各プロジェクトは個別にラベルを定義する必要がなくなってめでたしめでたしかと思いきや、そうではありません。さきほどのフローに妙なところがありませんでしたか?

プロジェクトラベルと同じ名前のグループラベルを設定

ここです。同じ名前のラベルを設定しましたね。グループラベルとプロジェクトラベルは別のオブジェクトとして扱われるため、同じ名前のラベルが設定できます。設定できるのは問題ないのですが、いざ使ってみようとすると

f:id:enigmo7:20200522105830p:plain

こんな感じでまったく区別のつかないラベルがラベル付与の候補として表示されるようになってしまいました。どうしてくれるんだ。しかも厄介親切なことにこのラベル候補、グループラベルかプロジェクトラベルかという情報は表示されません。

重複したらどうする?

区別がつかなくても同じ意味合いだったら別にどっちでもいいじゃん、と思わないでもありませんが、ラベルを付与するたびに「なぜ2つあるのか?どちらを付与したらいいのか?表示のバグ?」とチラッと考える時間が無駄ですね。じゃあこの重複を解消しましょう、と迂闊に既存のプロジェクトラベルを削除してしまうと、これまで該当のラベルを付与していたIssueやMRからラベルが削除されてしまいます。

仮にreview requestedなどのアクションが必要なラベルの場合、削除によって対応が漏れてしまうおそれがあります。この事象に対して、GitLab.orgのIssueで対応方法が提案されていました。

  1. 重複しているプロジェクトラベルかグループラベルをリネームする
  2. プロジェクトラベルが付与されたIssueやMRに、グループラベルを追加する
  3. プロジェクトラベルを削除する

たとえばin reviewという名前のプロジェクトラベルとグループラベルが設定されている状態では、以下のような対応になります。

  1. プロジェクトラベル:in reviewin review(project)へリネーム
  2. in review(project)が付与されたIssueやMRに、グループラベル:in reviewを付与
  3. プロジェクトラベル:in review(project)を削除

リネームと削除はグループ設定画面やプロジェクト設定画面から容易に対応できますが、活発に開発が行われているプロジェクトにおいて、Issue一覧やMR一覧で重複しているラベルが付与されているものを探してIssueやMRの編集画面でラベルを設定する、という流れを1つ1つ対応するのはそれこそ時間の無駄です。

APIを使ったラベルの修正

幸い、GitLabではプロジェクトラベルやグループラベル、MRを操作するAPIが提供されています。

これらのAPIを利用して以下のようなスクリプトを作成しました。

#!/bin/bash
# require
# - httpie
# - jq
# - GitLab Administrator Role & User Token 

TOKEN=************
total_pages=$(http --ignore-stdin 'https://gitlab.example.com/api/v4/groups/1/merge_requests?labels='"review requested(project)"'&per_page=100' Private-Token:$TOKEN --verbose | grep X-Total-Pages | awk {'print $2'} | sed -e 's/
//g')
for page in $(seq 1 ${total_pages})
do
   http --body --ignore-stdin \
   'https://gitlab.example.com/api/v4/groups/1/merge_requests?labels='"review requested(project)"'&per_page=100&page='${page}'' \
   Private-Token:$TOKEN | jq '.[]|{ id: .id, iid: .iid, project_id: .project_id, labels: .labels}'
done  > request.json

jq -c '.' request.json | while read mr
do
  id=$(echo ${mr} | jq '.id')
  iid=$(echo ${mr} | jq '.iid')
  pid=$(echo ${mr} | jq '.project_id')
  labels=$(echo ${mr} | jq '.labels|join(",")')
  replaced_labels=$(echo ${labels} | jq -r 'sub("review requested\\(project\\)";"review requested")')
  http --body --ignore-stdin \
      PUT 'https://gitlab.example.com/api/v4/projects/'${pid}'/merge_requests/'${iid}'?labels='"${replaced_labels}"'' \
      Private-Token:$TOKEN
  sleep 5
done

今回は重複しているラベルが少なかったため、ラベル名は引数ではなく直接定義してしまっています。大雑把には以下のようなフローです。

  1. 対象グループ内でリネーム後のプロジェクトラベルが付与されたMRの一覧(ラベル定義の変更に必要なメタ情報を含むJSON)を作成
  2. ラベル定義を修正したJSONを使って各MRの情報を更新
    • スクリプト内ではラベルの置換をしていますが、前述の対応方法に記載されているようにプロジェクトラベルを削除することで更新されるため、置換ではなく単純な追加でもOKです

各プロジェクトのラベル一覧を確認して重複しているラベルがあったらスクリプトを適宜修正して実行、という対応で重複を解消しました。

すべてのグループやプロジェクトに対してラベル重複のチェックをして解消したい場合は、以下のような各言語向けのモジュールを使ったスクリプトを作成するのも良いかもしれません。

結論

  • 汎用的なラベルはグループラベルに定義すること
  • 既存のラベルと同名のラベルを適当に作らない
  • 実際にラベルを使う場面をちゃんと考えましょう

株式会社エニグモ 正社員の求人一覧 hrmos.co

CA × atmaCupに参加しました

はじめに

こんにちは、初投稿になります。
今年の1月からエニグモでデータサイエンティストをしている堀部と申します。

1/25(土)に行われたCA × atmaCup というオフラインのデータコンペティションのイベントに参加しました。

【東京・大阪同時開催】CA × atmaCup - connpass

85チーム中、Public 12th/Private 5th*1とKaggle Expertとしてはまさかの最上位という結果でした。

コンペ前、コンペ中、コンペ後に分けて振り返ります。

※本記事ではコンペのデータやタスクについては情報公開が難しいため触れません。

コンペ前

コード整理

以前のコンペや業務で使っていたコードをなるべくクラス・メソッド化してgitにまとめて整理しました。綺麗なコードとは言えないですがまとめておいたおかげで、新しい特徴量の作成やタスクに特化したデータの前処理に時間を割くことができました。主に汎用的な特徴量生成の処理やGBDTモデルの実験をやりやすくする下記のようなコードをまとめていました。

  • EDA

  • 特徴量生成

    • 1種類しかないカラムを削除
    • 集約特徴量の生成(mean, median, max, min, std)
    • label encording
    • count encording
    • target encording
  • 学習(モデル)

    • ベース:lightgbm 5-fold
    • オプション:random seed averageやlightgbm tunerをオンオフできるように追加

コンペ中

素敵な環境でした

サイバーエージェントさんのできたてほやほやのオフィスのスクランブル交差点の見える席でもくもくと作業しました。頭を使いすぎて最後2時間くらいは少し頭がぼーっとしてました。 f:id:enigmo7:20200203165320j:plain

コンペティションサイトの出来が素晴らしかったです。○aggleと違ってすいすいsubmitができストレスなく作業に集中することができました。

コンペ中の作業方針

  1. 必要最低限の特徴量でベースライン作成してsubmit
  2. EDAやfeature importanceを見て特徴量追加
  3. CVもpublic LBも伸びたら採用
  4. ひたすら、2,3を繰り返す

モデルはずっとlightgbmシングルでアンサンブルはしませんでした。lightgbmのハイパーパラメータはcat_smoothのみ手動チューニングしました。
そして、特徴量のアイディアが一旦つきたところで lightgbm tunerを使って一度パラメータ探索をした結果を最後まで使いました。

コンペ後

表彰式、懇親会

コンペが終わってすぐ結果発表&表彰式がありました。上位3名の方の解法を聞くことができました。 その特徴量効きましたよねと共感したり、そのアイディアは思いつかなかったなとメモしたりすぐに情報共有できるオフラインコンペならではのよさを実感しました。

その後の懇親会でも当日のコンペだけでなく別のコンペについての取り組みについても話を聞くことができ、普段からコンペに参加しておくメリットも感じました。

次の日

次の日は日曜日だったので表彰式や懇親会での話を受けて思いついたアイディアを実装して追試をしていました。少し考えればこの特徴量は要らないかもと思ったものを削除しただけで、スコアが伸びたりとなぜコンペ中に気がつかなかったのかと後悔もありました。

また、上位の方が公開してくれたコードやgitを見て自分のコードの汚さに愕然としました。
普段から綺麗なコードを書く、他の人のコードを見て学ぶということを日頃から意識しようと胸に刻み込みました。

最後に

(真剣に取り組んだ)コンペの数だけ強くなる、強い人に会いに行くという大切さ、オフラインコンペならではの大変さ、楽しさを味わうことができたとても充実した一日でした。次回があればまたぜひ参加したいです。

また、エニグモ に入社して1ヶ月が経ちましたが楽しい日々を過ごしています。ABテストの効果検証×傾向スコア、商品のクラスタリング、回帰予測モデリングなど、自分で思いついてビジネスに貢献できそうなものであれば自由に取り組んでよいボトムアップの環境です。

個人的には、

  • ファッションEC × CtoCというドメインならでは多様性のあるデータ(テーブル、テキスト、画像)が扱えること
  • 社内の半数以上の方がSQLをかけデータをビジネスに活用する文化が定着していること
  • BigQuery, AWS, Lookerなど、ツールへの投資が当たり前のように行われていること

によさを感じています。

コンペ以上に業務も楽しく取り組める強いチームをつくれたらと思っております。興味のある方はぜひ気軽にご応募ください。選考とは別にカジュアル面談も受け付けております。データサイエンティスト、データアナリスト、データエンジニア、検索エンジニアなど各種募集中です。

株式会社エニグモ 正社員の求人一覧 hrmos.co

*1:ランキングシステムがPublicとPrivate で分かれていて、コンペティションが終了するまではテストデータの一部だけを使ってスコアを算出されたPublicのみのランキングが公開されます