nykergoto’s blog

機械学習とpythonをメインに

データコンペサイトを作る: フロントエンド編

この記事はオンサイトデータコンペ atmaCup のシステムぐるぐる https://www.guruguru.ml/ のフロントエンドの話です。

atmaCupとは?

オンサイトデータコンペatmaCupとは実際に会場に集まり、準備されたデータをテーマに沿って分析・予測を行い、その精度を競うイベントです。 データコンペで有名なのはKaggleですが、みんなで実際に集まり、かつ時間もその日のうち8時間など短いのが特徴で、 参加者のスキルがオンラインのデータコンペより強く結果に表れます。

直近 atmaCup#4 はリテールAI研究会様 との共同開催でした。期間が 3/1 ~ 8 の1周間、すべての分析はAzure上で行うというものでした。 今までのatmaCupでは1日でかつ環境は自由というものだったので、期間・環境ともにはじめての試みでした。 環境構築が大変だったかたや途中トラブルなどもありましたが、無事に終わることができ主催としてはホッとしているところです。

https://www.guruguru.ml/competitions/9/leaderboard より最終結果をみることができます。

このatmaCupですが「実際に集まる」と言ってもスコア計算用のファイル(以下では kaggle に合わせて submission file と呼びます)をUSBか何かでもらうわけには行かないので、submission file を upload してスコア計算し、チームごとにランキングが出るようなウェブアプリケーションぐるぐる https://www.guruguru.ml/ を別途作っています。この記事はぐるぐるのフロントエンド周りの話です。

バックエンドについては以前記事にしています のでそちらもどうぞ。

全体構成

まずは全体構成ですが、バックエンドのDRFに対して、SPA(SinglePageApplication)で実装されたフロントエンドがいて、適宜バックエンドに必要な情報をとっている、という感じです。

f:id:dette:20200311080735p:plain
ぐるぐるの全体構造(一部省略)

https://cloudcraft.co/view/45c7af86-888a-49cd-a7ff-6ca026e9e6e0?key=opGf3vj8B6oy9f0Z2zJwyQ

バックエンド、フロントともに ECS でデプロイされていて適宜スケーリングするような設定になっています。

フロントは Nuxt.js (Vue.jsをSPAとして適用しやすくするためのフレームワーク) で実装されていて、master merge のタイミングで ECS へのデプロイを gitlab CI で動かすような感じです。

フロントの構成は大きく

  • Vue.js
  • Nuxt.js
  • Vuetify.js

に依存していますので、まずはその話から。

Vue.js

f:id:dette:20200311080830p:plain

https://jp.vuejs.org/

ぐるぐるフロントではメインのjsフレームワークとしてVue.jsを採用しています。Vueはウェブページに表示されるテンプレート部とデータ部分の管理をするためのライブラリで、データに合わせて動的に表示を変えたりすることが簡単に出来るのが特徴です。

普通のjsで画面を操作しようと思うと、データが変わったあとに自分で画面を書き換える操作が必要です(jQueryなどはそう)が、Vueはその整合性をVue側で担保してくれます。そのため僕を含めた実装者はデータの操作部分の整合性に注力するだけで済む用になります。

Nuxt.js

f:id:dette:20200311080859p:plain

https://ja.nuxtjs.org/

Nuxt.js とは 「Vue.js のゆにばーさるあぷりけーしょん」を作れるフレームワークです。ゆにばーさるあぷりけーしょんってなんやねんということなんですが要するにVueで動くようなウェブページを便利に作れるぐらいな認識で良いと思います。これを使うことで自分でhtmlを書いてVueをレンダリングする処理をまったく書かずにVue Compnentだけ記述してウェブページを作成することができます。

Server Side Rendering (略してSSR. サーバー側で js を読み込んでページを作ってからクライアントに返すためクライアント側での展開が不要になる。) にも対応しているモダンな子です。

Nuxt のいいところを列挙すると以下のような感じです。

  • 基本的に使うライブラリが初期で入っている
  • 強い制約がある
  • 使い回しを考えてコードを書きやすい

基本的に使うライブラリが初期で入っている

これはたとえばSPAアプリケーション全体での変数を保持する仕組みを提供してくれる vuexvue-router 等を指します。 Nuxt.js を入れるだけでこれらのライブラリは一通り揃いますし、これらのライブラリを使って実装することを想定してライフサイクルが定義されているので、自分でごちゃごちゃと config をいじることがほとんどありません。
なのでNuxt.jsを入れればあとはアプリケーションの実装に集中できてとても便利です。

強い制約がある

もうひとつ大きないいところとして「強い制約がある」という点があります。たとえば /hoge にアクセスした時に表示するページを定義したい場合、 pages というディレクトリに hoge.vue というファイルを作ることを強制されています。これは最初始めるときに学習コストがかかるというデメリットはありますが、だれが書いても同じところに書かざるを得ないという素晴らしいメリットがあります。

