Utility First な CSS in JS フレームワークの導入と3ライブラリの比較

JX 通信社のフロントエンドでは React TypeScript や Emotion のような CSS in JS を技術選定することが多いです。弊社 SaaS の FASTALERT、新型コロナ関連情報などでも同様の技術選定で、過去にもエンジニアブログで紹介してきました。

tech.jxpress.net

tech.jxpress.net

今日は、Emotion の活用の極地「Utility First な CSS in JS フレームワーク」についてご紹介します。

Emotion で開発する悩み

素の Emotion や類似の CSS in JS ライブラリでは、 1 つの TS/JS ファイル内に CSS を書くような感じでスタイル設定を行っていきます*1。CSS in JS ライブラリに概ね共通しているのが、 styled.タグ名 でスタイリングすることです。

const Title = styled.h1`
  font-weight: bold;
  font-size: 20px;
`

const Entry = styled.div`
  line-height: 1.5;
  margin-top: 20px;
`

type Props = {
  title: React.ReactNode
}

const Article: React.FC<Props> = ({ title, children }) => {
  return <>
    <Title>{title}</Title>
    <Entry>{children}</Entry>
  </>
}

コンポーネント志向で作っていく上で、上記のコードには責務的な問題があります。 Entry は「上部の要素と 20px 空ける」というマージン指定をしていますが、「隣接要素との距離」は当該コンポーネントの責務外です。あくまで Entry を呼び出す側(親)が、どれくらいの間を開けるのが適切か知っているべきです。

Utility First な CSS in JS フレームワークを導入すると、次のように書くことができます。以下の例での「mt」という prop は、margin-top を指定するための prop です。

import { Title, Entry } from '~/components'

const Article: React.FC<Props> = ({ title, children }) => {
  return <>
    <Title>{title}</Title>
    <Entry mt={4}>{children}</Entry>
  </>
}

マージン、サイズ、色などのあらゆるスタイルが prop で渡せるような CSS in JS フレームワークを、本稿では「Utility First な CSS in JS フレームワーク」と呼称します。このようなフレームワークは、素の Emotion や styled-components を拡張するような形(一緒にインストールする)で作られています。

Utility First な CSS in JS フレームワークのメリット

上記の例は inline style (style prop にわたす形)でも実現できる例ですが、Utility First なフレームワークには次のようなメリットがあります*2

  • 型安全性が高い
  • レスポンシブに対応している
  • デザインシステムとの親和性が高い
  • ホバー時などの挙動も指定できる

例えば、以下のコードは、上記のメリットを同時に満たしています。

import { x } from '@xstyled/emotion'

const Box = (props) => {
  return <x.div
    // primary のような独自定義の色名が型安全に
    color="primary"
    // レスポンシブ指定
    display={{ _: "block", md: "flex"}}
    // ○○px じゃなく、デザインシステムに則ったマージンパターンを指定
    mt={4}
    // ホバー時の色指定
    hoverColor="red"
    {...props}
  />
}

実際に書いてみるとわかるのですが、 TypeScript (JavaScript)の仕組みの上で動いているためコード補完が効きやすく、コード量も少なくスタイルが書けるので、生産性が高く感じられます。そのため、自分が関わるプロジェクトでは積極的に導入しています。

f:id:yamitzky:20210404212309g:plain

ここからは、このような特色を持った React 向けの CSS in JS フレームワークを3つ紹介していきます。いずれのフレームワークも、2019年ごろにリリースされ、アクティブに更新されています。特に最近は Chakra UI の人気が高いようです。2021 年 4 月現在の GitHub Stars をライブラリ名に併記しています。

www.npmtrends.com

Chakra UI (★16.6k)

chakra-ui.com

Chakra UI は Utility First な CSS in JS フレームワークとしての機能と、(Material UI のような)リッチな既成コンポーネントが一緒になったライブラリです。 <Box> が基本的なユーティリティコンポーネントになっているので、これを使っていきます。<Flex><Grid> といったレイアウト用の Box も用意されています。

import { Box } from "@chakra-ui/react"

