TECH PLAY

株式会社エニグモ

株式会社エニグモ の技術ブログ

241

こんにちは、 @pompom0c0 です。 この記事は Enigmo Advent Calendar 2018 の18日目の記事です。 17日目の記事は @natten の はじめてのキーボード設計における アンチパターン でした。 今日は BUYMA 内で使用している見出しデザインついて紹介して行こうと思います。 前置き 今回この記事を書こうと思った経緯 Sassの @mixin を実践でどんな風に使っているか知ってほしい! 使う見出しデザインがパターン化されているので、デフォルトであると便利だと思ったから 想定読者 LPの実装をするデザイナーの方 @mixin は自分から書いたことがない人。 今回扱わないこと FLOCSSの設計について @extend や @content での書き方紹介 mixinのおさらい 別で定義したスタイルを @include でなんども呼び出すことができます。 引数を指定して、関数的な使い方もできます。 @mixin hoge($color: #fff , $size: 1rem) { color : $color; font-size : $size; } .ttl__1 { @include hoge( #000 , 2rem ); } .ttl__2 { // 引数は初期値のを使用 @include hoge(); } .ttl__1 { color : #000 ; font-size : 2rem ; } .ttl__2 { color : #fff ; font-size : 1rem ; } 引数を使えば初期値で出力するだけでなく、「今回はもう少し違った雰囲気で見せたいな...」という際には値を変えれば デフォルトで CSS を設定していても、出力結果を冗長させることなく記述することができます。 あとはよくある見出しデザインをSassでまとめて記述しておきます。 よく使う見出しデザインSass 縦線 @mixin line--ver($color: #000 , $size: 30px) { &::after { content : '' ; width : 1px ; margin-top : 1em ; margin-bottom : . 5em ; display : inline-block ; // 線の色と長さを引数にする height : $size; border-right : solid 1px $color; } } 横線 @mixin line--under($color: #000 , $size: 1px) { position : relative ; &::before { content : '' ; position : absolute ; left : 50 %; bottom : -15px ; display : inline-block ; width : 60px ; -moz- transform : translateX( -50% ) ; -webkit- transform : translateX( -50% ) ; -ms- transform : translateX( -50% ) ; transform : translateX( -50% ) ; // 線の色と長さを引数にする height : $size; background-color : $color; } } 文字横に線 @mixin line--side($color: #000 , $size: 1px) { border-left : solid $size; border-right : solid $size; width : 45 %; margin : 0 auto 1em ; padding-bottom : 0 ; border-color : $color; } 文字下に 蛍光ペン でマーキング @mixin line--maker($color: #ffc63b , $size: .2rem) { background : linear-gradient( transparent 60% , $color 60% ) ; padding : 0 $size; } コンテンツの幅の分だけ色がついてしまうので、マーキングしたい箇所にのみ span タグでclass指定してください。 < div class = "framework__ttl" > < h2 >< span class = "ttl" > タイトル </ span ></ h2 > </ div > 鍵カッコ @mixin mark --quo($color: #000 , $size: 1px) { position : relative ; padding : 1rem ; &::before , &::after { content : '' ; width : 20px ; height : 30px ; position : absolute ; display : inline-block ; } &:: before { border-left : solid $size $color; border-top : solid $size $color; top : 0 ; left : 0 ; } &:: after { border-right : solid $size $color; border-bottom : solid $size $color; bottom : 0 ; right : 0 ; } } まとめ 久しぶりに使う見出しデザインを実装する直前になって 「あ、これどうやって書くんだっけ・・・」と ググる のは生産的じゃないな〜と思ってまとめてみました。 @mixin 使ってるけど引数までは使ってない!使いこなしてみたい!って方に参考になればと思います。 参考サイト Sass(SCSS)のmixin, extendなどまとめ Sassで@mixinを作る時に知っておきたい基礎知識 より素早くCSSコーディングするための、Sass(SCSS)のmixinスニペット集
アバター
こんにちは。Enigmo インフラグループの @natten です。 この記事は Enigmo Advent Calendar 2018 の17日目の記事です。 16日目の記事は @enigmo7 の デザインパターンとリファクタリング でした。 本日の記事は技術寄りの話ではなく趣味の世界、自作キーボードのお話です。 キーボードを設計するモチベーション 2015年登場のErgoDoxに端を発する(諸説あります)メ カニ カルキーボード自作のムーブメントは国内でも盛んになる一方で、今年はHelixやErog42, Mint60にCrkbdなどなどエポックメイキングなプロジェクトが多く登場しました。 自分もご多分に漏れずこの潮流に飲み込まれてしまい、キーボードを購入するために技術書典に足を運んだり、キーキャップ一式を購入してみては「こんなプラスチックの塊が2万円?!」と愕然とする日々です。 さて、しばらくキーボードを買ったり作ったり積んだりしていると、不思議なもので「自分でもキーボードを設計してみようかな?」という気持ちが芽生えてきます。 様々なキーボードのキー配置に触れているうちに好みのキー配列や指の可動域、多用するキーのポジションがわかってきて自分に最もフィットしたキーボードは自分にしか作れない、という結論になりました。 とにかくキーボードを設計する じゃあ実際にキーボードを設計してみましょう、ということで 教則本 を買います。 Crkbdを設計した @foostan さんによる、自作キーボード設計ノウハウ集です。 KiCADの使い方からケースの設計、PCBの発注と自作キーボード作成に必要な作業の一通りがわかりやすく記述された良書です。 こちらを斜め読みして、とにかく勢いとお試し感覚で設計してできあがった基板の実物がこちらになります。 https://pskbd.booth.pm/items/1044084 自作キーボードの世界に明るくない方はなんだこれ、となるでしょう。 じゃあ自作キーボード界の人ならどうかというと、同じくなんだこれ、という感想になります。 パーツを仮置きしてみると不思議がさらに加速していきます。 はみ出るように不自然に配置されたProMicroスペース、分離型でもないのに リバーシ ブル基板、キーと平行するように伸びるUSBケーブル。 なんでこんなことになってしまったのか、と冷静に振り返ってみると思い当たる節が次から次へと。 といったわけで、今回は こんなキーボードができたよ!すごいだろ! ではなく、 こんなキーボードができてしまった!こんなことしなければよかった! という点をご紹介します。 キーボード設計 アンチパターン コンセプトを山盛りにする 3列キーボードとか4列キーボードに追加できる、数字キー/ファンクションキー代わりの1列キーボードが欲しいな。数字キーだから0〜9で10キーかな キースイッチテスターってただのアクリルベース土台タイプが多いけど、打鍵感だけじゃなくて接点の違いとかも確認しようと思うと実際のキー入力が必要になるよね。 ホットスワップ タイプにすれば実用的なテスターとしても使えるんじゃない? 普段は使わないけど気まぐれに光らせたいときがあるなー。表面実装のLEDはハードルが高いから、とりあえずUnderGlowのLEDテープが取り付けられるようにパッドだけ用意しとこ モゲMicro怖いからProMicroソケット化したいし、もしものときに取り外せるようにトッププレートで覆わないようにしておいたほうがいいかな ダメなところ そんなにいっぱい並立できません 机上の空論なので、実際に基板を作るとコンセプト間で対立・矛盾が発生する 改善方法 各コンセプトの要素(実用,趣味,利便,実験,コストなど)のバランスを見直す。特に実験方向にステータスを振れば振るほど破綻する可能性が高くなる コンセプトはなるべく絞ると設計しやすい。反面、凡庸なものになってモチベーションが下がることもあるので、少しだけ背伸びするのも良い 何も考えずに リバーシ ブル基板にする 今後左右分離型のキーボードを作ることになるだろうし、多くの分離型キーボードと同じように リバーシ ブル基板にしよう! 気分や環境によって選べるのは利点だよね? ダメなところ MCU 周辺が無秩序なビアだらけになって汚い リバーシ ブルにしたばかりに配線がめちゃくちゃ面倒になる そもそも意味がない 改善方法 作ろうとしているキーボードの用途や量産体制、目的が リバーシ ブル基板に見合っているかよく考える 実例が多いからという雑な理由で リバーシ ブルにするより、片面できれいな配線方法を模索するのも一つの手です 基板以外の構成パーツについて深く考えない いろんなキーボードのソースデータからフットプリントのパーツも流用して基板の設計ができたぞ! 実際にフットプリントを置いて配線してみると、どうにもならない箇所があったから多少妥協して当初のイメージとは違う形になったけど、実物が見たいからとりあえず発注しよう。リセットスイッチとかスペーサーはあとで検討して調達すればいいや ダメなところ なんとなく流用したパーツの入手性やコスト、サイズ感を意識していない 平面上の寸法しか見ていないと、サンドイッチタイプのケースで物理的な干渉が発生することが多い 改善方法 実際にキーボードとして組み立てる場合のパーツ構成と具体的な寸法を書き出してみる 参考にしたキーボードが手元にあったら採寸して、パーツ配置の意図を読み取る 使用する可能性のあるパーツは検証用に一通り手元に揃えておく 二言目には未完成とか言い出す 最低限は形になったけど、ダメな点もいっぱいあるし Github で公開するのはちょっと気が引けるな…マサカリ飛んできたら心が折れちゃうかもしれないし、しばらく手元で温めようかな ダメな点 そういうこと言ってるやつは永久に完成させられない 改善方法 とにかくプロジェクト名を決めて Github に空 リポジトリ を作れ、 Twitter とか自作キーボードDiscordでガンガン進捗を上げてバンバン叩かれよう アンチパターン を振り返って よくよく振り返ってみると、どれもキーボード設計に限ったことではありませんね。 プロダクト開発やシステム設計でもありがちな話ばかりです。 コンセプトを山盛りにする →理想を高く掲げすぎず、実装する要素を精査する 何も考えずに リバーシ ブル基板にする →一般事例に惑わされず、自分の目的を見直す 基板以外の構成パーツについて深く考えない →細部にとらわれず、最終的なビジョンと全体像を考えて念入りに準備をする 二言目には未完成とか言い出す →初手から完璧を求めず、スモールスタートでもいいから人の目に晒して育てる 何かを利用・享受する側だけではなく、作り手の側に回り実際に手を動かしてみると、趣味の世界からでも得るものがあるという学びでした。 自分と同じようにはじめてキーボードを設計してみようかな、と考えている方の参考になれば幸いです。 Enigmoでは何かを作り出すことに熱中できる 仲間を募集しています 。 Adevent Calendarの記事ラインナップからもわかるように、バラエティに富んだメンバーがお待ちしていますので、 自作キーボードを一緒にワイワイやってくれる方の  ご応募お待ちしております。 明日の記事の担当は @pompom0c0 です。お楽しみに。
アバター
この記事は Enigmo Advent Calendar 2018の16日目 です デザインパターン と リファクタリング こんにちは。 iOS チームでエンジニアをやっています 今チームでは、プロジェクトの進行と並行して リファクタリング を行なっています 対象プロジェクトは、MVVMの デザインパターン を多用しています そのプロジェクトをどう リファクタリング していったかをツラツラと書いていこうかと思います TL;DR デザインパターン って、設計パターン。うまく対処するためにどう設計していけばいいかをまとめたものです デザインパターン を各画面で分けよう 簡単な実装なのに、MVVMを利用したら複雑になってしまった、であれば、設計を間違えている リファクタリング リファクタリング については、短期間で見ると、ビジネスとしてぶっちゃけ一円にもなりません。 しかし、 リファクタリング をする、しないでは、未来のプロジェクトの進行速度に影響していきます。 なぜ リファクタリング をしたのか メンテ、新規開発がしづらい 数行いじると全然違う画面でエラーがでてしまう どの画面がどのViewControllerなのかわからない Swiftらしいコードに MVVMなのに、UIViewControllerがふとっている 着手前 [アプリ設計] RxSwift / RxSwift Community の様々なライブラリを使って MVVM を 試してみた実装 iOS5 の時代のライブラリをそのまま使い続け、 iOS の新しい機能が実装されていない [構成] Managers サーバ API へアクセスする レスポンスをModelへパースする パースしたModelの一部を、 インスタンス 変数で管理 シングルトン ViewController 別ViewControllerに遷移 Userのアクションへのリアクション View SnapKit によるレイアウト実装 ViewModel Managerにデータを要求、受け取ったデータを管理 UIViewControllerにアクセスしてUIの更新等を行う Model データ 今 [アプリ設計] RxSwiftのみを使って他のCommunityライブラリを削除 実装は各画面でわけた  - 自由にかけるように変更 デザインパターン を各Componentで決めることにしました iOS5 から使っている OSS で、メンテがされていないものは削除 iOS / Swift の新しい機能を実装 https://developer.apple.com/design/human-interface-guidelines/ios/user-interaction/feedback/ https://developer.apple.com/documentation/uikit/uistackview https://developer.apple.com/documentation/swift/codable [構成] Components Model データ ViewController 別ViewControllerに遷移 Userのアクションへのリアクション Delegate / DataSourceの実装 Storyboard / Xib レイアウト実装 View iPhone のサイズによるFontサイズの調整など Network APIClient サーバに要求する Responseを生成する Response Codableによって API をモデル化 比較 Before After やったこと Componentsという ディレクト リを作ってその中に各画面毎のfileをいれるようにしました その画面に関係するfileが明確になり、関係ないものは使わないようにチームで心がけるようになりました 各Componentで デザインパターン を変更できるようにしたので、あった設計をできるようにしました 簡単な画面については、コード量が少ない設計に変更 APIClient / JSON デコーダ ー ともに複数あったので、新しいAPIClientを作成し、Codableを使うように変更 -> 古いものはまとめて削除。 Objective-C の時のような、 json["key"] as? Int のような実装をなくしました ソースコード に対するコメント / BTS なぜその実装になったのか等記載がない -> 現状を知り着手しやすいように、複雑な処理になる部分は、シーケンス図 / コメントで動作を記載 一行直すだけで、関係ないと思ったところでエラーが起こる -> Component間で扱うデータを減らし、非結合にしてComponent間の影響を減らしました Manager / Utility クラスは、シングルトンで実装 -> シングルトン実装のクラスを極力減らす。シングルトンにするとpropertyをつけたくなる人がいるので避けます UIはSnapKitのみで実装 -> Storyboard / Xib で実装、IBInspectable, IBDesignableを使い、 GUI で状況を把握しつつ実装  デザイナーが作ってくれているレイアウトに沿った物を作れる ( SnapKitだけの時、cornerRadiusなど漏れが発生していた ) まとめ よかったこと コードの削除をかなりできた 着手前と今の差分: 2467 files changed, 142326 insertions(+), 271177 deletions(-) すべての画面に、MVVMをあてようとして無理している部分が多々あり結果、読みにくいコードになっていたのを直せた レビューする際に、コードよりも、Storyboard / Xib で見た方がわかりやすかった Before: override func viewDidLoad () { super .viewDidLoad() ... . 40 行ほど } After: override func viewDidLoad () { super .viewDidLoad() imageView.image = UIImage(named : ... ) // 1行のみ } ドキュメント作成した事で効率改善が行えた デザイナーとの画像受け渡しは、 Xcode から直接やってもらえることになった ドキュメント自体もレビューされるので、その際に共有できた レビューする側も、どういう事をしたいロジックなのか理解できた リファクタリング を行う時、考慮すること チーム全員なので、非エンジニアにも理解してもらわないといけない リファクタしたいところを共有しておく 企画・ディレクター案件がある際、その画面対応の際にまとめて行う ディレクターには、案件を画面毎にまとめるように整理してもらう e.g. - ホームが重いので改善する作業中に、haptic feedbackの導入や、お気に入りのハートを他の画面と同期するように仕組みを入れました - 検索画面を改修する際に、保存した検索条件に起因する部分もまとめて、書き直し整理を行ったりしました
アバター
はじめに エニグモ サーバーサイドエンジニアの @gugu です。 この記事は Enigmo Advent Calendar 2018の15日目 です。 日頃は BUYMA の機能改修を行っていますが、弊社では月末の プレミアムフライデー は業務と関係ない開発を行って良い日となっています。 そこで、前から興味のあった 機械学習 で何か作ってみようと思いました。 Chainerを使って「まるばつゲーム」を学習させてみたので、簡単にやったことを書こうと思います。 github.com ※ちょくちょくリファクタするかもです。 本題に入る前に 私のスペック 日頃は Rails でサーバーサイドの開発 python はラズパイでLチカをしたぐらい はじめての 機械学習 仕様など みんな知っているまるばつゲームです。 学習させるコンピュータは常に先手 0〜8の計9マス。どこに打つかを学習させます。(もう既に打たれたマスにも打つことが可能。当然ルール違反なので負け) | | 0 | 1 | 2 | | -----+-----+----- | | 3 | 4 | 5 | | -----+-----+----- | | 6 | 7 | 8 | | 盤面のデータは誰も打ってないマスは0、○は1、☓は2の値が入ります。 | | | | ○ | | -----+-----+----- | | ☓ | ○ | ○ | | -----+-----+----- | | | ☓ | ☓ | | ↓ ↓ ↓ | | 0 | 0 | 1 | | -----+-----+----- | | 2 | 1 | 1 | | -----+-----+----- | | 0 | 2 | 2 | | 配列だと [0, 0, 1, 2, 1, 1, 0, 2, 2] コンピュータと戦わせて学習させます。(完全ランダムに打つ。打たれているマスには打たない。不毛な戦いになるので。) 盤上の状態を入力して、出力結果のうちの最大値のマスに打ちます | | | | ○ | | -----+-----+----- | | ☓ | ○ | ○ | | -----+-----+----- | | | ☓ | ☓ | | のとき入力値は [0, 0, 1, 2, 1, 1, 0, 2, 2] 出力結果がもし [0.1, 0.2, 0.4, 0.2, 0.1, 0.3, 0.9, 0.1, 0.5] だったら、数値が最大のマス6に打ちます。 勝敗結果から打ったマスが好手だったのか悪手だったのかを教えます Chainerの基礎知識 ニューラルネット を定義 from chainer import Link, Chain, ChainList import chainer.functions as F import chainer.links as L class MyChain (Chain): def __init__ (self): super (MyChain, self).__init__( # 9-20-9の3層(隠れ層の20はなんとなく。) l1 = L.Linear( 9 , 20 ), l2 = L.Linear( 20 , 9 ) ) def __call__ (self, x): # 伝播(sigmoidでも良いがleaky_reluの方が結果が良いような気がする。) h = F.leaky_relu(self.l1(x)) o = self.l2(h) return o 初期設定 # ニューラルネット model = MyChain() # 確率的勾配降下法(Stocastic Gradient Descent)を使用 opt = optimizers.SGD() opt.setup(model) 打つ場所を決める # 盤上の状態データをfloat32に形成する(chainerがfloat64には対応していないため) x = Variable(np.array([input_data], dtype=np.float32)) # 勾配を0に初期化(chainerのお決まりごと) model.zerograds() # 入力xを変換し出力yへ y = model(x) # 出力の最大値を打つ y.data.argmax() 学習させる # 教えるデータをfloat32に形成する t = Variable(np.array([teacher_data], dtype=np.float32)) # 出力yと教えるデータtとの差分を算出(平均二乗誤差) loss = F.mean_squared_error(y, t) # 逆伝播 loss.backward() # 最適化 opt.update() 上記の基礎知識を使ってまるばつゲームを学習させていきます。 学習方法 強化学習 はよく理解していないので、基礎知識のみで自己流に学習させます。 # result: 勝敗結果 # ドロー:0 # 勝ち:1 # 負け:2 # 既に打たれたマスに打った:3 def learn (self, result): for i, y in enumerate (models): # marks[i]: 打ったマス番号 # y.data[0]: 出力値 teacher = self.teacher(result, marks[i], y.data[ 0 ]) t = Variable(np.array([teacher], dtype=np.float32)) # 出力yと正解tとの差分を算出(平均二乗誤差) loss = F.mean_squared_error(y, t) # 逆伝播 loss.backward() # 最適化 opt.update() # 学んだらリセット del models[:] del marks[:] def teacher (self, result, mark, model): data = [] # draw, win if result == Settings.WIN: for i in range ( 9 ): if i == mark: # 打ったマスに値を与える data.append( 1 ) else : # 打ってない箇所は現状維持 data.append(model[i]) # lose or same place elif result == Settings.LOSE or result == Settings.SAME_PLACE: for i in range ( 9 ): if i == mark: data.append(- 1 ) else : data.append(model[i]) # draw else : for i in range ( 9 ): if i == mark: data.append( 0 ) else : data.append(model[i]) return data 学習方法説明 勝った場合 | | | | ○ | | -----+-----+----- | | ☓ | ○ | ○ | | -----+-----+----- | | ○ | ☓ | ☓ | | 最後の一手の出力結果が [0.1, 0.2, 0.4, 0.2, 0.1, 0.3, 0.9, 0.1, 0.5] で、マス6に打った場合、 出力結果: [0.1, 0.2, 0.4, 0.2, 0.1, 0.3, 0.9, 0.1, 0.5] 教えるデータ: [0.1, 0.2, 0.4, 0.2, 0.1, 0.3, 1 , 0.1, 0.5] とマス6の値を 1 にします。 同じように3手目が [0.3, 0.1, 0.9, 0.7, 0.5, 0.3, -0.3, -0.8, 0.5] で、マス2に打っていた場合 出力結果: [0.3, 0.1, 0.9, 0.7, 0.5, 0.3, -0.3, -0.8, 0.5] 教えるデータ: [0.3, 0.1, 1 , 0.7, 0.5, 0.3, -0.3, -0.8, 0.5] とマス2の値を 1 にします。 ※これを1手目まで繰り返します。 ※上記は勝った例なので 1 にデータを変換しましたが、負けた場合は -1 に、ドローは 0 にデータになります。 学習させてみた ※データは最後から100戦の勝率 100戦 win: 0.04 lose: 0.04 draw: 0.0 same_place: 0.92 1,000戦 win: 0.22 lose: 0.01 draw: 0.0 same_place: 0.77 10,000戦 win: 0.61 lose: 0.06 draw: 0.0 same_place: 0.33 100,000戦 win: 0.81 lose: 0.08 draw: 0.02 same_place: 0.09 強くなってる! VS 人間 実際に100,000戦したコンピュータと戦ってみました。 ※コンピュータ:○、 人間:☓ | | 0 | 1 | 2 | | -----+-----+----- | | 3 | ○ | 5 | | -----+-----+----- | | 6 | 7 | 8 | | ↓ ↓ ↓ | | 0 | 1 | x | | -----+-----+----- | | ○ | ○ | 5 | | -----+-----+----- | | 6 | 7 | 8 | | ↓ ↓ ↓ | | ○ | 1 | x | | -----+-----+----- | | ○ | ○ | x | | -----+-----+----- | | 6 | 7 | 8 | | ↓ ↓ ↓ | | ○ | 1 | x | | -----+-----+----- | | ○ | ○ | x | | -----+-----+----- | | 6 | 7 | x | | まともに戦えるが、弱い。。 学習方法を変えてみた どう変えたか? 「既に打たれたマスに打った」で負けた場合は、最後の一手のデータを調整するように変更します。 ソース # result: 勝敗結果 # ドロー:0 # 勝ち:1 # 負け:2 # 既に打たれたマスに打った:3 def learn (self, result): for i, y in enumerate (models): # 既に打たれたマスに打った and 最後の一手 if result == 3 and i == len (models) - 1 : # marks[i]: 打ったマス番号 # y.data[0]: 出力値 teacher = self.teacher(result, marks[i], y.data[ 0 ], True ) else : teacher = self.teacher(result, marks[i], y.data[ 0 ], False ) t = Variable(np.array([teacher], dtype=np.float32)) # 出力yと正解tとの差分を算出(平均二乗誤差) loss = F.mean_squared_error(y, t) # 逆伝播 loss.backward() # 最適化 opt.update() # 学んだらリセット del models[:] del marks[:] def teacher (self, result, mark, model, last_flg): data = [] # draw, win if result == Settings.WIN: for i in range ( 9 ): if i == mark: # 打ったマスに値を与える data.append( 1 ) else : # 打ってない箇所は現状維持 data.append(model[i]) # lose elif result == Settings.LOSE: for i in range ( 9 ): if i == mark: data.append(- 1 ) else : data.append(model[i]) # same plase elif result == Settings.SAME_PLACE: if last_flg == True : for i in range ( 0 , 9 ): if i == mark: # 最後に打ったマスだけを`-2`に調整する。 data.append(- 2 ) else : data.append(model[i]) else : for i in range ( 0 , 9 ): data.append(model[i]) # draw else : for i in range ( 9 ): if i == mark: data.append( 0 ) else : data.append(model[i]) return data 学習させてみた 10,000戦 win: 0.96 lose: 0.04 draw: 0.0 same_place: 0.0 コンピュータ相手だと9割以上勝てるようになって、打たれているマスには打たなくなりました。 VS 人間 (2) 100,000戦したコンピュータともう一度戦ってみました。 ※コンピュータ:○、 人間:☓ | | 0 | 1 | 2 | | -----+-----+----- | | 3 | ○ | 5 | | -----+-----+----- | | 6 | 7 | 8 | | | | 0 | ○ | 2 | | -----+-----+----- | | 3 | ○ | x | | -----+-----+----- | | 6 | 7 | 8 | | | | 0 | ○ | ○ | | -----+-----+----- | | 3 | ○ | x | | -----+-----+----- | | 6 | x | 8 | | | | x | ○ | ○ | | -----+-----+----- | | 3 | ○ | x | | -----+-----+----- | | ○ | x | 8 | | 強い!まだ最強とまでは言えませんが、2つリーチを作れることを覚えたようです。 まとめ 機械学習 はハードルがものすごく高いイメージでしたが、Chainerの基礎的な関数を駆使すれば初心者の私でも簡単な 機械学習 を作成できることがわかりました。 今後は 強化学習 ( DQN など)をちゃんと勉強して、負けないレベルに修正できたらと思います。
アバター
エニグモ データ活用推進室 @kmt47 です。 この記事は Enigmo Advent Calendar 2018の14日目 です。 概要 redashがv5にバージョンアップしました。(少し時間経ちますが) この記事では、ユーザ目線でredash v5の新機能を紹介します。 v3→v4へのバージョンアップと比べると、機能的な追加は少なめ&追加された機能の便利度も低めといった印象です。 また、v4で追加になった超便利機能「表示形式(format)」設定において、v5からパーセント表示の仕様が変更になっています。 それでは、上手に新機能を使って、redash作成を効率化していきましょう! クエリ編(表、グラフ) クエリを「お気に入り登録」できるようになった(便利度:★★) クエリを「お気に入り登録」できるようになりました。 「お気に入り登録」したクエリは、redashのトップページに「Favorite Queries」として表示されます。 ※ redashのトップページとは、画面の最上段のリ ダッシュ のマーク(💭)をクリックしたときの画面 また、クエリ一覧画面(Queries)では、右側の「☆Favorites」で絞り込むことができます。 小技として、最上段の「Queries」の右の下のボタンをクリックすると、「お気に入り登録」したクエリが選択できるようになっています。 「お気に入り登録」の方法は簡単で、クエリの画面でクエリ名の左の☆をクリックするだけです。 登録されたクエリは、★ が黄色になります。 もう一度 ★ をクリックすると、登録が解除されます。 良く利用するクエリは「お気に入り登録」しておくことで、トップページから簡単にアクセスできるようになります。 クエリに「タグ」を付けることができるようになった(便利度:★★) クエリに任意の「タグ」を付けることができるようになりました。 「タグ」を付けることで、クエリ一覧画面(Queries)の右側のリストから選択したタグで絞り込むことができます。 「タグ」を付ける方法は、クエリの画面の最上段にマウスをナビゲーションすると「+Add tag」のボタンが表示されます。 そのボタンを押すとポップアップ画面が表示されるので、既に登録済みのタグを選択、または新たなタグを入力して付与することができます。 1つのクエリにタグは複数付けることができます。 付けたタグを削除したい場合は、タグの右に表示される鉛筆マークをクリックして、削除したいタグの「×」を押してください。 パラメータの作成ボタンが追加になった(便利度:★★) これまでは、パラメータを作成するときは、クエリ入力画面に波括弧を二つ {{ }} 入力して、その間にパラメータ名を入れていたかと思います。 こんな感じです。→ {{ hoge }} v5では、パラメータ追加ボタンが追加されました。 クエリ入力画面の下の {{ }} ボタンがそれです。 ボタンをクリックすると「Add Parameter」というポップアップが表示されます。 「Keyword」は、クエリに埋め込むパラメータの名前、「Title」はパラメータの入力エリアの名前です。 「Type」でパラメータのデータ型を選ぶことができます。 なお、これまで通りクエリ入力画面で直接追加することもできます。 パーセント表示の仕様が変更になった(便利度:マイナス☆☆☆☆) この記事 「Redashがバージョンアップ(v4)して便利になったこと」 にも記載しましたが、v4で追加された最大の便利機能「数値の表示形式(フォーマット)をredashで設定できるようになった」のパーセント表示に関する仕様が変更になりました。 例えば、前年比を表示する場合、 「売上(今年) 120万円」÷「売上(去年) 100万円」の結果に対して、表示形式で「0.0%」を指定すれば、 120 ÷ 100 = 1.2 で 「120.0%」と表示されていました。 しかし、v5では、このままでは「1.2%」となってしまいます。 つまり、v4では自動的に100倍されましたが、v5でクエリで100倍する必要があります。 個人的な意見としては、v4の仕様の方が良い(正しい)と思います。 Excel でも1.2の値をパーセント表示にしたら120.0%になりますよね。それを敢えて100倍するなんて。 パーセント表示を使っていたクエリは、v5へのバージョンアップによって、全て修正が必要になりました。 ダッシュ ボード ダッシュ ボードを「お気に入り登録」できるようになった(便利度:★★) 使い方はクエリと同じなので、詳細は省略します。 ダッシュ ボードに「タグ」を付けることができるようになった(便利度:★★) 使い方はクエリと同じなので、詳細は省略します。 ダッシュ ボードの変更時にグチャってならなくなった。(便利度:★★★★) v5でもっとも良くなった点は、ここだと思います。 v4のユーザであれば「グチャって」いう意味が分かると思いますが、 v4では、 ダッシュ ボードのクエリのサイズ、特に高さを変更していると、クエリのサイズが崩れて(「グチャって」)、取り返しのつかないことになった経験がある方も多いと思います。 その為、一度変更したら、保存して、また変更したら保存して、といったように崩れても元に戻れる対策を講じながら作業を行う必要がありました。クエリのサイズを自由に変更できる機能はv4で追加された便利な機能なのですが、ちょっと不具合があったようです。 それがv5では修正されています。ちょっと変更するごとに保存して、「グチャって」なったら修正をキャンセルして、といった作業から解放されたのは非常に助かりました。 まとめ v5へのバージョンアップでは、便利機能の追加は少なめでした。 v4では、かなり便利な機能が多く追加されていたので、偶数バージョンのv6に期待します。
アバター
Org-modeを半年くらい使ってみた Enigmo Advent Calendar 2018の12日目の記事です。 こんにちは、エンジニアの @t4ku です。半年ほどorg-modeを使ってメモや、日々のタスク管理を行ってきたのでやってみた感想を共有しようと思います。 org-modeとは何か? org-modeとは emacs 上で動作する アウトライナー です。 アウトライナー は有名なところでいうと Mac アプリでは Omnioutliner や webアプリでも workflowy などがあります。 workflowlyについてはこちらの紹介記事がわかりやすいです(丸投げ) http://goryugo.com/20180412/dynalist-workflowy/ 課題やタスクのブレークダウンなど考えをまとめたりするのに org-modeではこのようなツリー構造をプレーンテキストで書いておけば 鞍上いい感じに表示してくれます。 markdown でも同じようなことができますが、ノードを移動したりインデントを変えたりするのが面倒なのでそういう用途で markdown を使う人はいないと思います。 また、スケジュール機能やTODOやタグやクロック機能もあるのでこれだけで 見積もりや振り返りがプレーンテキストで完結します。 org-modeのここがいい 自分が使っていて特によいと思った機能です。 テーブル表記の入力が楽 勝手にフィールドの幅を調整してくれたりなかなか便利だなと思いました。 体験すると、qiitaの markdown でtableを書くことが苦行というかほとんど罰ゲームに感じるようになってきました。 画像や数式が差し込める プレーンテキストでありながら画像も入れれるので、gitなどで履歴管理しつつ最低限わかりやすい ビジュアルをキープできるので、 プログラマ のメモとしてはいいバランスだと思います。 Latex 記法で書いたものは数式が表示されます。 ソースコード が実行できる org-babelという拡張があるので ソースコード ブロックで書いたものを評価して、結果を表示できます。 ※ob-ipythonというjupyterに繋ぐ拡張が必要ですが ob-ipython org-babel integration with Jupyter for evaluation of ( Python by default) code blocks スケジュール機能( アジェンダ ) ノードにスケジュールを設定しておくと、 アジェンダ コマンドを利用してその日にスケジュールされたタスク一覧( アジェンダ ビュー)を表示することができます。 ※実際のファイルが出せないのでテキトーなタスクなのでわかりにくくてすいません アジェンダ ビューはスケジュール日別に出したり、deadlineごとに出したり、タグごとに出したりいろいろカスタマイズできますが、自分の場合は、オペレーション系のタスクとプロジェクトごとのタスクごとに一覧化するようにして、一日毎の作業を管理する別のorgファイルにコピーします。 一日のタスクを直列に並べると、あんまり余計なことを考えずにただこなしていけるような気がします。 クロック機能が便利 各ノードにTODOステータスやスケジュールを設定するだけでなく、実際に作業をする時にクロックインすると 時間を記録してくれます。また、任意の期間でレポートを作成できます。 活用法 何も考えずに単体のorgファイルをそのまま使っててもいいのですが、自分の場合は下記のように Dropbox 配下の ディレクト リを分けてメモと予定/振り返りを管理しています。 orgファイル間は簡単にリンクを貼って辿れるので、 アジェンダ ファイル(Agenda/work.org)内のトピックから必要なファイルにリンクを張っておけば、だいたい事足ります。 どのようにファイルをオーガナイズするかやどういう単位で分割するかということについては深遠なテーマで、半年くらい使った素人ではまだキャッチアップできない(というか一生できる気がしない)のですが、下記の youtube シリーズはすごく勉強になりました。 org-mode tutorials 半年くらい使ってみた感想 何をどこに書くべきかが決まってきて キーバインド にも馴染んでくると、フローを壊さずに開発してるときもも打ち合わせしてるときも、ア イデア をためておけるので、何かのインタラプションがあっても、安心して忘れられる他、 ググる 回数やブラウザで遷移する回数が減ったきがします。 また、テキストなのですべてgitで管理できるので、週次ごとにプルリク エス トを作るようにすると diffを見れば振り返るのが一目瞭然です。 あと副産物ですが、普段プログラミングをする際は vim を使っているのですが、org-modeのためだけに emacs を使うようになり少し emacs の良さがわかってきました。そして両方の宗教を理解することで、世界平和に少し貢献できるような気がしてきました。 参考 org-mode tutorials How I use Emacs and Org-mode to implement GTD
アバター
Enigmo Advent Calendar 2018 の12日目の記事です。 注意: この記事のサンプルコードで使われている各ライブラリのバージョンは下記になります。 react 16 . 4 . 0 react-dnd 4 . 0 . 2 react-dnd-html5-backend 4 . 0 . 2 react-dnd-touch-backend 0 . 5 . 1 React DnD Reactで ドラッグアンドドロップ での並び替えを実装する際によく使われるのが React DnD というライブラリです。 このライブラリでは HTML5 の Drag and Drop API を利用して ドラッグアンドドロップ を実現していますが、この API 自体が スマートフォン などのタッチデ バイス には対応しておらず、 スマホ でそのまま ドラッグアンドドロップ を実装することができません。 TouchBackend React DnD を使う際、 ドラッグアンドドロップ したい コンポーネント を DragDropContext という HOC(Higer Order Component ) に渡します。 この DragDropContext の最初の引数に渡すのは通常、 HTML5Backend というバックエンドモジュールです。 import HTML5Backend from 'react-dnd-html5-backend' import { DragDropContext } from 'react-dnd' class YourApp { /* ... */ } export default DragDropContext(HTML5Backend)(YourApp) 前述した通りタッチデ バイス の場合はこの HTML5Backend は使えません。 しかしタッチデ バイス 対応した TouchBackend というものがあるのでそちらを使います。 import HTML5Backend from 'react-dnd-html5-backend' import TouchBackend from 'react-dnd-touch-backend' ; import { DragDropContext } from 'react-dnd' const isTouchDevice = () => { /* タッチデバイス判定 */ } class YourApp { /* ... */ } export default DragDropContext(isTouchDevice() ? TouchBackend : HTML5Backend)(YourApp) これだけでタッチデ バイス 対応ができました。 しかし、 HTML5Backend のようにいい感じにプレビューされません。 HTML5Backendではちゃんとプレビューされている TouchBackendではプレビューされていない! ※ Chrome のDevToolsで スマートフォン をエミュレートして録画しているためマウスカーソルが表示されています。 DragLayer React DnD には DragLayer という、ドラッグ時のプレビュー表示をカスタマイズできる API があります。 これを使うことでタッチデ バイス でもいい感じのプレビューを表示することができます。 利用側のサンプルコードは以下です。 import React from 'react' import DragLayer from 'react-dnd/lib/DragLayer' import TouchBackend from 'react-dnd-touch-backend' ; import { DragDropContext } from 'react-dnd' function collect(monitor) { const item = monitor.getItem() return { currentOffset: monitor.getSourceClientOffset(), previewProps: item && item.previewProps, isDragging: monitor.isDragging() && monitor.getItemType() === 'IMAGE' } } function getItemStyles(currentOffset) { if (!currentOffset) { return { display: 'none' } } const x = currentOffset.x const y = currentOffset.y const transform = `translate($ { x } px, $ { y } px) scale(1.05)` return { WebkitTransform: transform, transform: transform, } } class PreviewComponent extends React.Component { render() { const { isDragging, previewProps, currentOffset } = this .props if (!isDragging) { return null } return ( <div> { /*...*/ } </div> ) } } const DragPreview = DragLayer(collect)(PreviewComponent) class YourApp { render() { return ( <div> { /* ... */ } </div> ) } } export default DragDropContext(TouchBackend)(YourApp) かんたんに解説 DragLayer の引数 collect 関数では DragLayerMonitor のオブジェクトが渡されます。 monitor.getItem() で DragSource にアクセスすることができ、 任意で渡した props (今回の場合は previewProps という名前で渡していますが、どんな名前でも渡すことができます) にアクセスできます。 また、 monitor.isDragging で実際にドラッグされているか判定することができます。 同一画面の他の コンポーネント でも ドラッグアンドドロップ するために、 DragDropContext が複数ある場合は monitor.getItemType() でどのコンテキストなのかを判定するとよいでしょう。 プレビューがタッチした部分に追従するように monitor.getSourceClientOffset() を使ってオフセット座標を返しておきます。 collect 関数の返り値のオブジェクトはそのままプレビュー用の コンポーネント で props として受け取ることができます。 getItemStyles 関数では受け取った props.currentOffset を使って CSS を調整しています。 DragDropContext に渡した コンポーネント で DragLayer を描画することで、ドラッグ時にプレビューを表示することができます。 スマホ でもプレビューができた! ※ Chrome のDevToolsで スマートフォン をエミュレートして録画しているためマウスカーソルが表示されています。 最後に スマートフォン などのタッチデ バイス で HTML5 のような ドラッグアンドドロップ を実現する方法を解説しました。 実際に実装する際は、 TouchBackendのリポジトリ に完全に動作するサンプルがあるのでそちらも参考にしてみてください。 参考リンク http://react-dnd.github.io/react-dnd/about https://github.com/yahoo/react-dnd-touch-backend
アバター
はじめに この記事は Enigmo Advent Calendar 2018の11日目 です。 Enigmoでは、データウェアハウス(DWH)としてBigQueryを使っていて、サービスの アクセスログ やサイト内の行動ログ、データベースのデータをBigQueryへ集約させています。 データベースからBigQueryへのデータ同期には Apache Airflow を使っていて、今日はその仕組みについて紹介します。 Apache Airflowとは Airflowは、 python でワークフロー(DAG)を定義すると、そのとおりにタスク(オペレーター) をスケジューリングして起動してくれるツールです。 GCP でもGKE上でAirflowを動かすCloud Composerというサービスが提供されていてご存知の方も多いと思います。 データの処理の単位をオペレータで定義し、その処理の依存関係を反映したワークフローをDAGで定義してやればデータ処理のパイプラインを実現することが可能となります。 DBからBigQueryへのデータパイプライン データの流れ データの流れとしては、上の図の通り大きく2フェーズに分かれていて、まずはDB( SQL Server )から Google Cloud Storage(GCS)へデータをアップロードしています。その次にGCSからBigQueryへそのデータをロードしています。 それぞれのフェーズをAirflowのタスクの単位であるオペレーターで実現していて、さらに2つのオペレーターはそれぞれ同期するテーブルごと別のタスクとして存在し、それらをDAGという1つのワークフローの単位でまとめています。 SQL Server からGCSへ JdbcToGoogleCloudStorageOperator SQL Server からGCSへのデータの移動は JdbcToGoogleCloudStorageOperator というAirflowのオペレーターが担当します。 DBが MySQL の場合は MySqlToGoogleCloudStorageOperator というAirflowに組み込みのオペレーターがあるんですが、バイマのデータベースは SQL Server なので、 JDBC のクライアントで同様の働きをするオペレーターを自前で作ったものが JdbcToGoogleCloudStorageOperator です。Airflowの プラグイン として公開しています。 github.com このオペレータでの処理は、まずDBから SQL でデータを抽出し、一度JSONL形式のファイルとしてのオペレーターが動くサーバーのローカルに保存され、それがGCSへアップロードされるという流れです。BigQueryへロードするときに スキーマ 定義が必要なので、データファイルとは別に スキーマ 定義のファイルも JSON 形式でGCSへアップロードされます。 スケジューリングと更新差分抽出の仕組み DAGのスケジューリング間隔は1時間に設定しています。するとAirflowは時間を1時間ごとに期間を分けてDAGにその期間の開始時刻( execution_date )、終了時刻( next_execution_date )をテンプレートのパラメーターとして渡してくれます。それらを データ抽出 SQL のWHERE句のところでレコードの更新日時を記録するカラム(下の例では updated_at )を基準に期間指定すると、その期間に更新があったレコードだけが抽出され、BigQuery側へ送られる仕組みです。 SELECT * FROM table1 WHERE " {{execution_date.strftime('%Y-%m-%d %H:%M:%S') " <= updated_at AND updated_at < "{{next_execution_date).strftime(&# 039 ;%Y-%m-%d %H:%M:%S&# 039 ;)}}" もし間隔を変えてもDAGを編集することなく SQL がその期間に合わせて変わってくれるので便利です。 GCSからBigQueryへ GoogleCloudStorageToBigQueryOperator GCSからBigQueryへはその名の通りAirflow組み込みの GoogleCloudStorageToBigQueryOperator というオペレーターがやってくれます。 BigQuery側のデー タセット は同期元DBのデータベース単位、テーブルは同期元DBのテーブル単位に分けています。BigQuery側のテーブルはDB側のレコードの更新日ごとに日付分割しています。 BigQueryの更新は DML は使わずに、ファイルを読み込みジョブで更新されます。そうするとDB側のレコードが更新されるとBigQuery側には重複してレコードが溜まっていくのですが、それは後述の重複除外ビューで解決しています。 BigQuery側でレコードの重複を除外 BigQuery側のテーブルでは、次のような SQL でビューテーブルを作ることで、同期元のDBでレコードが何度も更新されても常に最新のレコードしか現れない仕組みになっています。 この例は、主キーが id で更新日時のカラムが  updated_at の場合の SQL です。同一 id に対して常に最新の updated_at をもつレコードしかこのビューには出てきません。 SELECT * FROM ( SELECT *, ROW_NUMBER() OVER ( PARTITION BY id ORDER BY updated_at DESC ) etl_row_num FROM `db1.table1_*`) WHERE etl_row_num = 1 Airflowで便利だった機能 Airflowの機能でこの仕組みをつくるのに助けられた機能がいくつかあったので紹介します。書ききれてないですが、ほかにもたくさんあります。 Catchup DAGのスケジュールを過去の期間にさかのぼって実行してくれる機能なんですが、非常にありがたかったです。 過去のデータの移行でも差分同期の仕組みがそのまま使えましたし、一度に同期せずに、期間を区切って少しずつデータを持っていけたので、同期元のDBにも負荷をかけずにすみました。 Connection、Variable Connection は接続先となるDBや GCP への認証情報を一元管理してくれ、一度設定すればどのDAGからアクセスできて便利でした。次のPoolも同じなんですが、設定は GUI でも CLI でも設定できるので、ansibleなどのプロビジョニングツールでも設定できたのもありがたかったです。 Variable も単なるキーと値を設定できるだけなんですが、DAGを汚すことなくdevやproductionなどリリースステージごとに値を切り替えられて便利でした。 Pool タスクの同時実行数を制限する機能です。Poolはユーザーが定義でき、そのPoolにオペレーターを紐付けるとそのオペレーターはそのPoolのslot数を超えて同時実行されません。データ抽出のタスクが1つのDBに対して多数同時実行されてしまうとそのDBのコネクションも同時に消費され、枯渇しかねませんが、このPoolで上限数を設定できたので安心でした。 まとめ 最初は手っ取り早くcronと スクリプト で作ってしまおうと思ったのですが、すこしなれるまで時間はかかったもののAirflowで作って良かったです。開発が進むにつれ、特にプロダクション環境で動かすにあたっていろいろ考慮すべきことが出てくると思うのですが、作りながらほしいと思った機能が先回りされているかのように用意されていてとても助かりました。全て使いきれてないですが、ワークフロー運用のノウハウがたくさん詰まった良いプロダクトだと思いました。
アバター
この記事は Enigmo Advent Calendar 2018の10日目 です。 はじめに OptunaはPFN社が公開したハイパーパラメータ自動最適化 フレームワーク です。 https://research.preferred.jp/2018/12/optuna-release/ 目的関数さえ決めれば、直感的に最適化を走らせることが可能のようです。 今回、最適化自体の説明は割愛させていただきますが、 機械学習 の入門ということを考えるとハイパーパラメータの調整としては、gridsearchやRandomizedSearchCVで行う機会が多いと思います。 スキル、あるいはリソースでなんとかするということになるかと思いますが、特に、kaggleのような0.X%の精度が向上が重要になるような状況では、ハイパーパラメータのチューニングが大きなハードルの一つになります。 そこで、titanicでのsubmitはあるものの、Kaggleの経験がほぼゼロな筆者でも、Optunaで簡単にチューニングができるかどうかを試してみようと思います。 今回の対象コンペ 既にcloseしているコンペの中で、下記のPorto Seguro’s Safe Driver Predictionを選びました。 https://www.kaggle.com/c/porto-seguro-safe-driver-prediction 選定理由は以下の通りです。 データがそれほど大きくない 手元(自宅)のラップトップのRAMは8GBと大きくないので、XGboostではなくメモリ消費が抑えられるLightGBMでやってみたい 解法がシンプルかつ、LightGBMで上位のスコアを解法を公開している カーネル がすぐに見つかった 公開解法の再現 https://www.kaggle.com/xiaozhouwang/2nd-place-lightgbm-solution 上記をそのままコピペして一回submitします。 Python2対応のようなので、下記のようにPython3で動くように修正しました。 # part of 2nd place solution: lightgbm model with private score 0.29124 and public lb score 0.28555 import lightgbm as lgbm from scipy import sparse as ssp from sklearn.model_selection import StratifiedKFold import numpy as np import pandas as pd from sklearn.preprocessing import LabelEncoder from sklearn.preprocessing import OneHotEncoder def Gini (y_true, y_pred): # check and get number of samples assert y_true.shape == y_pred.shape n_samples = y_true.shape[ 0 ] # sort rows on prediction column # (from largest to smallest) arr = np.array([y_true, y_pred]).transpose() true_order = arr[arr[:, 0 ].argsort()][::- 1 , 0 ] pred_order = arr[arr[:, 1 ].argsort()][::- 1 , 0 ] # get Lorenz curves L_true = np.cumsum(true_order) * 1. / np.sum(true_order) L_pred = np.cumsum(pred_order) * 1. / np.sum(pred_order) L_ones = np.linspace( 1 / n_samples, 1 , n_samples) # get Gini coefficients (area between curves) G_true = np.sum(L_ones - L_true) G_pred = np.sum(L_ones - L_pred) # normalize to true Gini coefficient return G_pred * 1. / G_true cv_only = True save_cv = True full_train = False def evalerror (preds, dtrain): labels = dtrain.get_label() return 'gini' , Gini(labels, preds), True path = "input/" train = pd.read_csv(path+ 'train.csv' ) train_label = train[ 'target' ] train_id = train[ 'id' ] test = pd.read_csv(path+ 'test.csv' ) test_id = test[ 'id' ] NFOLDS = 5 kfold = StratifiedKFold(n_splits=NFOLDS, shuffle= True , random_state= 218 ) y = train[ 'target' ].values drop_feature = [ 'id' , 'target' ] X = train.drop(drop_feature,axis= 1 ) feature_names = X.columns.tolist() cat_features = [c for c in feature_names if ( 'cat' in c and 'count' not in c)] num_features = [c for c in feature_names if ( 'cat' not in c and 'calc' not in c)] train[ 'missing' ] = (train==- 1 ).sum(axis= 1 ).astype( float ) test[ 'missing' ] = (test==- 1 ).sum(axis= 1 ).astype( float ) num_features.append( 'missing' ) for c in cat_features: le = LabelEncoder() le.fit(train[c]) train[c] = le.transform(train[c]) test[c] = le.transform(test[c]) enc = OneHotEncoder(categories= 'auto' ) enc.fit(train[cat_features]) X_cat = enc.transform(train[cat_features]) X_t_cat = enc.transform(test[cat_features]) ind_features = [c for c in feature_names if 'ind' in c] count= 0 for c in ind_features: if count== 0 : train[ 'new_ind' ] = train[c].astype( str )+ '_' test[ 'new_ind' ] = test[c].astype( str )+ '_' count+= 1 else : train[ 'new_ind' ] += train[c].astype( str )+ '_' test[ 'new_ind' ] += test[c].astype( str )+ '_' cat_count_features = [] for c in cat_features+[ 'new_ind' ]: d = pd.concat([train[c],test[c]]).value_counts().to_dict() train[ '%s_count' %c] = train[c].apply( lambda x:d.get(x, 0 )) test[ '%s_count' %c] = test[c].apply( lambda x:d.get(x, 0 )) cat_count_features.append( '%s_count' %c) train_list = [train[num_features+cat_count_features].values,X_cat,] test_list = [test[num_features+cat_count_features].values,X_t_cat,] X = ssp.hstack(train_list).tocsr() X_test = ssp.hstack(test_list).tocsr() learning_rate = 0.1 num_leaves = 15 min_data_in_leaf = 2000 feature_fraction = 0.6 num_boost_round = 10000 params = { "objective" : "binary" , "boosting_type" : "gbdt" , "learning_rate" : learning_rate, "num_leaves" : num_leaves, "max_bin" : 256 , "feature_fraction" : feature_fraction, "verbosity" : 0 , "drop_rate" : 0.1 , "is_unbalance" : False , "max_drop" : 50 , "min_child_samples" : 10 , "min_child_weight" : 150 , "min_split_gain" : 0 , "subsample" : 0.9 } x_score = [] final_cv_train = np.zeros( len (train_label)) final_cv_pred = np.zeros( len (test_id)) for s in range ( 16 ): cv_train = np.zeros( len (train_label)) cv_pred = np.zeros( len (test_id)) params[ 'seed' ] = s if cv_only: kf = kfold.split(X, train_label) best_trees = [] fold_scores = [] for i, (train_fold, validate) in enumerate (kf): X_train, X_validate, label_train, label_validate = \ X[train_fold, :], X[validate, :], train_label[train_fold], train_label[validate] dtrain = lgbm.Dataset(X_train, label_train) dvalid = lgbm.Dataset(X_validate, label_validate, reference=dtrain) bst = lgbm.train(params, dtrain, num_boost_round, valid_sets=dvalid, feval=evalerror, verbose_eval= 100 , early_stopping_rounds= 100 , ) best_trees.append(bst.best_iteration) cv_pred += bst.predict(X_test, num_iteration=bst.best_iteration) cv_train[validate] += bst.predict(X_validate) score = Gini(label_validate, cv_train[validate]) print (score) fold_scores.append(score) cv_pred /= NFOLDS final_cv_train += cv_train final_cv_pred += cv_pred print ( "cv score:" ) print (Gini(train_label, cv_train)) print ( "current score:" , Gini(train_label, final_cv_train / (s + 1. )), s+ 1 ) print (fold_scores) print (best_trees, np.mean(best_trees)) x_score.append(Gini(train_label, cv_train)) print (x_score) pd.DataFrame({ 'id' : test_id, 'target' : final_cv_pred / 16. }).to_csv( 'model/lgbm3_pred_avg.csv' , index= False ) pd.DataFrame({ 'id' : train_id, 'target' : final_cv_train / 16. }).to_csv( 'model/lgbm3_cv_avg.csv' , index= False ) 公開解法でのsubmit Private Scoreで0.29097。5169チーム中46位のスコアとなり、シルバーメダル圏内に入りました。 コンペは終了しているので、もちろんスコアボードの本体は更新はされません。 なお、実際のコンペでは、 カーネル の著書から他のNeral Networkでの予測値の平均と記載があるので、2位のsubmitの再現というわけにならないようです。 しかし、このようなシンプルな方法でシルバーメダルのスコアを取れるのは、個人的にもKaggleに積極してみたいという励みになったと感じています。 ハイパーパラメータのチューニング さて、ハイパーパラメータのチューニングを フレームワーク の力を借りて、ハードルをぐっと下げようという、本題に移ります。 他のKaggleのコンペや、Stack over flowで雑に調査し、パラメータの範囲を決めました。 そうしてできた修正した ソースコード が、以下のようになります。 import lightgbm as lgbm import optuna from scipy import sparse as ssp from sklearn.model_selection import StratifiedKFold import numpy as np import pandas as pd from sklearn.preprocessing import LabelEncoder from sklearn.preprocessing import OneHotEncoder def Gini (y_true, y_pred): # check and get number of samples assert y_true.shape == y_pred.shape n_samples = y_true.shape[ 0 ] # sort rows on prediction column # (from largest to smallest) arr = np.array([y_true, y_pred]).transpose() true_order = arr[arr[:, 0 ].argsort()][::- 1 , 0 ] pred_order = arr[arr[:, 1 ].argsort()][::- 1 , 0 ] # get Lorenz curves L_true = np.cumsum(true_order) * 1. / np.sum(true_order) L_pred = np.cumsum(pred_order) * 1. / np.sum(pred_order) L_ones = np.linspace( 1 / n_samples, 1 , n_samples) # get Gini coefficients (area between curves) G_true = np.sum(L_ones - L_true) G_pred = np.sum(L_ones - L_pred) # normalize to true Gini coefficient return G_pred * 1. / G_true cv_only = True save_cv = True full_train = False def evalerror (preds, dtrain): labels = dtrain.get_label() return 'gini' , Gini(labels, preds), True path = "input/" train = pd.read_csv(path+ 'train.csv' ) #train = train.sample(frac=0.1, random_state=0).reset_index(drop=True) train_label = train[ 'target' ] train_id = train[ 'id' ] test = pd.read_csv(path+ 'test.csv' ) #test = test.sample(frac=0.1, random_state=0).reset_index(drop=True) test_id = test[ 'id' ] NFOLDS = 4 kfold = StratifiedKFold(n_splits=NFOLDS, shuffle= True , random_state= 218 ) y = train[ 'target' ].values drop_feature = [ 'id' , 'target' ] X = train.drop(drop_feature,axis= 1 ) feature_names = X.columns.tolist() cat_features = [c for c in feature_names if ( 'cat' in c and 'count' not in c)] num_features = [c for c in feature_names if ( 'cat' not in c and 'calc' not in c)] train[ 'missing' ] = (train==- 1 ).sum(axis= 1 ).astype( float ) test[ 'missing' ] = (test==- 1 ).sum(axis= 1 ).astype( float ) num_features.append( 'missing' ) train.shape for c in cat_features: le = LabelEncoder() le.fit(train[c]) train[c] = le.transform(train[c]) test[c] = le.transform(test[c]) # 事前にlabelEncoderを行っているから、この使い方でユニークな値で割り当てられる。引数categories = 'auto'で警告を消す enc = OneHotEncoder(categories= 'auto' ) enc.fit(train[cat_features]) X_cat = enc.transform(train[cat_features]) X_t_cat = enc.transform(test[cat_features]) ind_features = [c for c in feature_names if 'ind' in c] count= 0 for c in ind_features: if count == 0 : train[ 'new_ind' ] = train[c].astype( str )+ '_' test[ 'new_ind' ] = test[c].astype( str )+ '_' count += 1 else : train[ 'new_ind' ] += train[c].astype( str )+ '_' test[ 'new_ind' ] += test[c].astype( str )+ '_' cat_count_features = [] for c in cat_features+[ 'new_ind' ]: d = pd.concat([train[c],test[c]]).value_counts().to_dict() train[ '%s_count' %c] = train[c].apply( lambda x:d.get(x, 0 )) test[ '%s_count' %c] = test[c].apply( lambda x:d.get(x, 0 )) cat_count_features.append( '%s_count' %c) train_list = [train[num_features+cat_count_features].values, X_cat] test_list = [test[num_features+cat_count_features].values, X_t_cat] X = ssp.hstack(train_list).tocsr() X_test = ssp.hstack(test_list).tocsr() def objective (trial): drop_rate = trial.suggest_uniform( 'drop_rate' , 0 , 1.0 ) feature_fraction = trial.suggest_uniform( 'feature_fraction' , 0 , 1.0 ) learning_rate = trial.suggest_uniform( 'learning_rate' , 0 , 1.0 ) subsample = trial.suggest_uniform( 'subsample' , 0.8 , 1.0 ) num_leaves = trial.suggest_int( 'num_leaves' , 5 , 1000 ) verbosity = trial.suggest_int( 'verbosity' , - 1 , 1 ) num_boost_round = trial.suggest_int( 'num_boost_round' , 10 , 100000 ) min_data_in_leaf = trial.suggest_int( 'min_data_in_leaf' , 10 , 100000 ) min_child_samples = trial.suggest_int( 'min_child_samples' , 5 , 500 ) min_child_weight = trial.suggest_int( 'min_child_weight' , 5 , 500 ) params = { "objective" : "binary" , "boosting_type" : "gbdt" , "learning_rate" : learning_rate, "num_leaves" : num_leaves, "max_bin" : 256 , "feature_fraction" : feature_fraction, "verbosity" : verbosity, "drop_rate" : drop_rate, "is_unbalance" : False , "max_drop" : 50 , "min_child_samples" : min_child_samples, "min_child_weight" : min_child_weight, "min_split_gain" : 0 , "min_data_in_leaf" : min_data_in_leaf, "subsample" : subsample } x_score = [] final_cv_train = np.zeros( len (train_label)) final_cv_pred = np.zeros( len (test_id)) cv_train = np.zeros( len (train_label)) cv_pred = np.zeros( len (test_id)) params[ 'seed' ] = 0 kf = kfold.split(X, train_label) best_trees = [] fold_scores = [] for i, (train_fold, validate) in enumerate (kf): print ( 'kfold_index:' , i) X_train, X_validate, label_train, label_validate = \ X[train_fold, :], X[validate, :], train_label[train_fold], train_label[validate] dtrain = lgbm.Dataset(X_train, label_train) dvalid = lgbm.Dataset(X_validate, label_validate, reference=dtrain) bst = lgbm.train(params, dtrain, num_boost_round, valid_sets=dvalid, feval=evalerror, verbose_eval= 100 , early_stopping_rounds= 100 ) best_trees.append(bst.best_iteration) cv_pred += bst.predict(X_test, num_iteration=bst.best_iteration) cv_train[validate] += bst.predict(X_validate) score = Gini(label_validate, cv_train[validate]) print (score) fold_scores.append(score) cv_pred /= NFOLDS final_cv_train += cv_train final_cv_pred += cv_pred print ( "cv score:" ) print (Gini(train_label, cv_train)) print ( "current score:" , Gini(train_label, final_cv_train / (s + 1. )), s+ 1 ) print (fold_scores) print (best_trees, np.mean(best_trees)) x_score.append(Gini(train_label, cv_train)) print (x_score) pd.DataFrame({ 'id' : test_id, 'target' : final_cv_pred / 16. }).to_csv( 'model/lgbm3_pred_avg_2.csv' , index= False ) pd.DataFrame({ 'id' : train_id, 'target' : final_cv_train / 16. }).to_csv( 'model/lgbm3_cv_avg_2.csv' , index= False ) return ( 1 - x_score[ 0 ]) study = optuna.create_study() study.optimize(objective, n_trials= 150 ) パラメータの設定の範囲を抜粋すると以下のようになります。 drop_rate = trial.suggest_uniform( 'drop_rate' , 0 , 1.0 ) feature_fraction = trial.suggest_uniform( 'feature_fraction' , 0 , 1.0 ) learning_rate = trial.suggest_uniform( 'learning_rate' , 0 , 1.0 ) subsample = trial.suggest_uniform( 'subsample' , 0.8 , 1.0 ) num_leaves = trial.suggest_int( 'num_leaves' , 5 , 1000 ) verbosity = trial.suggest_int( 'verbosity' , - 1 , 1 ) num_boost_round = trial.suggest_int( 'num_boost_round' , 10 , 100000 ) min_data_in_leaf = trial.suggest_int( 'min_data_in_leaf' , 10 , 100000 ) min_child_samples = trial.suggest_int( 'min_child_samples' , 5 , 500 ) min_child_weight = trial.suggest_int( 'min_child_weight' , 5 , 500 ) なお、Optuna自体の使用方法は、下記の記事と公式リファレンスを参考させていただきした。 https://qiita.com/ryota717/items/28e2167ea69bee7e250d https://optuna.readthedocs.io/en/stable/index.html (18/12/11 19:41追記) コメントいただけた通り、'verbosity'は、警告レベルの表示を制御するパラメータであり、予測性能の最適化としては意味の無いパラメータでした。ですので、チューニングの対象にはすべきではありませんでした。 以下のように試行回数を定めていますが、 n_trials= 150 時間が足りなくなった関係で、その時点で計算されたパラメータで最適化を中断しております。 20時間ほど回し回しましたが、ハイパーパラメータによって検証の時間は1分から60分程度となり、 100回くらいの試行数だったようです。 そうしてできてパラメータが、以下のように、2位の解法と比較すると以下のようになります。 ハイパーパラメータ 今回のチューニング結果 2位の解法 drop _rate 0.3015600134599976 0.1 feature_fraction 0.46650703511665226 0.6 learning_rate 0.004772377676601769 0.1 subsample 0.8080720420805803 0.9 num_leaves 718 15 verbosity -1 0 num_boost_round 1942 10000 min_data_in_ leaf 212 150 min_child_samples 68 10 min_child_weight 151 150 2位コンペとの解法とは、雰囲気が異なるセットとなり、公開解法の再現ということにはならないようです。 K_fold=4 でやっていることも異なる要因になると思います。 算出できたハイパーパラメータでsubmit 最初のpython3の スクリプト からパラメータを入れ替え、予測値を算出しました。 K_fold =4, また、ランダムシートの数を16から4に減らしております。 結果 スコアは下がってます。 1176位相当。。ハイパーパラメータ次第でシルバーメダル圏内ということを考えると、微妙な結果です。 所感 結果としては残念ですが、grid searchだけに頼らない、ハイパーパラメータの最適化方法の導入のきっかけになりました。 また、非常に手軽に使えたというのもあり、今後もチューニングの場面でOptunaを活用してみたいと思います。 反省としては、探索するハイパーパラメータの設定が悪く、計算の効率化が著しく悪くなった恐れがあります。 validationの際に、fold数の全て計算するのではなく、スコアが下がらなそうなら、そのハイパーパラメータの計算をやめるとか、一定時間以上かかってしまったらまた、次に試行に移るとかできれば効率化できたように思えます。 フレームワーク は ブラックボックス でもある程度は動かすことができますが、やはり中身をある程度理解しないと遠回りしてしまうというのは、当然の結果と言えます。 もっと使いこなせるよう精進しなければと思いました。 公式リファレンスでも、OptunaでLightGBMをチューニングする例が出ており、そちらの例も参考にしながらリベンジしたいと思います。 github.com 最後にですが、この記事が何かの役に経てば幸いです。
アバター
0. はじめに 18年10月にKotlinのコルーチンがexperimentalからstableになりました。 遅ればせながら、コルーチンを触ってみました。 この記事は、これからコルーチンを学習する人向けの記事です。 *Kotlin1.3、 kotlinx-coroutines1.0.1の環境です。 *Kotlinが初めての方は、 こちら で気軽に試せるので触ってみてください。先頭に import kotlinx.coroutines.* を忘れずに。 1. コルーチンとは Wikipedia から引用します。 コルーチン(英: co-routine)とはプログラミングの構造の一種。サブルーチンがエントリーからリターンまでを一つの処理単位とするのに対し、コルーチンはいったん処理を中断した後、続きから処理を再開できる。 どういうことなのか。簡単なプログラムを例にして説明をします。 fun main() { /* ここからコルーチン */ println( "start foo" ) 時間のかかる処理 println( "end foo" ) /* ここまでコルーチン */ println( "bar" ) } 例えば"start foo"から"end foo"をコルーチンとして実行することで、時間のかかる処理のタイミングでmainスレッドがその処理を中断し、中断中は別の処理をすることができます。 ここでは、中断中は"bar"を表示させることにします。 よって、出力結果をこのようなります。 start foo bar end foo 2.初めてのコルーチン それでは実際にコルーチンを作成して、スレッドが中断して再開するところをみてみます。 作成するプログラムは、 1.コルーチンとは のプログラムに、コルーチンを適用します。説明通りの結果になるか確認します。 コルーチンを作成するにはコルーチンビルダーというものを使います。 コルーチンビルダーには様々ありますが、ここではもっともシンプルな launch関数 を使います。 使い方は簡単です。 launch関数 にコルーチンとして実行するラムダを渡します。 fun main() { GlobalScope.launch { println( "start foo" ) delay( 1000 ) println( "end foo" ) } println( "bar" ) } これで"start foo"から"end foo"まではコルーチンとして実行されます。 なお、 GlobalScope と delay関数 はあとで説明します。 delay関数 は Thread.sleepメソッド のようなものだと現時点では思っておいてください。 「時間のかかる処理」を delay関数 で代替しています。引数として中断したい時間をミリ秒単位で指定できます。 結果はこのようになります。("start foo"が表示されないこともあります。) bar start foo 想定した出力結果になりませんでした。 まず、"bar"が先に表示されてしまいました。 これは launch関数 がコルーチンの実行をスケジュール化だけして、処理を先に進めてしまうからです。 また、"end foo"が表示されませんでした。原因は、"end foo"から処理を再開をする前にmain関数からリターンして、プログラム自体が終了してしまうからです。 launch関数 では、mainスレッドの実行を止めることできないので、何か工夫が必要です。 launch関数 の代わりに runBlocking関数 というコルーチンビルダーを使うことにします。 runBlocking関数 は、コルーチンが完了するまで呼び出し出し元のスレッドを停止させるコルーチンビルダーです。 fun main() { runBlocking { println( "start foo" ) delay( 1000 ) println( "end foo" ) } println( "bar" ) } 当然ですが、これでも期待した出力結果にはなりません。 start foo end foo bar なぜなら runBlocking関数 をコールした時点で、コルーチンの処理が終わるまで呼び出し元のスレッドがブロックされるからです。(出力結果として想定したものではありませんが、 delay関数 のポイントで中断および再開はしています。) それでは先の launch関数 と組み合わせたらどうなるでしょうか。 fun main() { runBlocking { launch { println( "start foo" ) delay( 1000 ) println( "end foo" ) } delay( 500 ) println( "bar" ) } } 先述したように launch関数 はコルーチンの実行をスケジュール化して処理を先に進めてしまうので、"start foo"が表示される前に"bar"が表示されてしまいます。 これを防ぐために"bar"の直前に delay(500) を置きます。 (前回と違い、 launch関数 を呼び出す際に GlobalScope がない理由はあとで説明します。) 結果はこのようになりました。 start foo bar end foo 想定した出力結果になりました。 どのスレッドで各々が実行されているか調べてみましょう。 また、少しだけKotlinっぽく書いてみます。 fun main() = runBlocking { launch { println( " $threadName :start foo" ) delay( 1000 ) println( " $threadName :end foo" ) } delay( 500 ) println( " $threadName :bar" ) } val threadName: String get () = Thread.currentThread().name main:start foo main:bar main:end foo 中断する前の処理、中断中の処理、中断から再開した処理、全てmainスレッドで実行されていることが確認できました。 なお、このプログラムは2回中断が発生しています。 launch コルーチンのスケジュール化 →  delay(500) で中断(1回目) →  launch の実行開始 → delay(1000) で中断(2回目) → delay(500) から再開 → delay(1000) から再開 3.中断はいつ発生するのか コルーチンの実行が中断され、そして再開される様子を見ることができましたが、中断とはどういう時に発生するのでしょうか。 ドキュメントに このような 記載があります。 Suspending functions can be used inside coroutines just like regular functions, but their additional feature is that they can, in turn, use other suspending functions, like delay in this example, to suspend execution of a coroutine. サスペンド 関数は、コルーチンの中で通常の関数のように使えます。通常の関数との違いは、 サスペンド 関数はコルーチンの実行を中断するために、他の サスペンド 関数を使うことです。(この例のdelayのように) サスペンド 関数という新しい用語が出てきました。 サスペンド 関数とはこのように関数の先頭に susupend修飾子 がついた関数のことです。 suspend fun hoge() このドキュメントによると サスペンド 関数をコールすることで中断が発生するようです。 確かに delay関数 の定義にもこのように suspend修飾子 がついています。 public suspend fun delay(timeMillis: Long ) それでは、 delay関数 のように中断を起こす サスペンド 関数を作成してみましょう。 せっかくなので、中断から再開するときに値を返す サスペンド 関数を作成してみます。 今回は4096bitで表現可能な 素数 を返す getPrimeNumber関数 を作成します。 getPrimeNumber関数 の利用側はこのようにします。 fun main() = runBlocking { println( " $threadName :start runBlocking" ) launch { println( " $threadName :start launch" ) val prime = getPrimeNumber() println( " $threadName :prime number = $prime " ) println( " $threadName :end launch" ) } delay( 500 ) println( " $threadName :end runBlocking" ) } 大体の流れは、 getPrimeNumber関数 をコールしたらmainスレッドはコルーチンを中断 → その間に"end Blocking"を表示 →  素数 が求め終わったら、 素数 を表示させるところから再開 です。 次に、 サスペンド 関数である getPrimeNumber関数 はどう作成すればいいのでしょうか。 まずは、 素数 を求めるコードを書く必要がありますが、 BigInterger.probablePrime という 素数 を求めるのに便利なメソッドがあります。 このメソッドの詳しい使い方は割愛しますが、 BigInterger.probablePrime(4096, Random()) が 素数 (正確には「おそらく 素数 」)を返してくれます。私の手元のマシンでは呼び出してから返ってくるまでに10秒程度かかりました。 次に実際に中断を起こすコードを書いていきます。 suspend fun getPrimeNumber() = BigInterger.probablePrime( 4096 , Random()) このように書ければシンプルですが、このようにしても getPrimeNumber関数 で中断されず、mainスレッドが 素数 を求めるために停止してしまいます。 スレッドを中断させるには suspendCoroutine関数 をコールする必要があります。 suspendCroutine関数 はこのように定義されています。この関数も サスペンド 関数です。 inline suspend fun suspendCoroutine( crossinline block: (Continuation) -> Unit ): T ラムダが受け取る Continuationインターフェース にはこのような拡張関数が定義されています。 fun Continuation.resume(value: T) この resumeメソッド をコールすることで、コルーチンが再開します。 それでは中断はいつ発生するのでしょうか。 あえて、 resumeメソッド をコールせず、このようにして実行してみてください。 suspend fun getPrimeNumber() { println( " $threadName :hoge" ) suspendCoroutine { println( " $threadName :fuga" ) } println( " $threadName :piyo" ) } 結果はこのようになります。 main:start runBlocking main:start launch main:hoge main:fuga main:end runBlocking また、このプログラムは永遠に終了しません。なぜなら、コルーチンが再開しないためです。 この結果をみると、"fuga"の後に"end ranBlocking"が表示されているので、"fuga"を表示後、つまり suspendCoroutine関数 に渡したラムダの実行終了後に中断が発生している ことがわかります。 これが中断が発生するタイミングです。 今度は、中断が発生後、約1秒経過してから resumeメソッド をコールして再開してみます。 suspend fun getPrimeNumber() { println( " $threadName :hoge" ) suspendCoroutine { cont -> println( " $threadName :fuga" ) Thread { Thread.sleep( 1000 ) cont.resume( 1234 ) }.start() } println( " $threadName :piyo" ) } 中断から再開しました。 main:start runBlocking main:start launch main:hoge main:fuga main:end runBlocking main:piyo main:prime number = kotlin. Unit main:end launch また、 getPrimeNumber関数 は 素数 を返さないので kotlin.Unit と表示されてしまっています。 それでは getPrimeNumber関数 が 素数 を返すように変更します。 resumeメソッド に渡した値が suspendCoroutine関数 の戻り値になるので、このように書けます。 suspend fun getPrimeNumber(): BigInteger = suspendCoroutine { cont -> Thread { cont.resume(BigInteger.probablePrime( 4096 , Random())) }.start() } これで、先ほどの結果で kotlin.Unit となっていた箇所に 素数 が表示されます。 目的である中断の発生タイミングについて、確認できました。 4.コルーチンビルダーについて少し詳しく これまでで、 launch関数 や runBlocking関数 の2つのコルーチンビルダーを使いました。 この2つ以外にも様々なコルーチンビルダーが提供されています。 例えば、先ほど 素数 を求めるために作成した getPrimeNumber関数 ですが、 withContext関数 というコルーチンビルダーを使うとこのように書けます。 suspend fun getPrimeNumber() = withContext(Dispatchers.Default) { BigInteger.probablePrime( 4096 , Random()) } このコルーチンビルダーは値を返すことができます。 また、第一引数に値を指定することで、コルーチンを実行するスレッドを切り替えています。 実用的なコルーチンビルダーは他にもありますが、この記事ではそれらを紹介しません。 ここでは、この記事でまだ触れていない重要な2つの内容について説明します。 コルーチンビルダーは、中断可能な世界へのエントリーポイントのようなもの コルーチンスコープが必要なコルーチンビルダー 中断可能な世界へのエントリーポイント まずは1つ目です。 サスペンド 関数として getPrimeNumber関数 を作成し、コールすることでコルーチンが中断されることを見ましたが、このようなコードは コンパイル エラーになります。 fun main() { val primeNumber = getPrimeNumber() } 理由は、 サスペンド 関数は サスペンド 関数もしくは サスペンド ラムダ からし かコールできない というルールがあるからです。 通常のラムダと サスペンド ラムダの違いは、関数と同様に suspend修飾子 の有無です。 例えば、 launch関数 の定義はこのようになっています。 fun CoroutineScope.launch( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> Unit ): Job (source) block の型をみると suspend修飾子 がついているのがわかります。これは サスペンド ラムダを受け取ることを表しています。 このように コルーチンビルダーは サスペンド ラムダを受けることで中断可能な世界へのエントリーポイントを提供しています。 コルチーンスコープ 次に2つ目のコルチーンスコープについて。 launch関数 の定義を見ていただくと、 launch関数 は CoroutineScopeインターフェース の拡張関数として定義されているのがわかります。 fun CoroutineScope.launch(..) よって、 launch関数 をコールするには CoroutineScope の インスタンス が必要です。 最初の方のコードで GlobalScope.launch と書いていたのはそのためです。 GlobalScope は CoroutineScopeインターフェース を実装した インスタンス です。 object GlobalScope : CoroutineScope launch関数 をコールするには CoroutineScope の インスタンス が必要ですが、 runBlocking関数 に渡すラムダ内では GlobalScope.launch{..} ではなく、シンプルに launch{..} と書けます。 この理由は runBlocking関数 の定義をみるとわかります。 fun runBlocking( context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T ): T (source) block のレシーバの型は CoroutineScope となっています。 これが理由で、 runBlocking関数 に渡すラムダ内では、 GlobalScope.launch と書く必要がなかったのです。 レシーバ付きラムダに馴染みがない方のために、少し補足します。 あえて this を使って書くとこのようになります。 runBlocking { this .launch {..} } この this は、runBlocking関数が作成した CoroutineScope の インスタンス を参照しています。 補足 コルーチンスコープが導入されたのはkotlinx-coroutines0.26.0からです。 0.26.0がマークされたのが18年9月です。0.26.0より古いバージョンを前提に書かれた記事では GlobalSope がないコードを見ることがあるかもしれません。 // 0.26.0より前 fun main() { launch {..} } // 0.26.0以降 fun main() { GlobalScope.launch {..} } 5.終わりに 予定では CoroutineScope 、 CoroutineContext 、 Job についても書くつもりでしたが、記事が長くなってしまったので、全く触れられませんでした。 コルーチンを使った実用的なコードも同様です。 コルーチンを勉強をしている身ではありますが、何かの機会があれば、それらについても書いてみたいと思います。 この記事が、これからコルーチンを初める方に少しでも役に立てば幸いです。
アバター
こんにちは、Enigmo 新卒エンジニアの @sean0628_i です。 Enigmo Advent Calendar 2018 8日目の記事です。 ちなみに、 Enigmo は、海外通販サイト BUYMA や、世界中のトレンドをお届けするファッションメディア STYLE HAUS を運営する会社です。 Enigmo では、 「社内ビール制度」 が存在し、定時の18:30以降 ビールが飲み放題 です。 毎晩定時後は宴会が繰り広げられて、、、ということはなく、日によっては金曜日ですらも人が疎ら、なんていうこともあります。。。 さてはEnigmo 社員はビールが嫌いなのか?とも思いましたが、実際に話を聞いてみると当然そんなことはありませんでした(安心 実際の声 みんなで楽しくビール飲みたいけど、みんな忙しそうだし。。。 声を掛けるのは気が引ける。。。 社員Tさん さらに時々こんな声も、 呑んでたなら教えてよ。。。言ってくれれば参加したのに。。。 社員Yさん ニーズはあるのに、多数障壁があるようです これは、 「社内ビール制度」 始まって以来の危機だと思い、新卒エンジニアは立ち上がりました。 そうだ、 Amazon Dash Button を使ってこの危機を乗り越えよう。 内容 Amazon Dash Button (以下、Button)とは? Buttonの仕組み # DHCP # ARP request/responce # MAC address Hackの流れ 気づき・学び 今後の展望 Amazon Dash Button とは? Amazon ダッシュ ボタンは、注文商品を簡単かつ迅速に作成できるように設計された小型の電子デ バイス である。 出典: Wikipedia - Amazon Dash 一般的な利用方法 Amazon 公式アプリに Wi-FI & Bluetooth 経由で連動させる アプリ上でButton に登録したい商品を選択 Step2 で設定した商品がほしくなったらButton を押す 数日後、商品が手元に届く Button の仕組み Buttonの押下により、電源オン アプリで設定しておいた、 Wi-Fi に接続 DHCP でIPを取得 商品注文リク エス ト とここで文系新卒の私は DHCP とはなんぞやと思ったわけです。。。 で、調べました。 利用技術 DHCP とは? コンピューターに IPアドレス を自動割り当てする仕組み IPアドレス は、いわゆる住所のようなもの (IPは 8.8.8.8 こんな感じの数字のやつ) と、 DHCP はなんとなく理解したものの、次なる敵が現れました。 MAC address 、 ARP です。。。 もう横文字嫌だー、と思いながらも開発に必要なため背に腹は代えられず調べました。 MAC address とは? 識別に利用される原則一意の 物理アドレス ARP とは? IP から MAC を調べる仕組み のことらしい。。。 出典: What is ARP and ARP spoofing? Hack の流れ ARP を利用し、Buttonの MACアドレス を取得* Buttonの MACアドレス を監視 Buttonからの ARP Requestをキャッチ 任意の処理をする(今回はSlackにmessageを投下) *: 初回のみ。実際Buttonが押されてからの流れは2~3 実際の実装 長くなるので割愛します。 ソースコード (Sean0628/dash_button): https://github.com/Sean0628/dash_button テスト運用 ↓こんな感じ :tada: *: リゾート とは、Enigmo 社員の憩いの場所 気づき・学び 普段業務では主に Ruby を使っていて、node を初めて使いました。 しかし、やってみると意外に簡単。メンタルブロックをいかに外すかが大切なのだと感じました。 業務では関わることのできないネットワークの分野も面白いなぁと思いました。 機会があれば深掘りしてみたいなぁと。 今後の展望 勉強も兼ねて不要なPCに CentOS を入れて、 CentOS をサーバにしてButton を活用したいと思っています。 まだ、本格的なリリースまでは行けてないので、早く始動させてみんなで楽しくビールを飲みたいと思います。 最後まで読んでいただきありがとうございます。 参考: Dash Buttonの設定を管理する 参考: Enabling Interactions with Bots
アバター
Enigmo Advent Calendar 2018 の7日目の記事です。 概要 Enigmo の Steven です。 プログラミング言語 に対して興味を持ってますので、今日は Ruby について話したいと思います。 Ruby は オブジェクト指向 だと言ったら、反対する人は多分いないと思いますが、 関数型言語 の特徴も持ってると言ったら、ピンとこない人はそれほど少なくはないかと思います。 それでも、 Ruby プログラマー でしたら、 関数型言語 から受け継がれたそういう機能はおそらく毎日使っています。 そういう機能がなかったら、 Ruby は世界中で使われてる現在の言語にならなかったかもしれません。 この記事ではその機能を説明して、 Ruby の理解と 関数型言語 に関する知識が少し深まる機会になれればと思います。 Ruby の特徴について Wikipedia によると、 Ruby は 10個以上の プログラミング言語 から影響を受けて作られました。 その中には Python 、 C++ 、 Smalltalk などの オブジェクト指向 プログラミング言語 は複数ありますが、その中には Lisp もあります。 Lisp というのは現在、一つの言語というより、言語のファミリーですが、そのファミリーの言語は特徴的な文法を用いてることで有名でしょう。 ただし、 Ruby では括弧は Lisp と比べて極めて少ないですし、文法も全然違うので、最初は関係を疑うかと思いますが、言語の根本的なところで Lisp の一部の特徴を確認できます。 Lisp から引き継がれたもの 条件式の場合、 Ruby の世界では nil と false は偽として解釈されます。それ以外の値はすべて真になります。 PHP などの言語と比べて、極めて簡単でわかりやすいルールですが、その特徴は Lisp からそのまま引き継がれました。 それに関しては Clojure ( jvm 上の Lisp )は Ruby と全く一緒です。 Common Lisp では false という値はないですが、それ以外は一緒です。 もう一つの特徴としては、 Lisp と同じく Ruby には文はなく、すべては式です。 C などの言語では if文、関数の定義などは文であって、値として扱えないのですが、 Ruby ではどんなものも式であって、値として扱えます。 if then else end は該当のブランチの値を返しますし、多くの場合はそんなに役に立たないと思いますが、 def foo; end はメソッド名をシンボルとして返します。 以上は Lisp 由来の特徴の一部ですが、引き継がれたものの中で一番影響が大きかったのは、 関数型プログラミング だと言えます。 Ruby の関数の扱い どんな段階で特定の言語が 関数型言語 になるかというのは定かではなくて、判断しにくい時がありますが、 Ruby ではやはり オブジェクト指向 の面が一番強いので、 関数型言語 だと言えません。 ただし、 Ruby ではある程度 関数型プログラミング が可能だと主張しても、誤りではないでしょう。 Ruby で 関数型プログラミング ができるのは Ruby の関数の扱いのおかげです。 厳密に言うと、 Ruby には関数はなく、すべてはメソッドですが、説明がより簡潔になるよう、以下の説明では両方が同じだと一旦みなしてください。 第一級関数(first-class functions) 第一級関数を提供する言語では関数を単なる値として扱えます。他の関数に引数として渡すこともできれば、関数から返すのはもちろん、変数に保存することもできます。 Ruby では既存のメソッドを値として扱うにはまずメソッドを Method か Proc オブジェクトとして抽出する必要がありますが、それができれば、そのオブジェクトを他のオブジェクトと何の違いもなく自由に扱えます。 def add (a, b) a + b end def apply (fn, *args) fn.call(*args) end add = method( :add ) apply(add, 22 , 44 ) # => 66 無名関数(anonymous functions) 無名関数はその名前の通り、名前のない関数を表しています。それだけです。 名前を与える必要がなくなると、整数と文字列と同じく、関数はただの リテラル になります。 Ruby では proc や lambda で無名な関数オブジェクトをもちろん生成できますが、ブロックも無名関数の一種だと言えます。 my_proc = proc { puts 1 } my_lambda = lambda { puts 2 } def with_my_block yield 3 end with_my_block { | x | puts x } クロージャ (closures) 関数を値として使える言語では、 クロージャ は、関数の内側から、関数定義時に関数の外側にしか存在しなかった変数名(グローバル以外にも)を参照することを可能にします。 Ruby でも クロージャ のサポートを確認できます。 def foo (n) o = n * 2 lambda { | p | o + p } # o は lambda の引数から受けられず、foo のローカル変数を指しています end bar = foo( 10 ) bar.call( 5 ) # => 25 無名関数と クロージャ は時々一緒くたにされることがありますが、違うものです。 無名関数がないが、 クロージャ がある言語と、その逆の言語を想像できます。 高階関数 (higher-order functions) 高階関数 は以上のものと違って、機能ではなく関数の一種類の名前です。 高階関数 というのは、他の関数を引数として受けるか、関数を返り値として返すか、それともその両方を行う関数を表しています。 どちらかが欠けてると、多少不便になるので、以上で紹介した機能をベースに、 高階関数 を可能にする プログラミング言語 は多いです。 Ruby では 高階関数 は多いですが、一番始めに頭に浮かぶのは Enumerable のメソッドです。 multiplier = 5 # map という関数はブロックを関数として受け取ってます [ 1 , 2 , 3 ].map { | n | n * multiplier } # => [5, 10, 15] したがって、 Ruby でプログラミングをしているのであれば、おそらく毎日、ある程度の 関数型プログラミング をしているということになります。 関数型プログラミング のいいところ Enumerable のメソッドを好む人は Ruby プログラマー のほぼ全員だと思いますが、そのようなプログラミングがなぜいいのかをもっと具体的に説明しましょう。 関数を組み合わせることで量の少ないコードでもかなりな処理を行えます 高階関数 になると、再利用できる関数は多く、 アルゴリズム をそれぞれの関数の組み合わせとして実装できます 命令型プログラミングと オブジェクト指向 プログラミングと比べて、ステートを扱うことが少ないので、ステートによるバグがより少ない 特徴は他にありますが、まとめて言いますと、 関数型言語 では数学により近い形でプログラムを実装すると言えます。 関数型プログラミング 向けの gem Ruby で以上のように 関数型プログラミング ができますが、 Ruby はあくまでも オブジェクト指向 の言語なので、それなりの限界があります。 高階関数 をライブラリーに追加することは簡単ですが、 Haskell や OCaml のような 関数型言語 に近づかせるにはかなりの努力が要ります。 それも度を越えると、 Ruby が Ruby じゃなくなって、デメリットの方が大きくなることがあるかと思います。 それでも場合によってはほかの 関数型言語 にある機能を Ruby に持ってくるメリットがありますので、以下ではいくつかの機能とそれを提供する gem を紹介します。 永続データ構造 永続データ構造では既存の値を変更することは不可能で、もとの値から新しい値を生成することしかできません。 新しい値の生成時に既存のデータ構造と一部の構造が共有されるので、 Hash と Array などをまるごとコピーするより早いです。 一見では追加の制限しかかけられてないと見えますが、新しい値をしか生成できないというのは逆に保障でもあって、変更してはならないデータ構造を気づかずに変更してしまうというバグが完全になくなります。 並行計算ではスレッドの間でデータ交換を行う時に伴う心配もなくなるほか、たまに見かける #dup と #freeze のメソッド呼び出しも不要になります。 関数型言語 に限る機能ではなく、それぞれの オブジェクト指向言語 にも普及しつつある機能であって、 Ruby でもこれから String が完全にイミュータブルになることから見て、 Ruby では将来的にそのデータ構造が公式的に導入されるかもしれません。 Hamster パターンマッチング Ruby では値を比べるには == はもちろん、 case when end で使われる === もあります。 ただし、そのメソッドで値が同じであるかどうかを簡単に判断できても、データの一部だけ(いわゆるデータのパターン)を簡単に比べるのはもっと難しいです。 その問題を解決するため、データの比較をより汎用的に行えるように、パターンマッチングという機能が存在します。 パターンマッチングでは、比較したいパターンを定義することで、データがパターンにマッチした場合、もしくはマッチしなかった場合の処理を指定できます。 パターンがいくら複雑でも定義可能なので、複雑なデータ構造の比較時に特に役に立ちます。 例のパターンとして以下のはどうでしょうか? 値が配列で、最初の要素がハッシュである必要があるが、ほかの要素は気にしません ハッシュに :foo と :bar のキーが必須ですが、追加で他のキーがあって問題にしません :foo に対する値は 10 の整数である必要があります Ruby でパターンマッチングを可能にする gem の中には以下のがあります。 pattern-match Qo モナド Array に対して使える #map と #flat_map メソッドは人気でしょう。 ただし、 Array での使い方が言語を問わず一番普及しているとはいえ、 #map と #flat_map は配列限定のメソッドではありません。 #map はコレクションの各要素に対して任意な関数を適用して、その結果をもとの要素に置き換える処理を行います。 Array の場合、ここでいうコレクションは配列ですが、 #map を提供できるコレクションは他にもいろいろあります。 配列は 0 以上の要素を含むとして、 Maybe は 0 か一つの要素を含むコレクションです。 フューチャーでしたら、将来的に一つの要素を含むことになりますが、現在はまだないかもしれません。 全然違うものに見えるかもしれませんが、以上のものはすべて要素を含むコレクションとして一緒です。 そのコレクションでも #map は含まれてる要素に対して任意の関数を適用する処理を行うことになります。 #flat_map はコレクションの各要素に、その要素を引数にしながら、同じ種類のコレクションを返す関数を、適用する処理を行います。 関数から返された各コレクションを、コレクションの種類によって、連結したり、そのまま返したりします。 #flat_map と一緒に使われる時、最終的の結果を変更しない関数も使用可能な場合は(以下は #pure と呼ぶ)、 #map は #pure と #flat_map だけを使って実装可能です(配列の場合は pure = ->(x) { [x] } )。 #pure と #flat_map を提供するデータタイプ(コレクション)は モナド と呼ばれています。 #map と #flat_map のいいところの中には以下のがあります。 高階関数 なので、再利用性は高いですが、その中でも #map と #flat_map はかなり根本的な関数なので、それをベースに、何のコレクションにも対応する関数を、実装できます。 #map と #flat_map は通常の実装で全域 写像 (total function)なので、渡された関数も全域 写像 であれば、どんなコレクションに対して適用されても、エラーになりません( Maybe のコレクションで #map と #flat_map を使えば、 nil のチェックは完全に不要になります)。 モナド を本格的に使うには協力な型を提供する静的型付けな言語が大体必要になりますが( Haskell など)、簡単な ユースケース なら、 Ruby でも以下のライブラリーなどで モナド を使えます。 Kleisli dry-monads 言語とライブラリーによって #map と #flat_map は違う名前になったりします。他によく使われる名前としては fmap(map)と bind( flat _map)はあります。 終わりに プログラミング言語 の世界では、最近の一つの流れとして、今まで 関数型言語 にしかなかった機能が少しずつ他の言語でも採用されるようになってます。 Ruby ではかなり以前から 高階関数 を使えますが、もっと最近な例として Java 8 があります。新しい言語なら、Swift と Rust もあります。 当分はその流れはおそらく止まらないので、これからも 関数型プログラミング の機能がどんどん普及して、もしかすると Ruby の方にも新しい機能が現れるかもしれません。 そうなれば、今以上にも Ruby で 関数型プログラミング のいいところを活かせるのでしょう。
アバター
エニグモ データ活用推進室 @kmt47 です。 この記事は Enigmo Advent Calendar 2018の6日目 です。 概要 redashがv4にバージョンしました。 redash v4の新機能を紹介します。 上手に新機能を使って、redash作成を効率化していきましょう! クエリ(レポート(表、グラフ)) 数値の表示形式(フォーマット)をredashで設定できるようになった(便利度:★★★★★(MAX)) これまでは、 SQL で行っていた(面倒な)表示形式の変換を、redashの設定で可能となりました。 これまでは、文字列変換(CONVERT)や、文字置換(REPLACE)を駆使して金額の12,000.00の「.00」の除去や、 YoYなどにパーセンテージ(%)の付加を行って表示形式を整形していましたが、v4ではredashの設定で可能です。 手順 ・「Table」画面の左下にある「Edit Visualization」をクリック   ※ 自分が編集できる(自分が作ったor編集権限のある)クエリの場合に「Edit Visualization」が表示されます。  ⇒ 「Visualization Editor」が表示されます ・表示形式を変更したい項目の「Number format」を変更(数値項目のデフォルト値は「0,0.00」) 「0.0」に変更することで、小数点以下が除去され「12,000」のように表示されます。 ※ 表示形式を変えると、右側の表示の形式も瞬時に変更されますので、確認ができます。 YoYのようなパーセントの項目の場合は、「0.0%」に変えることで、「123.4%」のように表示されます。 ※ パーセンテージの場合の元の値は、100を掛ける必要はない(100掛けちゃダメ) 表示形式は、以下のURLを参照してください。 http://numeraljs.com/ 項目を非表示にできるようになった(便利度:★★★★) v4以前は SQL から項目を削除する必要があったが、 SQL には残したまま、redash上で非表示にできるようなった。 表(Table)では表示させたくないが、グラフでは使いたい、といった場合も、クエリを分ける必要がなく、 1つのクエリで実現できるようになった。(開発生産性、運用保守性が格段にあがりますね!) 手順 ・「Table」画面の左下にある「Edit Visualization」をクリック ・非表示にしたい項目の項目名にある チェックボックス からチェックを外す 項目の表示順を変更できるようになった(便利度:★★) 項目の表示順を SQL の変更なしに、redashで変更できるようになりました。 手順 ・「Table」画面の左下にある「Edit Visualization」をクリック ・順番を変更したい項目をクリックして、移動したい場所に持っていきます。 項目の表示を右寄せ、中央寄せ(?)、右寄せの設定が可能になった(便利度:★★) v4以前は、数値でも、文字列でも、なんでも右寄せでしたが(なので特に数値は読みずらかった)、 項目ごとに変更できるようになりました。 手順 ・「Table」画面の左下にある「Edit Visualization」をクリック ・項目名の下にあるボタンで切り替える(説明が雑ですが、見れば分かると思います。) 表に表示する件数を変更できるようになった(便利度:★★★) これまでは15件(?)だった表示件数を変更できるようになりました。 手順 ・「Table」画面の左下にある「Edit Visualization」をクリック ・「Visuralization Name」の下の「Grid」をクリック ・「items per page」で表示したい件数を選択  ※ いまいま5, 10, 15, 20, 25から選択可能   (1か月の日数分表示するために50とか欲しいなぁ) ダッシュ ボード クエリの追加・削除・レイアウト変更の方法が変更 ダッシュ ボードにクエリ(表やグラフ)を追加・削除・レイアウト変更する方法が変更になりました。 手順 ・ ダッシュ ボードの右上の「・・・」をクリック ・「Edit」をクリック クエリの追加 ・画面右下の「Add Widget 」をクリック ・追加したクエリを選択  ※ v4以前で選択できた、表示サイズ(幅)は、ここでは選択できなくなりました。    追加した後にサイズを調整できるように変更となりました。 クエリの削除 ・削除したいクエリの右上の「×」をクリック クエリのレイアウト変更 ・移動したいクエリを選択して、マウスで移動したい場所まで移動 クエリのサイズのバリエーションが増えた(便利度:★★★) v4以前は、通常(画面の幅の半分)と2倍(画面の幅の全部)のみ選択できました。 v4になって、新たに画面の1/3のサイズにすることができるようになりました。 手順 ・ ダッシュ ボードの右上の「・・・」をクリック ・「Edit」をクリック ・サイズを変更したいクエリを選んで、クエリの枠にカーソルを置くと、カーソルが「⇔」に変わるので、クリックしてサイズを変更します。(説明が雑ですが、やれば分かります。) クエリの高さが変更できるようになった(便利度:★★★★) 上記と似てますが、幅だけではなく、v4から新たに高さを自由に変更することができるようになりました。 手順 ・ ダッシュ ボードの右上の「・・・」をクリック ・「Edit」をクリック ・高さを変更したいクエリを選んで、クエリの枠にカーソルを置くと、カーソルが「↕」に変わるので、クリックしてサイズを変更します。(説明が雑ですが、やれば分かります。)
アバター
エニグモ データ活用推進室 @kmt47 です。 この記事は Enigmo Advent Calendar 2018の5日目 です。 自己紹介 2018年4月に エニグモ に入社しました。 社会人経験、エンジニア経験は、かれこれ20年を超えました。 あえて分類するとデータベースエンジニアになるでしょうか。 SIer 時代は、ご多分に漏れず、PG, PL, PMなど経験しましたが… その頃は「DBといえば Oracle 」という時代でしたので、 Oracle は7から9iくらいまでかなりやりました。 そこからネット系の事業会社を転々と。 この記事の目的 タイトルに「はじめの一歩」とあるように、はじめて Google Spreadsheetで Google Apps Script(GAS)を利用する方向けの内容です。 どちらかというと非エンジニア向けの内容です。 新しいことを習得しようとしたときに、誰もが最初は初心者です。いろいろな壁にぶつかると思います。何時間も悩んだ挙句、気づいてみたらとても些細なことだった、なんてことも一度や二度ではないでしょう。 ただ、ある程度コツが掴めてくると、そこからは順調に事が進むようにもなりますよね。(エラーが出たときの対処の方法や、分からないことが出てきた時の調べ方のコツが身についてくるんだと思います。) ただ、コツを掴む前に挫折してしまう人もいるでしょう。 そもそも、最初の一歩すら踏み出せない方もいるでしょう。 本記事は、そんな一歩を踏み出すための「何となくできそうかも」を感じてもらうことを目的としています。なので、最小限の手順で、最小限のプログラムとし、難しい解説は省いています。 まずは「はじめの一歩」を踏み出していただければ幸いです。 GASとは? Google Apps Script(GAS)とは、 Google 社が提供する スクリプト言語 です。以上。 GASで出来ること GASでは、 Google スプレッドシート や Googleカレンダー など Google 社が提供するサービスに対して操作(処理)することができます。まぁ、いろいろできますが、それは追々分かっていけばよいと思います。 本題 目次 Google Spreadsheetを開く スクリプト エディタを開く GASを実行する スプレッドシート の値を取得する スプレッドシート に値を出力する Google Spreadsheetを開く Google ドライブから「新規」>「 Google スプレッドシート 」>「空の スプレッドシート 」を選択 空の スプレッドシート を開くことができました!!(GAS以前の話ですね) スクリプト エディタを開く 「ツール」>「 スクリプト エディタ」を選択 別のタブに スクリプト エディタが開きます。 プログラムを書くエディタらしき画面ができていましたね。(パチパチ) GASを実行する 2行目に以下を記載して、「▶」ボタンを選択 Logger.log( "Hello, World!" ); Logger.logは、引数の値をログに出力する関数です。 「表示」>「ログ」を選択、ログ画面が表示 おめでとうございます。あなたの作成したGASが見事に実行されました!(パチパチパチ) スプレッドシート の値を取得する A1のセルの値を読み込んで、ログに出力してみましょう。 ここから少しプログラミングっぽくなってきますよ。 function myFunction() { //アクティブのシートを取得 var sheet = SpreadsheetApp.getActiveSheet(); //セルA1を変数rangeに取得する var range = sheet.getRange( 'A1' ); //rangeの値をログに出力する Logger.log(range.getValue()); } SpreadsheetApp.getActiveSheet()で対象のシートを変数(sheet)に格納します。 getActiveSheet()では、アクティブ(いま選択されている)シートが選択されます。 getSheetByName(シート名)では、シート名からシートを選択できます。 次に対象のセルを変数(range)に格納します。 sheet.getRange('A1')のように、セルのアドレスを指定する方法と、 sheet.getRange(1, 1)のように、セルの行番号と列番号を指定する方法があります。 sheet.getRange(行番号, 列番号)の順で指定します。 Logger.log(range.getValue())では、指定したセルの値をログに出力しています。 A1のセルの値がログに出力されました。 スプレッドシート に値を出力する 次は、 スプレッドシート に値を出力するプログラムです。 function myFunction() { //アクティブのシートを取得 var sheet = SpreadsheetApp.getActiveSheet(); //セルA3を変数rangeに取得する var range = sheet.getRange( 'A3' ); //A3に"Hello, GAS!"を入力 range.setValue( "Hello, GAS!" ); } 「 スプレッドシート の値を取得する」と同様に SpreadsheetApp.getActiveSheet()でシートを変数(sheet)に格納し、 sheet.getRange('A3')で対象のセルを変数(range)に格納します。 そして、range.setValue("Hello, GAS!")で対象のセルに値を入力します。 A3のセルに値を出力することができました。 まとめ データを読み込んで、書き出すというプログラミングの基本を通して、GASによる Google スプレッドシート の操作について見てきました。 「何となくできそうかも」と感じていただけたでしょうか? 本格的にGASを利用するとなると、関数、変数、データ型、オブジェクト、メソッド、配列などなど、知っておく必要のあることは、いろいろあります。 ただ、いっぺんに全て理解する必要はありません。必要なことから少しずつ理解を進めていけば大丈夫です。 私はその過程で以下のサイトを大変参考にさせていただきました。 例題をもとに分かりやすく解説されていますので、是非参考にしていただければと思います。 「いつも隣にITのお仕事」 https://tonari-it.com/ 【保存版】初心者向け実務で使える Google Apps Script完全マニュアル https://tonari-it.com/google-apps-script-manual/ それでは、GASへの「はじめの一歩」を踏み出していただければ幸いです。
アバター
Enigmo Advent Calendar 2018 の4日目の記事です。 この記事の目的 Enigmoが運営している BUYMA では古代から運用している jQuery の他に、2016年頃から一部ページのフロントエンドをReact/Reduxで構築しています。 私自身もEnigmoに入社してからの約三年間でReact/Reduxアプリケーションの開発に多数携わってきましたので、そこで培った知見を共有したいと思います。 React/Reduxの利点 まずはじめに、ReactとReduxを使うメリットを再確認しておきたいと思います。 それぞれのメリットをしっかりと認識しておくことで、実装する際どう書くか迷ってしまった場合などにそのメリットを最大限活かす選択をすることができます。 Reactの利点 コンポーネント 化が容易で再利用性が高い 状態をDOMから分離できる(Stateless) Reduxのような外部ライブラリを併用すると コンポーネント 自体からも分離できる (props) => DOM Tree という関数のように扱える(同じinputなら常に同じoutputを得られる) Propsとして イベントハンドラ を渡していく アーキテクチャ なので、ロジックをまとめて 疎結合 にしやすい Reduxの利点 Reducer、ActionCreator、Componentといった利用側が書くべきモジュールはすべて純粋な関数で書ける ほとんど関数の集合だけでアプリケーションが完成する 副作用のある処理はすべて後述するMiddleware層に持っていける 上記の理由からテストが非常に書きやすい React/Reduxはこう書く!! では実際に私がReact/Reduxアプリケーションを実装する際指標としているプ ラク ティスを解説していきたいと思います。 Container Componentsの分割 Container Components とは Redux Store と connect している コンポーネント のことです。 詳細はReduxの公式Doc をご確認ください。 Container Componentsを適切に分割することでコードの見通しを良くします。 Containerの中にContainerが存在する、 Container in Container も許容します。 分割されていない例 import { connect } from 'react-redux' import * as React from 'react' import ComponentA from '../components/ComponentA' import ComponentB from '../components/ComponentB' import * as actions from '../actions' class BadContainer extends React.Component { componentDidMount() { this .props.fetch() } render() { return ( <div> <ComponentA name= {this .props.nameA } handler1= {this .props.handlerA1 } handler2= {this .props.handlerA2 } /> <ComponentB name= {this .props.nameB } handler1= {this .props.handlerB1 } handler2= {this .props.handlerB2 } /> </div> ) } } const mapStateToProps = state => { return { nameA: state.a.name, nameB: state.b.name } } const mapDispatchToProps = dispatch => { return { handlerA1() { dispatch(actions.actionA1()) } , handlerA2() { dispatch(actions.actionA2()) } , handlerB1() { dispatch(actions.actionA1()) } , handlerB2() { dispatch(actions.actionA2()) } , fetch() { dispatch(actions.fetchData()) } } } export default connect(mapStateToProps, mapDispatchToProps)(BadContainer) この例では一つのContainerで2つの コンポーネント 、ComponentAとComponentBを表示しようとしています。 今回の例では2つだけですが、今後3つ、4つと表示する コンポーネント が増えていくとその分必要なハンドラーやプロパティが増えていくため、 mapStateToProps と mapDispatchToProps が肥大化してしまい見通しが悪くなります。 分割されている例 // GoodContainer.jsx import { connect } from 'react-redux' import * as React from 'react' import ContainerA from './ContainerA' import ContainerB from './ContainerB' import * as actions from '../actions' class GoodContainer extends React.Component { componentDidMount() { this .props.fetch() } render() { return ( <div> <ContainerA /> <ContainerB /> </div> ) } } const mapDispatchToProps = dispatch => { return { fetch() { dispatch(actions.fetchData()) } } } export default connect(()=> { return {} } , mapDispatchToProps)(GoodContainer) // ContainerA.jsx import { connect } from 'react-redux' import * as React from 'react' import ComponentA from '../components/ComponentA' import * as actions from '../actions' const mapStateToProps = state => { return { name: state.a.name, } } const mapDispatchToProps = dispatch => { return { handler1() { dispatch(actions.actionA1()) } , handler2() { dispatch(actions.actionA2()) } , } } export default connect(mapStateToProps, mapDispatchToProps)(ComponentA) // ContainerB.jsx import { connect } from 'react-redux' import * as React from 'react' import ComponentB from '../components/ComponentB' import * as actions from '../actions' const mapStateToProps = state => { return { name: state.b.name, } } const mapDispatchToProps = dispatch => { return { handler1() { dispatch(actions.actionB1()) } , handler2() { dispatch(actions.actionB2()) } , } } export default connect(mapStateToProps, mapDispatchToProps)(ComponentB) 分割されている例では、もともと componentDidMount でデータフェッチしていた箇所だけを元のContainerに残し、 ComponentA と ComponentB に関するプロパティとハンドラーをそれぞれ ContainerA と ContainerB に分割しています。 こうすることでコードの肥大化を防ぎ見通しがかなり良くなったと思います。 ただし、全ての コンポーネント を connected にするとやりすぎなので適切な単位で分割する必要がありますのでご注意ください。 例えば ECサイト の場合、 商品名 、 商品コメント 、 配送方法 、のようなおおざっぱな単位で分割していき、それでもコードの見通しが悪いと感じたら更に分割するというような感じで実装しています。 またContainer Componentsのテストの仕方については以前書いた記事がありますので読んで頂けると幸いです。 https://qiita.com/hiyamamoto/items/281709cc2a98268fb6c2 Presentational Componentsの分割 前項に続き コンポーネント の分割についてです。 Presentational Components についての詳細は Reduxの公式Doc をご確認ください。 Presentational Components を分割する理由としては前項と同様に見通しを良くするため、テストのしやすさのためです。 また、Reactのメリットにも書いてある再利用性を高めるという利点もあります。 まずは分割されていない例を見ていきましょう。 適切に分割されていない例 function Page(props) { return ( <div> <div> <h1> { props.headerTitle } </h1> </div>> <div> <button>進む</button> <button>戻る</button> { props.value } </div> <div> <a href= "/path/1" >Link1</a> <a href= "/path/2" >Link2</a> </div> </div> ) } こちらの例では複数の div 、 button 、 a 要素が出てきています。 そんなに大きな コンポーネント ではありませんが既にちょっと見通しが悪くなっていると思います。 また、この コンポーネント のテストは以下になります。 ※ コンポーネント のテストは enzyme というライブラリの使用を前提としています describe( '<Page />' , () => { const wrapper = shallow(<Page />) it( 'render 進むButton' , () => { // button が複数出てくるのでうまく特定出来ない expect(wrapper.find( 'button' ).fisrt().contains( '進む' )).to.be. true } ) } ) このように複数の同一要素がある場合は wrapper.find('button').first() のように表示順を意識して コンポーネント の取得をする必要が出てきてしまい、ただ表示順が変わっただけでテストが壊れてしまいます。 では分割されている例を見てみましょう。 適切に分割されている例 function Page(props) { return ( <Container> <Header title= { props.headerTitle } /> <Body> <ForwardButton onClick= { props.onClickForward } /> <BackwardButton onClick= { props.onClickBackward } /> <BodyContent value= { props.value } /> </Body> <Footer> <Link1 /> <Link2 /> </Footer> </Container> ) } 分割されている例 では 分割されていない例 で見られた div などが一切出てきていないため見通しが良くなっているのがわかると思います。 また、それぞれの コンポーネント の意味が コンポーネント 名からひと目で分かるようになっています。 この コンポーネント のテストはこちらです。 describe( '<Page />' , () => { const wrapper = shallow(<Page />) it( 'render 進むButton' , () => { // ForwardButton というコンポーネントがあるかテストするだけで良い expect(wrapper.find(ForwardButton)).to.have.length(1) } ) } ) 進むボタン が コンポーネント 化されたため、 戻るボタン との表示順を気にする必要がなくなり、デザインの都合で表示順が変わったとしてもテストが壊れることがなくなりました。 ComponentをStatelessに保つ Reactの利点として状態をDOMから分離できるということを前述しましたが、 コンポーネント からも状態を分離することで本来の コンポーネント が持つViewとしての役割だけに専念させることができます。 Reactの基本として state というもので状態を管理することができますが、その state そのものを持たない コンポーネント を Stateless Functional Component と呼びます。 React/Reduxアプリケーションでは state は基本的に redux store で管理し、 コンポーネント では state を使わないようにすることで状態管理を一元化することができます。 ただし、ボタンクリック時にモーダルを表示したり、フォーカスしたときに値を加工するだけなど、その コンポーネント 内で完結するような state を持つことはさほど問題にはならず、全ての状態を redux store で管理しなければいけないというわけではないと個人的には考えています。 ある コンポーネント の状態を別のツリーの コンポーネント で参照したい場合や、 ドメイン ロジックに関わる状態は redux store で管理し、 コンポーネント 内部で完結する状態はその コンポーネント の state として管理するように柔軟に設計するのがベターです。 Stateless Functional Component の書き方 state や lifecycleメソッド を持たない場合は class シンタックス ではなく、関数を使います。 コンポーネント 名として関数名が使われるため、 arrow function より通常の関数として書くとよいです。 function FooComponent(props) { return ( <div> 名前: { props.name } 年齢: { props.age } </div> ) } 利点 ただの関数なのでテストしやすい stateを持ってないことが一発でわかる Reducerの分割 Reduxでは combineReducers という関数を使って通常Reducerを分割すると思います。 Reduxの公式Doc ではReducerを分割する際、 コンポーネント の レンダリング ツリーで分割するのではなく ドメイン データごとに分割することを推奨しています。 また、 DomainState 、 AppState 、 UIState という3つのStateに分割することが提案されています。 DomainState 例えば、商品や取引などの ドメイン 特有のstateのことです。 前述した レンダリング ツリーごとの分割と ドメイン データごとの分割の比較をしてみます。 レンダリング ツリーごとの分割 レンダリング ツリーごとの分割は簡単に言うと画面ごとの分割ということです。 下記は 新規画面 、 編集画面 、 一覧画面 といった画面ごとに分割している例です。 reducers ├── newReducer.js ├── editReducer.js └── listReducer.js ドメイン データごとの分割 変わって ドメイン データごとの分割では、 商品 、 配送方法 、 ブランド などの ドメイン データで分割しています。 reducers ├── productReducer.js ├── shipphingReducer.js └── brandReducer.js AppState アプリケーション全体の state は ドメイン データ用のReducerとは別のReducerとして用意すると、見通しが良くなります。 例えば、データをローディング中かどうかを管理する isLoading などの state はこちらに含めます。 UIState モーダルの表示状態などのUI特有の state も別のReducerで管理します。 ただし、前述のように コンポーネント の state として持つことも多いです。 stateは POJO (Plain Old JavaScript Object) state は基本的に immutable(不変) object として扱います。 下記のようなコードはNGです。 const defaultState = { foo: 'foo' , bar: 'bar' } const reducer(state = defaultState, action) => { if (action.type === 'Foo Action' ) { state.foo = action.payload // fooだけ変更したい return state } } immutable.js を使えばかんたんに不変オブジェクトを生成できます。 import { Map } from 'immutable' const defaultState = Map( { foo: 'foo' , bar: 'bar' } ) const reducer(state = defaultState, action) => { if (action.type === 'Foo Action' ) { return state.set( 'foo' , action.payload) // fooだけ変更したい } } 上記の Map.prototype.set は常に新しい Map の インスタンス を返します。 ただし、 Map は普通のオブジェクトのようにプロパティにアクセスできないので、 コンポーネント 内でアクセスする際には state.get('foo') のようにする必要があります。 また、 API などから取得してきた JSON を毎回 Map オブジェクトに変換する必要があるため結構面倒です。 それ、ES2015+でできるよ const defaultState = { foo: 'foo' , bar: 'bar' } const reducer(state = defaultState, action) => { if (action.type === 'Foo Action' ) { return { ...state, foo: action.payload // fooだけ変更できる! } } } state は色々な場所( コンポーネント 、 API クライアントなど)でアクセスするので、「immutableオブジェクトになってるか?」といちいち判定するのは面倒です。 常に POJO にしておくことでその手間を減らすことができます。 Reducerから ドメイン ロジックを分離したい場合 ドメイン データ用のReducerに ドメイン ロジックを書いてしまうとコードがどんどん肥大化していってしまうため、下記のように ドメイン データを models ディレクト リ配下に書きたくなると思います。 import MyClass from '../models/MyClass' const defaultState = new MyClass( { foo: 'foo' , bar: 'bar' } ) const reducer(state = defaultState, action) => { if (action.type === 'Foo Action' ) { // ドメインロジックはドメインクラスに委譲 return state.changeFoo(action.payload) } } このようにしたい場合は models 配下からクラスではなく関数をエクスポートすることで代替できます。 import { changeFoo } from '../models/MyDomainModel' const defaultState = new MyClass( { foo: 'foo' , bar: 'bar' } ) const reducer(state = defaultState, action) => { if (action.type === 'Foo Action' ) { // ドメインロジックはドメインモデルの関数に委譲 return changeFoo(state, action.payload) } } export const changeFoo = (model, value) => { // 複雑な処理 return { ...model, foo } } 他にも immutable.jsのRecordを使う方法 などがありますが、前述の通り変換の手間などを考えると個人的にはあまりおすすめしません。 FSA(Flux Standard Action)を使う Flux Standard Action とは https://github.com/redux-utilities/flux-standard-action actionの型が実装者によってまちまちになると読みづらいし不便だから標準化しましょうね、という話です。 割と界隈では デファクト になってる気がします。 非同期処理の成功、失敗ごとに LOAD_SUCCESS 、 LOAD_FAILURE のようにactionを分けるのではなく LOAD_FINISH のように一つにまとめることが出来ます。 { type: 'LOAD_FINISH' , payload: { id: 1, text: 'Do something.' } , meta: { ... // metadata } } { type: 'LOAD_FINISH' , payload: new Error(), error: true } アクションの型が統一されて書く方も読む方も負荷が減るのでおすすめです。 非同期処理 reduxの登場人物(component, reducer, actionCreator)は純粋な関数の集まりなので副作用のある処理を書く場所がありません。 一般的に副作用のある処理は Middleware層 を利用します。 非同期処理をする為のMiddlewareとしては redux-thunk と redux-saga が有名です。 redux-thunk vs redux-saga redux-thunk Pros API がかんたんで学習コストがほぼ0 サッと導入してサッと書ける Cons actionCreatorにロジックが入り込む actionCreatorから純粋さがなくなる そのため actionCreatorのテストが複雑になる redux-saga Pros 副作用のある処理が完全に分離できる API が豊富にあり並列化したりスロットルしたり色々出来る [redux-saga]フォームに高速で入力するとガックガクになる問題 - Qiita 高テスタビリティ Cons 学習コストが高い Generator関数はとっつきにくい API が多い 小さめのアプリケーションでは redux-thunk 、大きくて複雑なアプリケーションでは redux-saga 、 というように使い分けるといいでしょう。 まとめ どの フレームワーク にも言えることですが、見通しがよくメンテしやすいアプリケーションを書くためにはコードを適切な単位で分割することが非常に重要です。 React/Reduxアプリケーションでは コンポーネント を分割し再利用性を高めたり、状態を適切に分割することで、それぞれのメリットを最大限に活かせると思います。 Reduxの公式Doc では今回書いたReduxアプリケーションを設計する上での考え方が詳細に書かれていて非常に参考になるのでぜひご一読ください。 株式会社 エニグモ 正社員の求人一覧 hrmos.co
アバター
はじめに こんにちは。2018年9月入社でデータ分析担当の@Tawasshyです。 この記事は Enigmo Advent Calendar 2018の3日目 です。 弊社ではファッション ECサイト である BUYMA を展開しております。 売り手となるバイヤー(パーソナルショッパー)は世界中に在住しており、多種多様な商品を買い付けして膨大なSKUが存在します。 一方で、買い手側の購買行動も多岐に渡るのも必然となります。 そのような複雑な関係性を分析する状況においても、今回紹介するバスケット分析のような、基本的とも言える分析手法は解釈もしやすく、効果的な手段の一つになり得ると思っております。 バスケット分析について バスケット分析は何と何の商品が一緒に買われているかに着目する分析方法であり、マーケット・バスケット分析、アソシエーション分析とも呼ばれています。 一般的な話 有名な事例としては、「おむつとビール」が同時に買われやすい、というルールの発見があります。 また、Agrawal氏らが1994年に発表した論文「Fast algorithms for mining association rules」( IBM Almaden Research Center)が、この分析手法が普及したきっかけとなったことが有力な説のようです。 なぜ SQL でやるのか バスケット分析は R言語 のarulesというライブラリを使うのが、もっとも手軽な方法であると思いますが、今回は SQL での実行を紹介します。 弊社ではエンジニアだけではなく、ディレクターやマーケター、カスタマサポートといったビジネスサイドのメンバーも SQL を日常的に叩いており、 SQL での分析の幅を拡げることに貢献できたら良いなと思ったことが SQL でやる理由です バスケット分析詳細 まず、二つの商品X、Yがあったとします。 商品Xが買われた場合、商品Yという商品を買う確率を扱います。その指標として以下の指標を扱います。 今回算出する指標 信頼度(confidence) 商品Xが買われた場合に、商品Yを買う確率。 リフト値 商品Yが買う確率が、商品Xを買った場合に商品Yを買う確率がどれだけ変化したかの倍率。 (信頼度➗商品Yが買われる確率) Xを買ってYを買う確率がどれだけ持ち上がったかと解釈できる。 今回算出しない値 支持値(support) XとYが同時に買われている確率を示します。バスケット分析においてはこれも重要な指標ですが、今回は説明を割愛します。 環境構築について 手元のローカルマシンで簡単に再現できるので SQLite でやってみます。 macOS の場合は以下のように brew で一発です。なお、 Windows でも手軽にインストールできますが、ここでは割愛します。 $ brew install sqlite3 データの準備について SQLite を起動します。カレント ディレクト リにbasket.sqlite3というの名のDBが生成され、DBに接続した状態となります。 $ sqlite3 basket.sqlite3 無事に起動できたらこのようにterminalにこのように表示されます。 sqlite > まず、にテーブルの定義を行います。 create table sales_history( item_category char ( 12 ), user_id char ( 8 ) ); 購買履歴を作成します。 insert into sales_history values ( ' トップス ' , ' UID100001 ' ), ( ' 財布 ' , ' UID100001 ' ), ( ' 財布 ' , ' UID100002 ' ), ( ' トップス ' , ' UID100002 ' ), ( ' 靴 ' , ' UID100002 ' ), ( ' トップス ' , ' UID100002 ' ), ( ' アウター ' , ' UID100002 ' ), ( ' アウター ' , ' UID100102 ' ), ( ' ワンピース ' , ' UID100103 ' ), ( ' 靴 ' , ' UID100104 ' ), ( ' 財布 ' , ' UID100104 ' ), ( ' 靴 ' , ' UID100105 ' ), ( ' 靴 ' , ' UID100106 ' ), ( ' トップス ' , ' UID100107 ' ), ( ' トップス ' , ' UID100108 ' ), ( ' 靴 ' , ' UID100109 ' ), ( ' 財布 ' , ' UID100110 ' ), ( ' 靴 ' , ' UID100111 ' ), ( ' トップス ' , ' UID100111 ' ), ( ' 靴 ' , ' UID100112 ' ), ( ' 靴 ' , ' UID100112 ' ), ( ' トップス ' , ' UID100113 ' ), ( ' 財布 ' , ' UID100113 ' ), ( ' ワンピース ' , ' UID100114 ' ), ( ' ワンピース ' , ' UID100114 ' ) ; これでデータの準備はできました。 バスケット分析の実行 信頼度の算出 select combi_count.item_category, combi_count.item_category2, cast (combi_count.order_count as real)/item_count.order_count as confidence from ( select item_category, count ( distinct user_id) as order_count from sales_history group by item_category )item_count inner join ( select X.item_category, Y.item_category as item_category2, count ( distinct X.user_id) as order_count from sales_history as X inner join sales_history Y on X.user_id = Y.user_id and X.item_category Y.item_category group by X.item_category,Y.item_category )combi_count on combi_count.item_category = item_count.item_category ; 以下のように出力されます。 商品X、商品Y、信頼度という順番です。 商品X 商品Y 信頼度 アウター トップス 0.5 アウター 財布 0.5 アウター 靴 0.5 トップス アウター 0.166666666666667 トップス 財布 0.5 トップス 靴 0.333333333333333 財布 アウター 0.2 財布 トップス 0.6 財布 靴 0.4 靴 アウター 0.142857142857143 靴 トップス 0.285714285714286 靴 財布 0.285714285714286 財布を買ったユーザーがトップスを買う信頼度がもっとも高いことが分かります。 リフト値も追加して算出 上の sql が書ければ、全体でYが買われている確率を算出し、joinして信頼度を割ればリフト値が算出できます。 select combi_count.item_category, combi_count.item_category2, cast (combi_count.order_count as real)/item_count.order_count as confidence, cast (combi_count.order_count as real)/item_count.order_count / AllBuyY.order_count as lift from ( select item_category, count ( distinct user_id) as order_count from sales_history group by item_category )item_count inner join ( select X.item_category, Y.item_category as item_category2, count ( distinct X.user_id) as order_count from sales_history as X inner join sales_history Y on X.user_id = Y.user_id and X.item_category Y.item_category group by X.item_category,Y.item_category )combi_count on combi_count.item_category = item_count.item_category -- 全体でYが買われている確率を算出してjoinする inner join ( select item_category, cast ( count ( distinct user_id) as real) / ( select count ( distinct user_id) from sales_history) as order_count from sales_history group by item_category ) AllBuyY on combi_count.item_category2 = AllBuyY.item_category ; 以下のように、出力されます。 商品X、商品Y、信頼度、リフト値を出しております。 商品X 商品Y 信頼度 リフト値 アウター トップス 0.5 1.25 アウター 財布 0.5 1.5 アウター 靴 0.5 1.07142857142857 トップス アウター 0.166666666666667 1.25 トップス 財布 0.5 1.5 トップス 靴 0.333333333333333 0.714285714285714 財布 アウター 0.2 1.5 財布 トップス 0.6 1.5 財布 靴 0.4 0.857142857142857 靴 アウター 0.142857142857143 1.07142857142857 靴 トップス 0.285714285714286 0.714285714285714 靴 財布 0.285714285714286 0.857142857142857 信頼度、リフト値について ある商品を買う確率としての指標として、どちらが大事というのはケースバイケースとなりますが、 両方が高い値でないとユーザーの傾向を示す効果的な分析にならないことが言えます。 リフト値が高く、信頼度が低い場合 全体で商品X, あるいは商品Y自体を買われている確率が全体で低い場合に起きます。 商品Xが買われることが少ない為、リフト値が高い値を示してもYを買う確率は小さいままになります。 また、商品Y自体を買われている確率が全体で低い場合は、リフト値が掛けかれる元の確率が低いことになる為、Yを買われる確率は低いままということになります。 リフト値が低く、信頼度が高い場合 何もしなくてもYを買われる確率が高く、Xを買っても買わなくても、Yを買う確率の増減が小さい状態を示します。 そもそも購買履歴が少ない場合… 偶然、信頼度、リフトが高く出るということが起こり得るので、Xを買ったユーザーYの購買促進を行うキャンペーンなどの施策を行っても有効に働かない場合があります。 実際にDBに接続して行う場合は 同じテーブルへの問い合わせが散在しているので、 PostgreSQL , MySQL , SQLServer 等ではwith句は必須であると思います。 SQL でやるのがめんどくさいなと思ったら.. データを抽出する必要がありますが、 R言語 でarulesというライブラリを使えばもっと手軽にできますし、無料です。 さいごに 弊社ではデータ分析に取り組む一方、データ分析基盤を整え、データ活用の効率化とアクセスビリティを推進しております。 エンジニアはもちろん、データを活用しながら「世界を買える」ビジネスに挑戦してみたいメンバーのジョインをお待ちしております! やんちゃであれ! ENIGMO7より 参考 https://www.albert2005.co.jp/knowledge/marketing/customer_product_analysis/abc_association https://codezine.jp/article/detail/10284
アバター
こんにちは、Enigmo 新卒エンジニアの @sean0628_i です。 Enigmo Advent Calendar 2018 2日目の記事です。 初めに Accelerated Mobile Pages の略であり、高速でスムーズにWeb ページを表示するためのライブラリー、或いはその仕組みのことです。 通常のHTML に比べて、制限が多く存在します。 今回は AMP 公式のドキュメントの仕様に関する箇所 が英語だったので、読解し要点をまとめます。 AMP 制限 特定の要素を マークアップ に含める。 JavaScript は利用不可。 inline CSS のみ利用可。 -> 50KB の制限付き 特定のHTML tag は利用不可。 HTML 内、コメント不可。 特定の CSS properties は利用不可。 必要な マークアップ <!doctype html> でHTML を書き始める。 <html ⚡> tag を含める。(<html amp> でも代用可能) <head> tag と<body> tag を含める。(通常のHTML では任意) <head> tag 内の一番初めに、<meta charset=" utf-8 "> を含める。 <head> tag 内に、<link rel=" canonical " href="$SOME_URL"> を含める。( $SOME_URL は通常版のHTML のURL、通常版のHTML が存在しない場合はAMP のURLを入れる。) <head> tag 内に、<meta name="viewport" content="width=device-width"> を含める。 minimum-scale=1 および、 initial-scale=1 も含めることが勧められる。 <head> tag 内に、<script async src=" https://cdn.ampproject.org/v0.js "></script> を含める。 <head> tag 内に、AMP Boilerplate Code(head > style[amp-boilerplate] と noscript > style[amp-boilerplate])を含める。 HTML tags tag 適正 script ×(type が application/ld+json の場合のみ○) noscript ○ base × img amp-img にて代用 video amp-video にて代用 audio amp-audio にて代用 iframe amp-iframe にて代用 frame × frameset × object × param × applet × embed × form ○( amp-form extension を含めることで利用可) input elements 一部(<input[type=image]>、<input[type=button]>、<input[type=password]>、<input[type=file]>)を除いて○<fieldset>、<label> は○ button ○ style <head> tag 内に、一つのみ追加可。 amp-custom attribute を持たせる必要あり。 link microformats.org に、登録されている rel は○ meta http-equiv attribute が特定の値に利用可。詳細: AMP validator specification a ○ href attribute を javascript: から始めてはいけない。 target を設定する場合、 _blank を利用。 svg 殆どの SVG は利用可 Comments HTML 内でコメントは利用できない。 CSS 50,000 bytes の上限を超えてはいけない。 !important は利用できない。 transition および、animation 関連は opacity 、 transform properties のみ利用可。
アバター
こんにちは、18年新卒エンジニアの @sean0628_i です。 早いもので今年ももう12月、Enigmoにジョインしたのが今年の春なので、気がついたら入社してから半年ほどが経っていますね。。。 さて、12月といえば Advent Calendar の季節ですねー笑 今年はEnigmoも初めてAdvent Calendar を公開することとなりました。 というのも、新卒の私が「面白そうだなぁー。」、とボソッと呟いたところ、部長殿から「やってみたらー。」、とOKをいただきましたので、 他のメンバーにもご協力をいただき、 Enigmo Advent Calendar 2018  を公開することになりました。 記念すべきEnigmo Advent Calendar 2018の1日目は、文系学部から新卒でエンジニアになった私が今までに読んで、実際に業務で役に立った技術書5選をお届けします。 そもそもAdvent Calendarとは? さっきから、「”Advent Calendar”とか、横文字多用しやがって。」、という方、大変失礼しました、、、ご説明します。 Advent Calendar とは、元々クリスマスまでの日数を数えるためのカレンダーで、日ごとに小さなポケットを設け、そこにお菓子を入れて楽しんだりするものだそうです。 数年前からWeb 業界の企業ではではそれに倣って、クリスマスまでの25日間記事を公開するようになりました。 つまり、我々が意味するAdvent Calendar とは、25日間記事を公開し続けるという言わば苦行のことです。 ただ、エンジニアとして新たな知識の習得、既知の知識の共有は避けられません。 ということで、今回は苦行を断行します。 * なお、苦行ゆえ時々お休みすることもあるかと思いますが、その点はご容赦いただければと思います。 オススメの技術書5選 1. プロを目指す人のためのRuby入門 言語仕様からテスト駆動開発・デバッグ技法まで (Software Design plusシリーズ) こちらの本は、知人の勧めで入社する前に読みました。当時は本当に Ruby 初心者、というか プログラミング言語 初心者でした。 そんな私でも途中で挫折せず、 Ruby を学べたのはこの本のおかげかなぁと。 この本では実際に手を動かして学習することが出来ます。 ひたすら読むだけだと飽きてしまいますし、言語習得のためには実際に手を動かすのが一番かとも思います。 そういった意味でこの本はオススメです。 2. Ruby on Rails 5アプリケーションプログラミング 上記の本で Ruby を一通り学んだ後、こちらで Rails を学びました。 もし、アプリ作成の一連の流れを知りたいということであれば、 Rails tutorial がオススメです。 一方で、 この本では、 Rails 組み込みのメソッドや、 Rails の仕組みが事細かに紹介されています。 Rails を体系的に学びたいという方や、 Rails tutorial やったけどもう少し深く Rails を知りたいという方にはこの本がオススメです 。 3. Everyday Rails - RSpecによるRailsテスト入門 この本は入社直後、研修の一環で読みました。 当時は、午前中に本を読み、午後に実務でアウトプットするという形式でした。右も左もわからず、テストを書くのが本当に嫌いでした笑 しかし、この本で基礎を学び、実務でのテストコードの実装を重ねた結果、今ではテストを書くことが苦ではなくなりました。 さらには、テストコードがない ソースコード を見ると不安を覚える身体になってしまいました笑 とてもわかりやすいのでオススメです。 4. スッキリわかる SQL 入門 ドリル215問付き! (スッキリシリーズ) 実務では主に Ruby on Rails を使用しているのですが、事あるごとに SQL の記述に迫られます。 ちなみに、Enigmoのメンバーは、エンジニアに限らず殆どの人が SQL を扱う事ができます。 もともと マーケティング 業界で インターンシップ をしていた私には、これがとても衝撃でした。 ともかくも、この本が良かった点はブラウザ上に環境が整備されているので事前の準備が不要で、すぐに SQL を学習できたということです。 実践問題も付いているので、この本一冊で SQL の基本をしっかりと身に付けることができると思います。 5. リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice) プログラミングを進める上で必要な 命名規則 、コード記述の作法に関して学びました。 正直に話すと、プログラミングを初めて1ヶ月くらいの時にこの本を読みました。 その時は、大した気づきもなく「レビュー高い割に、ふーん。」、くらいな感想しかありませんでした。 しかし、つい最近読み返してみると、当たり前だけど現状できていないことが多く記述されていることに気がつきました。 Enigmoのエンジニアの卓上でもちらほら見かけますし、一読の価値はあると思います。 所感 駆け足になりましたが、新卒の私がオススメする実務で役立った技術書5冊上げてみました。 これまでに読んだ本で他にも紹介したい良書はたくさんあります。 オブジェクト指向 の話とか、DB設計の話とかたくさん。 しかし、今回は日々 Ruby を扱うエンジニアとして、言語系で実務に役立った本に的を絞り紹介してみました。 また機会があれば、他の良書も紹介したいと思います。この辺で、Enigmo Advent Calendar 2018 1日目終わりたいと思います。 最後まで読んでいただきありがとうございます。
アバター
こんにちは。エンジニアの Steven です。 広島で9月18日から 20日 まで行われた RubyKaigi 2017 に山本さんとエンジニア二人で行ってきましたので、どうだったのかについてレポートさせていただきます。 RubyKaigi は毎年 Ruby 言語を中心に行われる会議です。 最近は日本を巡って毎回違う都市で行われてて、今回は広島で行われました。 日本語のセッションもあれば、英語のセッションもあって、外国人の参加者が少なくはないイベントです。 Ruby のカンファレンスの中でレベルが一番高いと言われています( Ruby の作者曰く)。 会議は月曜日から水曜日まで行われてて、事前の週末に広島まで行けば、東京に住んでいる私達にとっては、広島で少し観光をする機会でもあって、そうできるように準備していましたが、残念なことにこの週末はちょうど台風がその近くまで通ってきて、観光するどころか、新幹線ではそもそも行けるのかと、逆に心配することになりました。 ですが、会議の3日間自体は天気は極めてよくて、日焼け止めが必要になるのではないかという逆転にもなりました。 1日目 朝起きて、 元安川 (今調べました)を渡って、会議が開催されてる 広島国際会議場 まで行ってから、入場券を受けて、会議スタート! (目をつぶっていてすみません。後から気づきました。) 初日で見てきた トーク は 基調講演、Nobuyoshi Nakada Fiber in the 10th year、 Koichi Sasada How Close is Ruby 3x3 For Production Web Apps?、Noah Gibbs Gemification for Ruby 2.5/3.0、Shibata Hiroshi How to optimize Ruby internal、Shizuo Fujita I quit my job to write my own language: Goby、Stan Lo Ruby Committers vs the World 今見返してみると、毎日の仕事ですぐに活かせそうな トーク は初日はそんなになかったかと思いますが、 Ruby のコミュニ ティー はどう構成されているのか、どんな課題を抱えているのか、何をやっているのかはより具体的に理解できて、他の Rubyist の毎日に触れて勉強になりました。 この日の最後のセッションとして、 Ruby のコミッターが壇上に上がって、全員で議論をしながら、聴衆の質問に答えてたというイベントがありましたが、この日の中で一番記憶に残ってるところです。 質問に答えるためにコミッターの皆さんが議論にのめり込んで、質問一つだけで1時間が立ってしまいそうな状況に数回なってしまいましたが、その議論を聞くのは単に面白くて、話題はなんであろうと、専門的な技術力を持ってる人が周りにいれば、それだけで面白くなると感しました。 初日のセッションが終わってから、晩御飯が目的で街に出ててたのですが、なぜか赤色のシャツを着ている人が多くて、最初はよくわかりませんでしたが、なるほど、広島の カープ が日本一になった直後でした。 広島市 は全体的に盛り上がっていて、東京に戻ってきたのではないかと、ところどころ人混みができてました。 その夜は広 島風 お好み焼 きを食べる機会になったのですが、やはり美味しくて、2日目も3日目ももう一度食べようという衝動に耐える必要が出ました。 2日目 翌日は朝に スターバックス によってから、会議場に戻って、2日目スタート! 2日目で見てきた トーク は 基調講演、 Yukihiro Matsumoto The Ruby Module Builder Pattern、Chris Salzberg Improve extension API : C++ as better language for extension、Kouhei Sutou Automated Type Contracts Generation for Ruby 、Valentin Fondaratov Type Checking Ruby Programs with Annotations、Soutaro Matsumoto Ruby Language Server、Fumiaki Matsushima Write once, run on every boards: portable mruby、Yurie Yamane Lightning Talks 基調講演は今回は Ruby の作者によって行われてて、やはりまつもとさんは本当に実在する人で、自分が普通の人間でも実際に会える方だとわかりました。 基調講演でも少し紹介されましたが、この日の注目は Ruby での型の導入の トーク だったかと思います。 最近新しくできた言語の中では静的型付けの言語が多くあって、どんどん人気になってますが、動的型付けである Ruby でも型のいいところを適切な形で活かせるのかという考えがあって、今回はそれについての トーク は2つもありました。 Ruby には型が実際に入ってしまうのかどうかはまだわからないのですが、型のチェックをしてくれるツールを Ruby に入れることでどうなれそうなのか、 Ruby の未来を少し見れました。 個人的には Lightning Talks はその日の一番面白いセッションでした。 1時間で5分だけの トーク が12あって、何かをゆっくり学ぶという目的より、いろんな情報を短時間で伝えて、面白話を交えながら、聴衆を楽しませるのがそのライトニング トーク の一つの印象でした。 ビデオは公開されましたので、技術の話で少し笑いたければ、その トーク をおすすめします。 その日は昼休みに 原爆ドーム を見に行く時間は少しあって、日本の 世界遺産 の一つを写真にとどめることができました。 一言でまとめさせていただきますと、感動しました。 その夜の晩御飯は海鮮料理で、少し高めな食事でしたが、広島のもうひとつの名物を食べれてよかったです。 3日目 いよいよの最終日! ホテルからチェックアウトして、会議場に戻って、最終日スタート! この日見てきた トーク は Compacting GC in MRI 、Aaron Patterson Ruby for Distributed Storage System、Satoshi Tagomori Pattern Matching in Ruby 、 Yuki Torii Memory Fragmentation and Bloat in Ruby 、Nate Berkopec Busting Performance Bottlenecks: Improving Boot Time by 60%、Julian Nadeau How to write synchronization mechanisms for Fiber、Masatoshi Seki Towards Ruby 3x3 performance、Vladimir Makarov この日は基調講演は特にありませんでしたが、注目はパフォーマンスに関する トーク でした。 Ruby の GC をどう改善できるか、メ モリー 関連の問題をどう調査・解決できるか、 Ruby をどう3倍早くに改善できるか、違う種類のパフォーマンスの問題についていろんな トーク があって、基礎知識としても実践力としても役に立ちました。 この前、 Ruby の作者が設定した Ruby 3x3 という目的は、設定されただけで実際にそれを簡単に実現できるのかどうかは個人的には少し疑問に思ってたのですが、最後の トーク でそれを実現できそうなプロジェクトを紹介してくださった方が現れて、そんなに遠い未来じゃないと思うようになりました。 面白い演説で聴衆の笑いを集めた閉会の辞の後、お土産を買って、無事に東京に帰りました。 今回会社で広島までこれたのは私達二人だけでしたが、得てきた知識を東京に残った他のエンジニアに共有する予定です。 終わりに イベントが多い出張でしたが、RubyKaigi に参加できてよかったです。 新しい基礎知識と Ruby コミュニ ティー に関する知識を積めて、勉強になったカンファレンスでした。 Ruby 言語のカンファレンスに参加するのはこれで初めてでしたが、 Ruby に限らずこれからも他に多くある プログラミング言語 を中心にしたカンファレンスに参加したいと思います。 今まで実際に参加してきたのは、大学で開催されたカンファレンスだけであって、それ以外の トーク はすべてビデオで見てきましたが、前の私と同じく、まだカンファレンスに出席したことがない方に出席をおすすめします。一度やってみたら、中々やめられなくなってしまうかもしれません。
アバター
こんにちは。エンジニアの山本です。 5/23に開催された、 【 ヒカ☆ラボ 】大規模サービスがリスクをとってまでモダンな開発環境にリプレイスした理由~ここだけの苦労話や手法を交えお話します~  というイベントに参加 & 登壇してまいりましたのでレポートします。 今回のイベントのテーマは レガシー改善 という、長年継続しているサービスでは避けては通れないものでした。 弊社のサービスである BUYMA もローンチから10年以上経ち、溜まりに溜まった技術的負債を日々返済しています。 Reactを導入した話 弊社からは2人登壇させて頂きまして、私、山本は "React導入時の苦労話とこれからについて" と題しまして、ある機能のリプレイスプロジェクトでReactを導入した話をしました。 既に デファクト・スタンダード になったと言えるReactですが、導入当時の一年前は大規模なサービスに導入したという事例もまだ少なく、如何にして非SPAな Webサービス にReact/Reduxをマッチさせるかという点でとても苦労しました。 当時の苦労話を共有することで、少しでもこれから大規模サービスにReactや他の JavaScript フレームワーク を導入しようとしている方々のお役に立てればと思います。 こちらが発表資料です。 商品検索を改善した話 木村の方は、 " BUYMA の商品検索システムの改善の取り組み" というテーマでした。バイマの商品検索システムをSolrCloudへリプレースし、耐障害性を向上させた取り組みについてや、レガシーな検索ロジックを、BigQueryなどを駆使しながら数値で検索精度を計測しつつ、新しいロジックへと改善していく取り組みについてお話しました。 発表資料はこちらです。 BUYMAの商品検索システムの改善の取り組み from 慎太郎 木村 当日の様子 木村 山本 質疑応答時 まとめ 一緒に登壇させて頂いたJapanTaxiさん、 一休さん のお話も非常に共感できるところが多く、また今回は最近開催されたヒカ☆ラボの中でも来場者数は多かったようで、"みんなレガシー改善に苦労しているんだなぁ"としみじみ感じました。 新しい技術がどんどん生まれる中、如何にして技術的負債を返済してくかはこれからますます重要になってくると思います。 みなさんもガンガン知見を共有して共にレガシーと戦っていきましょう。
アバター