この辺は Django / DRF にも通じるところがありますが、フレームワークの本質は強い制約を与えることで同じ書き方しかできなくすることだと思います。これによって他のプロジェクトに持っていく・持ち込むことが容易になりますし、他人のコードがとても読みやすく、コラボレーションのコストを下げてくれるのが良いところです。

使い回しを考えてコードを書きやすい

これもほぼ前項と一緒なのですが制約故に特定の機能を作りたい時、特定の場所に書くことが義務づけられているので、他のNuxtプロジェクトでの使い回しを考えつつコードを書けるのはメリットかなと思っています。

Vuetify

f:id:dette:20200311081108p:plain

https://vuetifyjs.com/en/

Vuetifyとは

Vuetifyは多数のコンポーネント(たとえばボタン要素やリストでの表示部分など)やグリッドレイアウトの仕組みが整ったVue.jsのためのフレームワークです。デザインにGoogleがかくあるべしを定義しているマテリアルデザインが採用されていてわりと最近人気です。そもそも僕がマテリアルデザインが好きというのも有るのですが、それ以外にもとてもいい点があります。

vuetify使っていていいなーと思うところをまとめると、以下のようになります。

実装済コンポーネントの種類が圧倒的に多い

一番最初に Vuetify を使い始めたのはこれが一番大きかったと思います。本当に種類が多くてボタンとかフォーム見たいな基本的な物は勿論のこと、画像の遅延ローディング(最初サムネイルを読み込んだあと綺麗な画像を読み込むやつ)とかTimelineの表示、ステップでの入力などなど多彩です。あまりに種類が多いので、ちょっと前までゴリゴリコンポーネントが増えていた時には、開発途中で知らない便利コンポーネントが生えていて「??」となることも多々ありました。最近は出揃った感もあって安定している気がします。

https://vuetifyjs.com/en/components/calendars/ このあたりから見て見てるとワクワク感が伝わるかなと思います。リンク先はカレンダーですが、パット見Googleカレンダーにしか見えない…。

種類が多いとやはり初速が出るので、ざっくりページを作る時非常に助かります。またデザインのカスタムもある程度考慮して定義されている(一部フォーム系は癖がありますが)ので、最初プロトタイプを作ってデザインを当て込んで精緻にしていく、という様なMVPに沿った開発ができるため重宝しています。

ありえんぐらいのコミット数

本当にありえないぐらい活発にコミットされています。僕のgithubアカウントの通知はほとんどVuetifyで埋まっています。

f:id:dette:20200311081206p:plain
ほぼすべてvuetifyからの通知で埋まったgmail

最近V2になったのですが、その前後などは「こういうコンポーネントあったら便利だよね」というのが次々実装されていっていて、勢いを感じました。(今現在OpenしているIssueが1000弱あり、PullRequestは66個あります。) それだけ発展途上という見方もできますが、次々にバグや使い勝手の良い機能が充実していくのでユーザーとして非常にありがたいです。

一緒についてくるCSSとレイアウトコンポーネントが優秀

Vuetifyがいくらコンポーネントがあると言っても、自分が使うときには少しデザインを直したいことが多いです。たとえばボタンと他の要素との距離を取りたいとか、画面の中央に浮かせたいとかですね。

そういう時 style で書いたりcssでクラス定義してやることが多いと思います。最近はBEMに従ってクラス名を書いて、Vueのscoped styleで外部に影響を及ぼさないようにして、というふうに書くわけですがどうしてもクラス間でのスタイルのバッティングや、paddingのpxを埋め込んでしまって後で直すのが大変だったりします。sassを使って変数定義しても良いのですが僕は面倒くさがりなので余りやりたくないのです…

Vuetifyではコンポーネントデザイン用のCSSと別に、スタイル調整用CSSがついています。このCSSがfunctional(名前どおりにスタイルが適用される)に実装されていて、これがとても使い勝手が良いのです。

たとえば class="pt-2" と書くと padding-top が 4 * 2 = 8px だけ付く、見たいな感じです。これを使うことでその場限りのデザイン用のスタイルをささっと書くことができます。また Vuetify のコンポーネントのスタイルよりもこのCSSのほうが強く付く様に定義されているため、自分が書いたclassが反映されない!!というよくある悲しい事件がないため、非常に使い勝手が良いです。

こだわりポイント

つらつらとフレームワークのいいとこを書きましたので、次にぐるぐるに入れている僕のこだわりポイントを紹介したいなと思います。

submission でスコアが上がった時の通知