const Entry = (props) => {
  return <Box 
    // as で div 以外のタグを指定できる
    as="section"
    // レスポンシブは { base: 2, md: 3 } のような形だけでなく、array で指定できる
    m={[2, 3]}
    color="gray.600"
    {...props}
  />

xstyled (★1.6k)

xstyled.dev

xstyled も Chakra UI と同じようなユーティリティ prop が定義されていますが、CSS in JS フレームワークとしての機能に特化しています。xstyled は <x.タグ名> というコンポーネントが用意されていて、通常のタグでのコーディングと書き味が似ています。

import { x } from "@xstyled/emotion"

const Entry = (props) => {
  return <x.section
    // レスポンシブ
    m={{ _: 2, md: 3 }}
    // 
    color="primary"
    {...props}
  />

4月1日にリリースした、信濃毎日新聞様との参院長野補選特設ページ でも使っています。

Theme UI (★3.6k)

theme-ui.com

Theme UI も Chakra UI 同様、 CSS in JS フレームワークとしての機能と、一部既成コンポーネントが一緒になったライブラリです。Chakra UI に比べて、既成コンポーネントは少なく、また、prop で網羅的に CSS 指定できることを志向していません*3。そのため <Box> だけでなく sx という prop (style prop に類似)を併用して使います。

import { Box } from "@xstyled/emotion"

const Entry = (props) => {
  return <Box
    m={[2, 3]}
    sx={{
      // textAlign という prop は Box に存在しない
      textAlign: 'center'
    }}
    {...props}
  />

番外編1:Tailwind CSS

最近もっともよく聞く Utility First な CSS フレームワークは Tailwind CSS でしょう。Tailwind はスタイルをクラス名で指定していきます。Tailwind はそもそも CSS-in-JS のライブラリではないので、番外編としました。Chakra UI や xstyled と似たような機能が生えていますが、型安全な prop で渡すわけではありません。

const Box = (props) => {
  return <div
    className="mt-4 rounded text-blue-600 md:text-green-600"
    {...props}
  />
}

番外編2:自前で整える

本稿で紹介したようなライブラリを入れたくないという人もいると思います。これらのライブラリは emotion や styled-components の上に作られているフレームワークに過ぎないため、自前で整えていくことも可能です。

// mixins.ts
type MarginProps = {
  m?: CSSProperties['margin']
}
const marginMixin = ({ m }: MarginProps) => ({
  margin: m,
})

// Entry.tsx
const Entry = styled.div<MarginProps>`
  ${marginMixin}
  line-height: 1.5;
`

// Page.tsx
const Page = () => {
  return (
    <>
      <Title />
      <Entry m={30} />
    </>
  )
}

自前であれば軽量に始めやすいのですが、最初に紹介したようなメリット(レスポンシブ等)を網羅的に用意していくのが相当面倒くさいです。styled-system のようなライブラリもありますが、メンテされていません。*4

IE 対応

Internet Explorer のサポートを捨てられない、という場合もあると思います。対応状況を簡単にまとめたので、ぜひ参考にしてください。

ライブラリ 公式サポートの宣言 動作 補足
Chakra UI
xstyled ごく一部に CSS Variables が使われているので、それを避ければ可能*5
Theme UI 公式サポートの記載はないが、CSS Variables を使わないための設定が明示されている
core-js/web/dom-collections のポリフィルが必要
Tailwind 未調査

残念ながら、明確に公式サポートを謳っているライブラリはないのですが、 xstyled、Theme UI あたりは動作可能です。先ほど紹介した参院長野補選の特設サイト では IE 11 もサポートしていますが、概ね問題なく動いています。

IE で全く動かないものは、CSS Variables を使っているものです。CSS Variables の方がパフォーマンスはよさそうなので、ブラウザ互換性とパフォーマンスとのトレードオフになりそうです。

まとめ

本稿では、Utility FIrst な CSS in JS フレームワークを紹介しました。デザインシステムや TypeScript との親和性が高く、生産性高くコーディングできるので、ぜひ利用を検討してみてください。

また、JX 通信社ではフロントエンドに限らずインターン生を通年で募集しています。フルリモートでも働けます。

*1:エディタのプラグインを入れると、スタイル部は CSS のようにハイライトされます

*2:xstyled の説明を元に加筆しています

*3:下位互換というよりは、思想の違いだと思います

*4:styled-system の作者が Theme UI を開発メンバーの一人です

*5:ソースコードを var で検索するとでてきますが、Flex 向け spacing や transform などです。色、サイズ、マージンなどの基本機能は問題なく動きます。

Google App Engineではじめる, らくらくTV砲対策 - AIワクチン接種予測の舞台裏

JX通信社シニア・エンジニアの@shinyorke(しんよーく)です.

ちょっと前のお話になりますが, JX通信社のニュース速報アプリ「NewsDigest」で, 「AIワクチン接種予測」という新機能の提供を開始しました.

prtimes.jp

「自分がいつコロナワクチンを接種できるか?」を簡単に予測できるサービスです. 使っていただけると嬉しいです🙏

大変ありがたい事に,「AIワクチン接種予測」はリリース後多くの反響を頂いていまして,

  • リリースから約半月で利用回数が100万回を突破(プレスリリース).
  • 同じく, リリースから半月で20本以上ものTV番組で紹介

と, 多くのユーザーさんにお使いいただきました. ありがとうございます🙏

これだけ多くの方に使っていただくとなると,

  • リクエスト数・ユーザー数の増減に合わせたコンピューティングリソースの配分
  • 特に, 「TV経由で認知したユーザーさんが一気にやってくる」という急激なトラフィックの増加(いわゆるTV砲)に耐える構造と運用

が重要となってくるのですが,

「AIワクチン接種予測」はGoogle App Engine(GAE)の基本的な設定でTV砲のアクセス増から無事サービスを守りきりました.

私はこのプロジェクトにおいて, プロダクトマネージャー兼エンジニアをさせていただきましたが, ひとまずこの山を乗り越えてホッとしています.

このエントリーでは,

  • 「AIワクチン接種予測」のざっくりなアーキテクチャ
  • 「TV砲をさばく」ためにやったこと

を可能な限り紹介します.

TL;DR

瞬間風速でやってくるトラフィックはApp Engineの基礎を知っていればいい感じにさばけます

おしながき

このエントリーで扱うこと(扱わないこと)

このエントリーでは「AIワクチン接種予測」の以下の話について扱います

  • Google Cloud Platform(GCP)まわりのアーキテクチャ話
  • Google App Engine(GAE)に関するTips

上記の話題に関して網羅的に扱います.

また, 以下の件については後日公開予定(もしくは非公開)とさせてください.

  • フロントエンド関連の話題
  • 予測モデルの内容および運用に関する全般的な話題

ご了承ください🙇‍♂️

AIワクチン接種予測のアーキテクチャ

全体像

AIワクチン接種予測のプロダクトは,

  • Next.js + TypeScript(フロントエンド)
  • Flask, Fast API(バックエンド)
  • App Engine(一部Cloud Run)

で構成されています.

ざっくりな全体像はこちらです.

f:id:shinyorke:20210224211951j:plain

ホントはストレージやCDNなど, 「一般的なWebサービスでいるもの」も当然いますが図では割愛しています🙇‍♂️

ポイントとしては,

GAE(一部Cloud Run)の採用により, スケールしやすい構成を取ったことです.

この構成のおかげでTV砲対策(一時的なインスタンス増加)がすごくやりやすくなりました.

App Engineを全力で使う

AIワクチン接種予測のプロジェクトではGCP, 特にGAEを全面的に採用しました.

サービス構成を決めるにあたり, 社内で何人かのメンバーに相談した結果,

  • シンプルなアプリケーションになりそうなので, 全力でサーバレスを前提としたアーキテクチャに乗っかれそう
  • 一時的な負荷増に対する対策(例えばメディアに取り上げられるなど)とかも楽にできるといいよね

という視点で考えた結果, チーム内で提案(と使いたい要望)があったGAEに決まりました.

私も, 以前在籍したベンチャーでGAEを運用した経験があり, GAEの利点(と辛み)を理解していた(かつ私もメッチャGAEを使いたかった)のでアッサリとGAE採用を決めました*1.

構成図の通り, フロントエンドとバックエンドの主要サービスはGAEにしたのですが, バックエンドの処理の一部(画像生成など)でGAEでやるにはややこしい部分*2があったので一部の処理をCloud Runで構築するなどしました.

実践・TV砲対策

実際のTV砲対策を(話せる範囲で)紹介します.

「AIワクチン接種予測」はNewsDigestのイチ機能としての提供であるため,

  • AIワクチン接種予測本体(GAE + Cloud Run)の負荷対策
  • 接種予測に至るまでの導線を提供する, NewsDigest(のバックエンド)の負荷対策
  • 広報, セールス等を含めたTV出演情報の共有(メディア対策もあるが障害時のエスカレも含む)

これらを出演の度に行いました.

App EngineとCloud Runの負荷対策

GAEとCloud Runの対応は,

TV出演でトラフィックが増えそうな時間帯に限り, インスタンスの数で押し切る(&トラフィックが落ち着いたら元に戻す)

というシンプルな対策で乗り切りました*3.

より具体的には公式ドキュメントを参考に,

  • TV出演の前に, GAEのapp.yamlmin_instances および max_instances の数を増やす
  • TV出演が終わり, トラフィックが落ち着いた頃合いで上記パラメータを元に戻す

これらを愚直にやりました.

なお作業はシンプルで, 定義値を変更したapp.yaml を含んだアプリをデプロイする. たったこれだけでした.

cloud.google.com

基本的には公式ドキュメントを読んで,設定を決めて対応しました*4.

また, Cloud Runの負荷対策も似たような感じで,

  • Cloud Runのコンソールでインスタンス数(最小・最大)を適切な値に設定
  • 上記を再デプロイ(ボタン一発)

これで終わりました.

cloud.google.com

私が携わるプロジェクトでCloud Runを採用したのは初めてでしたが, GAE同様違和感なく対応できてよかったです.

NewsDigestの負荷対策

AIワクチン接種予測に訪れるユーザーさんは必ずNewsDigestの導線を通ることになるので, NewsDigestの負荷対策も重要なタスクの一つでした.

以前はNewsDigestの構成を知っているメンバーが負荷対策をしていたのですが, TVでの露出が多くなると知ってる人に頼るのもどうかなー?と, SREのたっち(@TatchNicolas)さんに相談した結果,

  • 負荷対策の手作業オペレーションを社内ツール化して半自動化. 具体的には手元でスクリプトを叩けばOKぐらいに簡略化
  • 上記の社内ツールを担当者(今回は私)にハンズオンして引き継ぎ

...といった事を爆速で行ってくれました(圧倒的感謝).

このおかげで今まではNewsDigestの負荷対策を中身を知ってるメンバーにお願いしてたのですが, 私自身がコマンド一発でできるようになりました.

これぞDevOpsの醍醐味ですね, 素晴らしい.

ちなみにこの話を相談したのがリリースした2/15から間もない頃で, 翌日には爆速で仕組みができあがっていたので, 流石に驚きました.

TV出演スケジュール管理

今回はTVに連続して出る, という状況が続いたのでスケジュール管理が重要でした.

スケジュール管理はCTOの柳さんを中心にトライアルで導入を進めている, 今流行りのNotionを活用しました.

具体的な利用・感想については, AIワクチン接種予測に色々協力いただいた藤井さんが背景も含めた素晴らしいnoteにまとめていただいたのでこちらをご覧いただけると幸いです.

note.com

Notionの該当ページを見たら「出演時間」「内容」「負荷対策やってますか?」的な内容・チェックリストが確認できる仕組みだったのでとても楽でした.

結び

今回は「AIワクチン接種予測」の, 主にインフラやTV露出対策をどうしたか?というお話を紹介しました.

AIワクチン接種予測のプロダクト単体で言えば, 「GAE採用してよかった」というオチになるのですが,

  • NewsDigest本体の負荷対策をカイゼンしてくれたり
  • Notionを中心とした情報共有・オペレーションの最適化をしたり

といった, チーム力が生きたと思いますし, これが何よりもの「TV砲対策をらくらくにした真の理由」だったのかなと思っています.

また, 今回のプロジェクトではフロントエンド・サーバーサイド両方でエンジニアインターンの皆さんが活躍してくれました(圧倒的感謝).

www.wantedly.com

www.wantedly.com

JX通信社ではインターンの皆さんもユーザーさんに直接価値を届けるような開発タスクができます, ご興味ある方はぜひカジュアル面談来てください.

最後までお読みいただきありがとうございました!

*1:ちなみに私がGAEを触るのは5年ぶりでした

*2:例えばフォントの指定など. GAEだとできなかったっぽいのでCloud Runにしました.

*3:もっというと, このようなシンプルな対策で収まる事を期待してGAEとCloud Runにしました&狙いは見事に的中しました.

*4:個人的な話でいうと, GAEを使ったのが5年ぶりでそこそこブランクがあったのですが, 解説がわかりやすく割とアッサリ勘を取り戻せました. ドキュメント大切ですね.

リモートでも 1on1 の効率を最大化したいのでGROW モデルを導入してみました

f:id:jazzsasori:20201228225509p:plain

JX通信社 Engineering Manager の @jazzsasori です。

皆さん自身の成長にコミットしてますか?
マネージャーの皆さんメンバーの成長にコミットしてますか?
私はゼルダ無双の体験版をダウンロードしてしまったために成長にコミットできなさそうです。
あと買ってしまいそうです。

弊社もリモート中心のメンバーが増えました

こんなご時世なので弊社も多くのメンバーの勤務がリモート中心となって久しいです。
弊社はSlack, Zoom, Discord を活用、リモートに関する制度の充実などにより比較的コミュニケーションはうまくいっているように思います。
が、ご多分に漏れず多少のコミュニケーションに関する問題も起こっているのも事実です。

最近メンバーとエモい話してますか?
私は昭和の人間なので飲みニケーションが好きです。
私は生中を飲みながら「やったろうぜー!」なんて言いながらウェイするのが好きです。
翌日多少の二日酔いを抱えながら昨日の (半分覚えていない) 熱い話を思い出しながら仕事をがんばるのが好きです。
以前ならなんとなくの日常のコミュニケーションを通じて放っておいても伝わってきたメンバーの希望や今後やりたいことなどがなかなかカジュアルに知れなくなってきています。

私が1on1を担当している方々にGROWモデルを提案しました

そういった差分を埋めるためにどうしたらよいか。
自分の答えはGROWモデルを通したコーチングでした。

私はGROWモデルで皆様をブチ上げたい

というタイトルで社内のwikiツール (kibela) に投稿し、詳細を説明。
あくまで各自の判断にゆだね、やるやらを決めました。
結果一旦は自分の1on1 担当している方全員やっていただけることになりました。

GROW モデルとは

はじめのコーチング を書かれた John Whitmore さんなど (Graham Alexander、Alan Fine) が考えられたコーチングモデルです。
Google さんも re:Workで採用 されています。
Google さんのre:Work に沿って説明すると大きく4つのテーマに分かれており、それぞれで以下のようなことを質問し、チームメンバーとマネージャーが話し合います。

Goal: 目標の明確化

  • 「1 年後、5 年後、10 年後の自分はどうなっていると思いますか?」
  • 「収入や現在のスキルの制約がないとしたら、どのような仕事に就きたいと思いますか?」
  • 「興味、価値を置くもの、原動力となっているものは何ですか?」

Reality, Resource: 現状の把握 (Google さんの re:Work では Reality のみ)

  • 「現在の業務で最もやりがいを感じること、あるいは、ストレスを感じることは何ですか?」
  • 「現在の業務はやりがいがありますか?能力を伸ばせていますか?どうしたらさらにやりがいを感じられますか?やりがいのない業務は何ですか?」
  • 「自分の長所と短所について、他の人からどんな指摘を受けたことがありますか?」

Option: 選択肢の検討

  • 「以前話し合った目標達成のためのスキルを磨くのに、今できることは何ですか?」
  • 「自分を伸ばすために、どのような仕事やプロジェクトに挑戦したいですか?また、どのような経験をしたいですか?」
  • 「選択肢として、どのようなネットワークやメンターシップがありますか?」

Will: 意思の決定

  • 「何を、いつまでに行いますか?」
  • 「役に立つリソースは何ですか?目標達成のために役立つスキルは何ですか?」
  • 「どのような支援が必要ですか?自身のキャリア形成について、マネージャーやリーダーからどのようなサポートを受けたいと考えますか?」

私のGROWモデルを使った具体的なコーチングスタイル

Google さんが公開しているワークシート をコピーして面談に活用することが多いです。
とても良いフォーマットなので全力で乗っかりつつ、自分は相手に合わせて質問を変えたりしています。

「1 年後、5 年後、10 年後の自分はどうなっていると思いますか?」

という質問例がありますが、実際に話を進めていると "5年後" "10年後" は意外と想像しにくいものです。
そこで比較的想像しやすい1年後を聞いたあとに「その1年での成長をベースに2年後3年後はどうなっていたいですか?」と聞くとさらに深い話ができたりします。 逆に1年未満の期間での Goal を聞いてみたりしています。

一番重視していることは傾聴することです。 いいおじさんなので 武勇伝臭いことを語り始めないようにしてます。 コロナ禍でなかなかカジュアルに飲みにもいけませんので、GROW を通したコーチングをよいきっかけとしてメンバーの目指す場所を注意深く聞いています。
酩酊した状態で聞くよりはメンバーの意思を細かく受け取れている気がします。 (いや飲んでてもちゃんと聞いてますよ)

GROWをやってみて

結果として短期間で目に見えた成長を感じることが出来ました。

メンバーの意識が変わった

  • 今まで手を伸ばしていなかった領域を学ぶ時間を確保する
  • frontend が中心だったメンバーが backend のタスクを積極的をこなす (逆も)

対話を通してメンバーの役割を変更

  • スクラムマスターを目指すメンバーの役割を明確に変えた
    • 結果、スクラムマスターとしての役割を積極的にこなしていただけるようになった

良い反響があったり、正直うまくいかなかったり

自分が普段担当していない方にもGROW のために1on1お願いしてもらったりしました。

反面実際やってみてその方には合わないケースもありました。
そういった場合は別の方法で1on1を進めたりしています。
もちろん組織や人によって合わないケースもあると思います。

あくまで大事なのはメンバーとの対話なので柔軟に対応しましょう。

終わりに

私はGROW モデル試してみてとても良かったです。
1on1 においてより深い対話ができていると思います。
あくまで GROW も1つのフレームワークです。方法は組織ごとに色々あってよいと思います。
重要なことなのでもう一度、対話を大事にしましょう。

データサイエンティストの飛び道具としてのStreamlit - プロトタイピングをいい感じにする技術

(ちょっと遅れましたが)新年あけましておめでとうございます🪁

JX通信社シニア・エンジニアで, データサイエンスからプロダクト開発までなんでもやるマンの@shinyorke(しんよーく)と申します.

Stay Homeな最近は大河ドラマを観るのにハマってます&推しの作品は「太平記」です*1.

データ分析やデータサイエンティスト的な仕事をしていると,

「いい感じのアウトプットがでた!やったぜ!!なおプレゼン🤔」

みたいなシチュエーションが割とあると思います.

さあプレゼンだ!となったときにやることと言えば,

  • ドキュメントとしてまとめる. 社内Wikiやブログ, ちょっとしたスライドなど.
  • 分析・実験で使ったモノをそのまま見せる. より具体的に言うとJupyterのnotebookそのもの.
  • 社内のいろいろな方に伝わるよう, ちょっとしたデモ(Webアプリ)を作る.

だいたいこの3つのどれかですが, やはり難易度が高いのは「ちょっとしたデモ作り」かなと思います.

???「動くアプリケーションで見たいからデモ作ってよ!」

この振りってちょっと困っちゃう*2な...って事はままある気がします.

そんな中, 昨年あたりからStreamlitというまさにこの問題をいい感じに解決するフレームワークが流行し始めました.

www.streamlit.io

これがとても良く使えるモノで, 私自身も,

  • 昨年のPyCon JP 2020など, 登壇や個人開発にてプロトタイプが必要なときに利用.*3
  • 業務上, 「動くアプリ」を元にコミュニケーションが必要だったりプレゼンするときに利用

といった所でStreamlitを愛用しています.

とても便利で素晴らしいStreamlitをご紹介ということで,

  • Streamlitをはじめるための最小限の知識・ノウハウ
  • JX通信社の業務においてどう活用したか?
  • Streamlitの使い所と向いていない所

という話をこのエントリーでは語りたいと思います.

TL;DR

  • データサイエンティストが「アプリっぽく」プレゼンするための飛び道具としてStreamlitは最高に良い.
  • Jupyterでできること, Webアプリでできることを両取りしてPythonで書けるのでプロトタイピングの道具として最高
  • あくまで「プロトタイピング」止まりなので仕事が先に進んだらさっさと別の手段に乗り換えよう

おしながき

Streamlitをはじめよう

StreamlitのサイトSample Galleryドキュメントなどかなり充実しているのでそちらを見ていただきつつ, 触りながら覚えると良いでしょう.

触ってみよう

インストールそのものはPythonのライブラリなので,

$ pip install streamlit

でいけちゃいます.

$ streamlit hello

とコマンドを叩くと, http://localhost:8501 で用意されているデモが立ち上がります.

f:id:shinyorke:20210124152408j:plain

最初はデモを触る・コードを読みながら写経・真似しながら動かすと良さそうです.

重要なポイントとしては,

Webブラウザで動く動的なアプリケーションが, .py ファイルを書くだけで動く

ことです.

Javascriptやフロントエンドのフレームワークを使ったり, HTMLやCSSの記述が不要というのがStreamlitのミソです*4.

作って動かそう

hello worldで遊んだ後はサクッと作って動かすと良いでしょう.

...ということで, この先はイメージをつかみやすくするため,

f:id:shinyorke:20210124140258j:plain
こういうのを100行ちょいで作れます

このようなサンプルを用意しました.

github.com

サンプルの⚾️データアプリを元に基本となりそうなところを解説します.

README.mdに設定方法・動作方法があるので手元で動かしながら見ると理解が早いかもです.

(venv) $ streamlit run sample_demo.py

pandas.DataFrameを出力する

pandasに限らず,

  • 何かしらのテキスト
  • 何かしらのオブジェクト(グラフなど)

もそうなのですが, st.write(${任意のオブジェクト}) でいろいろなモノをブラウザで閲覧できるモノとして表示ができます.

たとえば,

st.write('# Stremlit Sample App :baseball:')
st.write('データは[こちらのアプリ](https://github.com/Shinichi-Nakagawa/prefect-baseball-etl)で作ったものです')

st.write('## ひとまずpandasデータフレームの中身を見る')
st.write('`st.write(df.head())`とかやればいい感じに')

import pandas as pd

df = pd.read_csv('datasets/mlb_batter_stats.csv')
st.write(df.head())

こちらはこのように表示されます.

f:id:shinyorke:20210124154332p:plain

※sampleのこの辺です.

作成中・試行錯誤の状態はこのような形でprint debugっぽいやり方でやると良さそうです.

入力フォームを使う

入力フォームもいい感じに作れます.

# サイドバーを使ってみる

st.sidebar.markdown(
    """
    # sidebar sample
    """
)
first_name = st.sidebar.text_input('First Name', 'Shohei')
last_name = st.sidebar.text_input('Last Name', 'Ohtani')
bats = st.sidebar.multiselect(
    "打席",
    ('右', '左'),
    ('右', '左'),
)

上記はこのような感じになります.

f:id:shinyorke:20210124154951p:plain

今回はsidebarという形で横に出しましたが, team_name = st.text_input('Team Name', 'Hanshin') という感じで, sidebarを介さず使うとページ本体に入力を設けることもできます.

フォームで入れたモノはこのように使えます.

st.write('## 打者の打席で絞る')

query = None
if '右' in bats:
    query = 'bats=="R"'
if '左' in bats:
    query = 'bats=="L"'
if ('右', '左') == bats:
    query = 'bats=="R" or bats=="L"'
if query:
    df_bats = df.query(query)
    st.write(df_bats.head())

f:id:shinyorke:20210124155351p:plain

※sampleのこの辺です.

これだけで, DataFrameをインタラクティブに使うアプリケーションが作れます.

グラフを描画する

また, 好きなライブラリでグラフなどを描画できます.

私はよくplotlyを好んで使うのですが,

import plotly.graph_objects as go


# グラフレイアウト
def graph_layout(fig, x_title, y_title):
    return fig.update_layout(
        xaxis_title=x_title,
        yaxis_title=y_title,
        autosize=False,
        width=1024,
        height=768
    )


title = f'{first_name} {last_name}の成績'

fig = go.Figure(data=[
    go.Bar(name='安打', x=df_query['yearID'], y=df_query['H']),
    go.Bar(name='本塁打', x=df_query['yearID'], y=df_query['HR']),
    go.Bar(name='打点', x=df_query['yearID'], y=df_query['RBI'])
])
fig = graph_layout(fig, x_title='年度', y_title=title)
fig.update_layout(barmode='group', title=title)

st.write(fig)

このコードはこうなります.

f:id:shinyorke:20210124155735j:plain

これをWebのちゃんとしたアプリで実装するのは苦労するのですが, ちょっと見せるレベルのモノがこれだけでできるのは感動モノです.

Streamlitを仕事で使う

実際の業務での活用ですが, 私の場合は以下のイメージで使っています.

f:id:shinyorke:20210124151037p:plain
実務でやってること(図)

より具体的には,

  1. まずはJupyterLabやGoogle Colabといった手段(どっちもJupyterが中心)でタスクをこなす
  2. こなしたタスクがいい感じになったらStreamlitでデモアプリを開発
  3. Streamlitで作ったデモでプレゼンを行い, チームメンバーからのフィードバックをもらう

というフローで活躍しています.

そもそもJupyterがアプリを作るのに向いてない所を補完するのがStreamlitの役割なので書き換えはすごく楽です.

JupyterからStreamlitへの書き換え(と比較)については, 以前こちらのエントリーに書いたのでご覧いただけると雰囲気がつかめると思います.

shinyorke.hatenablog.com

また, 「チームメンバーからのフィードバックをもらう」という意味では, 無味乾燥なセルでしかないJupyter(含むColab)よりも,

簡易的とはいえWebのアプリケーションとして見せることができるので, データサイエンティスト・エンジニア以外のメンバーにも伝わりやすい

という長所があります.

みんなで触る

(Streamlitに限った話ではありませんが)手元でWebアプリを動かせるということは, ngrokなど, 手元にあるアプリをproxyできる仕組みでチームメンバーに触ってもらいながらフィードバックをもらうことができます.

Streamlitの場合, デフォルトの設定だとhttp://localhost:8501 というURLが振られる(8501でhttpのportが使われる)*5ので,

$ # すでにstreamlitのアプリケーションが8501で立ち上がってると仮定して
$ ngrok http 8501

これで払い出されたURLを用いることにより, MTGの最中など限定されたシチュエーションで触ってもらいながら議論したりフィードバックをもらうことが可能となります.

様々な理由で決して万能とは言えない方法でもあったりします*6が, リモート作業・テレワーク等で離れた所にいても実際に見てもらいやすくなるのでこの方法はとても便利です.

Streamlitが不得意なこと

ここまでStreamlitの使い所・得意な事を網羅しましたが, 苦手なこともあります.

  • 複数ページに跨るアプリケーションを作ること. 例えば, 「入力->確認->完了 」みたいな複数ページの登録フォームを作るのは苦手(というよりできないっぽい).
  • Streamlitのデザインから変更して見た目を整える.例えば, 「(弊社の代表アプリである)NewsDigestっぽいデザインでデモ作ってくれ」みたいなのは辛い.

The fastest way to build and share data apps(データを見せるアプリを爆速で作るのにええやで) と謳っているフレームワークである以上, ガチのWebアプリなら考慮していることを後回しにしている(かつこれは意味意義的にも非常に合理的と私は思っています)関係上, 致し方ないかなと思います.

なお私の場合はこの長所・短所を把握した上で,

  • Streamlitに移植する段階である程度コードをクラス化したりリファクタリング(含むテストコードの実装)を行い, 将来のWebアプリ・API化に備える
  • 上記でリファクタリングしたコードをそのままFastAPIやFlaskといった軽量フレームワークでAPI化する

といった方針で使うようにしています.

アジャイルなデータアプリ開発を

というわけで, 「データサイエンティストがアプリを作る飛び道具としてStreamlit最高やで!」という話を紹介させていただきました.

最後に一つだけ紹介させてください.

私たちは、ソフトウェア開発の実践

あるいは実践を手助けをする活動を通じて、

よりよい開発方法を見つけだそうとしている。

この活動を通して、私たちは以下の価値に至った。

プロセスやツールよりも個人と対話を、

包括的なドキュメントよりも動くソフトウェアを、

契約交渉よりも顧客との協調を、

計画に従うことよりも変化への対応を、

価値とする。すなわち、左記のことがらに価値があることを

認めながらも、私たちは右記のことがらにより価値をおく。

アジャイルソフトウェア開発宣言より引用

「機敏(Agile)に動くもの作ってコミュニケーションとって変化を汲み取り価値を作ろうぜ!」というアジャイルな思想・スタイルで開発するのはエンジニアのみならずデータサイエンティストも同様です, XP(eXtreme Programming)はデータサイエンティストこそ頑張るべきかもしれません.

そういった意味では, データサイエンティストが使うPythonやその他のエコシステムも「スピード上げて開発して価値を出そう!」という所にフォーカスが当たり始めているのは個人的にとても嬉しいですし, こういった「アジャイルな思想の道具」を使って価値を出していくのは必須のスキルになっていくのではとも思っています.

このエントリーがデータサイエンティストな方のプロトタイプ開発に役立つと嬉しいです.

最後までお読みいただきありがとうございました, そして本年もどうぞよろしくお願いいたします🎍

*1:1991年度作品で, 2021年1月現在放送中の「麒麟がくる」と同じ脚本家さんの作品です.

*2:「そもそもWebアプリの作り方しらない」「作れるんだけど手間が」の二択かなと思います. なお, 私個人は(Streamlit関係なく)どっちでも無いですしむしろ好きな仕事だったりします笑

*3:機械学習的なタスクの成果を見せるデモとして使いました. (詳細はこちら

*4:Streamlitは, そんなWeb・フロントエンドの開発を(少なくとも初手では)やらずに済むようにできたものであると私は認識しています.

*5:ちなみにportを変えたい場合は, streamlit run sample_app.py --server.port 80という感じで, server.portというoptionの指定でいけます

*6:なお, ネットワークの帯域は動かしている環境次第で上手く回らない事もあるので決して万能ではなく, ちょいちょいトラブルもありましたというのを一応付け加えておきます.

Pythonでいい感じにバッチを作ってみる - prefectをはじめよう

JX通信社シニア・エンジニアで, プロダクトチームのデータ活用とデータサイエンスのあれこれ頑張ってるマン, @shinyorke(しんよーく)です.

最近ハマってるかつ毎朝の日課は「リングフィットアドベンチャー*1で汗を流してからの朝食」です. 35日連続続いています.

話は遡ること今年の7月末になりますが, JX通信社のデータ基盤の紹介&「ETLとかバッチってどのFW/ライブラリ使えばいいのさ🤔」というクエスチョンに応えるため, このようなエントリーを公開しました.

tech.jxpress.net

このエントリー, 多くの方から反響をいただき執筆してよかったです, 読んでくださった方ありがとうございます!

まだお読みでない方はこのエントリーを読み進める前に流して読んでもらえると良いかも知れません.

上記のエントリーの最後で,

次はprefect編で会いましょう.

という挨拶で締めさせてもらったのですが, このエントリーはまさにprefect編ということでお送りしたいと思います.

github.com

今回はprefectで簡単なバッチシステムを作って動かす, というテーマで実装や勘所を中心に紹介します.

prefectをはじめよう

prefect #とは

f:id:shinyorke:20201215220448j:plain

簡単に言っちゃうと, Pythonで開発されたバッチアプリのFrameworkで,

The easiest way to automate your data.

(意訳:あなたのデータを自動化していい感じにするのに最も簡単な方法やで)

がウリとなっている模様です.

公式リポジトリのREADME.mdの解説によると,

Prefect is a new workflow management system, designed for modern infrastructure and powered by the open-source Prefect Core workflow engine. Users organize Tasks into Flows, and Prefect takes care of the rest.

(意訳:prefectは今風のインフラストラクチャーに合わせて設計されたFrameworkで, 開発者はTaskとFlowを書いてくれたらあとはPrefect Coreがいい感じにワークフローとして処理するやで)

というモノになります.

ちなみにHello worldはこんな感じです.

from prefect import task, Flow, Parameter


@task(log_stdout=True)
def say_hello(name):
    print("Hello, {}!".format(name))


with Flow("My First Flow") as flow:
    name = Parameter('name')
    say_hello(name)


flow.run(name='world') # "Hello, world!"
flow.run(name='Marvin') # "Hello, Marvin!"

@taskデコレーターがついた関数(上記の場合say_helloがそう)が実際に処理を行う関数.

処理に必要な引数を取ったり関数を呼んだりするwith Flow("My First Flow") as flowの部分を開発者が実装, あとはよしなにやってくれます.

こんにちはprefect

という訳で早速prefectをはじめてみましょう.

一番ラクな覚え方・始め方は公式のリポジトリをcloneしてチュートリアルを手元で動かすことかなと思っています.

※私はそんなノリでやりました.

$ git@github.com:PrefectHQ/prefect.git
$ cd prefect
$ pip install prefect SQLAlchemy

SQLAlchemyが入っているのはひっそりとチュートリアルで依存しているからです(小声)*2.

ちなみにPython3.9でも動きました👍

ここまで行けば後はチュートリアルのコードを動かしてみましょう.

$ cd examples/tutorial
$ python 01_etl.py   

このブログを執筆した2020/12/18現在では, 06_parallel_execution.py以外, 滞りなく動きました.

ひとまずこんな感じで動かしながら, 適当に書き換えながら動かしてやるといい感じになると思います.

軽めのバッチ処理を作ってみる

exampleをやりきった時点で小さめのアプリは作れるんじゃないかなと思います.

...と言っても, 何にもサンプルが無いのもアレと思い用意しました.

github.com

baseballdatabankというメジャーリーグ⚾️のオープンデータセットを使って超簡単なETLバッチのサンプルです.*3

f:id:shinyorke:20201215205310p:plain

  • 選手のプロフィール(People.csv)を読み込み
  • 打撃成績(Batting.csv)を読み込み&打率等足りない指標を計算
  • 選手プロフィールと打撃成績をJOINしてのちcsvと出力

というETL Workflowなのですが, こちらの処理はたったこれだけのコードでいい感じにできます.

import logging
from datetime import datetime

import pandas as pd
import click
from pythonjsonlogger import jsonlogger
from prefect import task, Flow, Parameter

logger = logging.getLogger()
handler = logging.StreamHandler()
formatter = jsonlogger.JsonFormatter()
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(logging.DEBUG)


@task
def read_csv(path: str, filename: str) -> pd.DataFrame:
    """
    Read CSV file
    :param path: dir path
    :param filename: csv filename
    :return: dataset
    :rtype: pd.DataFrame
    """
    # ETLで言うとExtractです
    logger.debug(f'read_csv: {path}/{filename}')
    df = pd.read_csv(f"{path}/{filename}")
    return df


@task
def calc_batting_stats(df: pd.DataFrame) -> pd.DataFrame:
    """
    打率・出塁率・長打率を計算して追加
    :param df: Batting Stats
    :return: dataset
    :rtype: pd.DataFrame
    """
    # ETLで言うとTransformです
    logger.debug('calc_batting_stats')
    _df = df
    _df['BA'] = round(df['H'] / df['AB'], 3)
    _df['OBP'] = round((df['H'] + df['BB'] + df['HBP']) / (df['AB'] + df['BB'] + df['HBP'] + df['SF']), 3)
    _df['TB'] = (df['H'] - df['2B'] - df['3B'] - df['HR']) + (df['2B'] * 2) + (df['3B'] * 3) + (df['HR'] * 4)
    _df['SLG'] = round(_df['TB'] / _df['AB'], 3)
    _df['OPS'] = round(_df['OBP'] + _df['SLG'], 3)
    return df


@task
def join_stats(df_player: pd.DataFrame, df_bats: pd.DataFrame) -> pd.DataFrame:
    """
    join dataframe
    :param df_player: player datea
    :param df_bats: batting stats
    :return: merged data
    :rtype: pd.DataFrame
    """
    # ETLで言うとTransformです
    logger.debug('join_stats')
    _df = pd.merge(df_bats, df_player, on='playerID')
    return _df


@task
def to_csv(df: pd.DataFrame, run_datetime: datetime, index=False):
    """
    export csv
    :param df: dataframe
    :param run_datetime: datetime
    :param index: include dataframe index(default: False)
    """
    # ETLで言うとLoadです
    logger.debug('to_csv')
    df.to_csv(f"{run_datetime.strftime('%Y%m%d')}_stats.csv", index=index)


@click.command()
@click.option("--directory", type=str, required=True, help="Baseball Dataset Path")
@click.option("--run-date", type=click.DateTime(), required=True, help="run datetime(iso format)")
def etl(directory, run_date):
    with Flow("etl") as flow:
        run_datetime = Parameter('run_datetime')
        path = Parameter('path')
        # Extract Player Data
        df_player = read_csv(path=path, filename='People.csv')
        # Extract Batting Stats
        df_batting = read_csv(path=path, filename='Batting.csv')
        # Transform Calc Batting Stats
        df_batting = calc_batting_stats(df=df_batting)
        # Transform JOIN
        df = join_stats(df_player=df_player, df_bats=df_batting)
        # Load to Data
        to_csv(df=df, run_datetime=run_datetime)

    flow.run(run_datetime=run_date, path=directory)


if __name__ == "__main__":
    etl()

pandasの恩恵に授かって*4prefectのお作法に従うと比較的見通しの良いworkflowが書けますね, というのがわかります.

今回はcsvファイルを最終的なinput/outputにしていますが,

  • ストレージにあるjsonをいい感じに処理してBigQueryにimport
  • AthenaとBigQueryのデータをそれぞれ読み込んで変換してサービスのRDBMSに保存

みたいな事ももちろんできます(taskに当たる部分でいい感じにやれば).

この辺はデータ基盤やETL作りに慣れていない人でもPythonの読み書きができれば直感的に組めるのでかなりいいんじゃないかと思っています.

その他にできること&欠点とか

今回は「ひとまずprefectでETLっぽいバッチを作って動かす」という初歩にフォーカスしていますが, 実はこのprefect高機能でして,

  • タスクの進行状況をGUIで表示可能(AirflowとかLuigiっぽい画面)
  • 標準でDocker, k8sの他GCP, AWS, Azure等のメジャーなクラウドサービスでいい感じに動かせる

など, かなりリッチな事ができます.

一方, 使ったときのネガティブな感想としては,

  • 色々できるんだけど, 色々やるために覚えることはまあまあたくさんある.
  • 色々できるんだけど, それが故に依存しているライブラリとかが多く, 自前でホスティングするときのメンテ効率とかはちょっと考えてしまう.
  • ちゃんとデバッグしてないのでアレですが, 並列処理の機構がホントに並列で動いてるか自信がないときがある🤔

と, 心配なポイントもいくつかありました.

前のエントリーにも記載しましたが,

ETLフレームワーク, 結局どれも癖がありますので長いおつきあいを前提にやってこうぜ!

結局のところこれに尽きるかなあと思います*5.

結び - 今後のこの界隈って🤔

というわけでprefectを使ったいい感じなバッチ開発の話でした.

データ基盤や機械学習のWorkflowで使うバッチのFWやライブラリはホント群雄割拠だなあと思っていまして,

cloud.google.com

note.com

AirflowのDAGがシンプルに書けるようになったり(ほぼprefectと同じ書き方ですよね*6), BigQueryのデータをいい感じにする程度のETLならほぼSQLで終わる未来がくる(かも)だったりと, この界隈ホント動きが活発です.

このエントリーの内容もきっと半年後には古いものになってるかもですが, トレンドに乗り遅れないように今後もチャレンジと自学自習を続けたいと思います!

なおJX通信社ではそんなノリで共に自学自習しながらサーバーサイドのPythonやGoでいい感じにやっていく学生さんのインターンを募集しています.

www.wantedly.com

おそらく私が年内テックブログを書くのは最後かな...

皆様良いお年を&来年また新たなネタでお会いしましょう!

*1:執筆時点のLVは58, 運動負荷はMAXの30です. 筋肉が喜んでます💪

*2:このエントリーのため久々に試していましたがあっ(察し)となります.

*3:なぜ⚾のオープンデータ化というと, 私の趣味かつ手に入りやすい使いやすいオープンデータだったからです.

*4:この程度の処理だとprefectよりpandasの優秀さが目立つ気はしますが, デコレーターでいい感じにflowとtaskに分けられているあたりprefectの設計思想は中々筋が良いと言えそうです.

*5:ETLに限った話ではないのですが, 選んだ以上メンテをちゃんとやる, 使い切る覚悟でやるってことかなあと思っています.

*6:余談ですがprefectの作者はAirflowのコントリビューター?作者??らしいです.