通称?「緑の画面」と呼ばれているものです。これはsubmissionしたときのスコアが過去最高だったとき、画面全体を使っておめでとうコメントとスコアをカウントアップして出してくれる機能です。いいスコアがでたら喜びたいよね?というので作りました。結構反響があってわりと嬉しいです:D

使っているちょっと込み入った?こととして数値のカウントアップ部分があります。これは Tween を使って自作しています。

<template>
  <span>{{ text | toFixed(fixed) }}<template v-if="showChange">
    {{ change | toFixed(fixed) }}
  </template>
  </span>
</template>

<script>
import TWEEN from '@tweenjs/tween.js'

function animate(time) {
  requestAnimationFrame(animate)
  TWEEN.update(time)
}
requestAnimationFrame(animate)

export default {
  props: {
    value: {
      type: [Number, String],
      default: null
    },
    from: {
      type: [Number, String],
      default: null
    },
    duration: {
      type: [Number, String],
      default: 1000
    },
    delay: {
      type: [Number, String],
      default: 0
    },
    fixed: {
      type: [Number],
      default: 4
    },
    showChange: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      current: null,
      change: null
    }
  },
  computed: {
    text() {
      return (+this.current).toFixed(this.fixed)
    }
  },
  watch: {
    value(newVal, oldValue) {
      if (!!this.from) this.countUp(+this.from, newVal)
      if (oldValue) this.countUp(+oldValue || 0, newVal)
    }
  },
  mounted() {
    this.current = this.value
  },
  methods: {
    run() {
      this.current = this.from
      this.countUp(this.from, this.value)
    },
    countUp(start, end) {
      this.current = +start
      this.change = +end - this.current
      const updateObject = { value: +start }
      new TWEEN.Tween(updateObject)
        .delay(+this.delay)
        .to({ value: +end }, +this.duration)
        .onUpdate(() => {
          this.current = updateObject.value
        })
        .start()
    }
  }
}
</script>

実際の画面は参加してsubmitしてからのお楽しみということで、ここには貼らないでおきます😌

emoji で可愛く

基本的にコンペって殺伐としているじゃないですか。*1なのでちょっとでも可愛い要素を入れたいなと思い絵文字を導入しています。

これは twitter の絵文字をレンダリングする twemoji というライブラリを使って実装していてカスタムディレクティブとして global に定義して

import Vue from 'vue'
import twemoji from 'twemoji'

Vue.directive('emoji', {
  inserted(el) {
    el.innerHTML = twemoji.parse(el.innerHTML)
  }
})

こんな感じでつかいます

<template>
  <div v-emoji>🚀</div>
</template>

こうすると v-emoji で囲った部分に含まれる絵文字を twitter 社が出している絵文字の画像 dom に変換してくれるので、どの環境でみても同じような表示になってとてもハッピーです。 たとえばですがパスワードリセットページ https://www.guruguru.ml/auth/reset などに使っています。

f:id:dette:20200311081312p:plain
パスワード忘れるの、年を取ると増えますよね。悲しいです。

LeaderBoard Chart Race [NEW!]

棒グラフが時系列でどのように変化するかをアニメーションで表示したのが barchart race と呼ばれるものです。最近 twitter とかでも良く見かけていてとてもわかり易いし見ていて楽しいので「リーダーボードにも適用したら面白んじゃないか?」と思って前回の atmaCup#3 のときにウェブサービスを使って作成したのが好評だったため、自作してみました。

見ていただくのが早いので動画を貼りましたが、やっていることは単純で

  1. コンペ開始から終了までを一定間隔で区切って
  2. それぞれの時間までの一番良いsubmissionの値をチームごとに保存
  3. スライダーで設定された時間のスコアを並び替えて表示

という感じです。この画面ですがVue.jsのデータバインディングの効果が実に遺憾なく発揮されていて、僕は上記のチームごとのスコアの配列を作ることだけに集中できたので、結構さっくり作ることができました。むしろアニメーションや画面上の配置、色などのデザイン部分のほうが時間がかかっていた気がします。
とはいえ、アニメーションも Vue.js のリストトランジション機能を使っているので実質3行ぐらいで実装できてしまうのでVue.jsおそろしやという感じです。

まとめ

ぐるぐるのフロントエンドについて書きました。データ分析をしていてフロントをやっている人はあまりいないようなイメージがありますが(やっていてもAPI開発のバックエンドの方が多いような肌感)、データを見やすいようにグラフに落とし込んでいく作業と、ユーザーが使って楽しい・使いやすいサイトにするにはどう配置をしたら良いかを考えつつ実装していくフロントエンドの親和性はかなり高いなと感じています。

この記事をみて少しでもフロントエンド面白いな・このシステムが動いているatmaCup出てみたいなと思っていただければ幸いです。😆

atmaCup#5も開催できるように色々と練っているところですので、参加いただけると大変うれしいです。

参考リンク

*1:諸説あります