はじめに
複数の就職先候補で迷っている就活生・転職希望者の方、たくさんいらっしゃると思います。
そうした方々に向けて、選択肢の中から最高の就職先を算出するアプリをつくりました。
後悔なく意思決定するための少しでも足しになればと。
JobHunter's Choice(ジョブハンターズチョイス)
サービス概要
**AHP(階層分析法)**という分析法を用いて複数の就職先の選択肢の中からベストなものを算出する、という手法を採用しました。
階層分析とは
複数ある選択肢のうちから最良のものを選択するための意思決定法です。
概要
例えばお部屋探しという目標に対して、評価基準(どの条件で選ぶか)と代替案(候補物件)があるとします。それらについて、
- 評価基準同士の重要性を比較する(ex:家賃と間取りのどちらをどの程度重視するか)
- 評価基準ごとに代替案同士を比較する(ex:家賃に関して物件Aと物件Bのどちらがどの程度すぐれているか)
以上をすべての組み合わせで行い、いろいろ計算すると、その人がどの評価基準をどの程度重視するかを加味した上で、それらの基準を最も満たす代替案を算出することができるわけです。
実績
- 工場の建設地の選定
- 大学教授の選抜
- ソフトウェアシステムの品質の数量化 etc..
企業活動、公共事業における幅広い分野で利用されているようです
サービスの目的
で、この作業には前述の通りいろいろな計算が伴うので、表計算ソフトとかでやると相当面倒です。そもそも分析のノウハウどころか階層分析の存在すら知らない人のほうが多いですよね。
というわけで、
就職先選びにおいて、定量的なデータを提供し、かつその手間を最小限まで削減する
これを目的として、サービス作成に至りました。
使い方
トップページのSTARTボタンを押すと分析開始。
##STEP1 選択肢の記入
就職先の選択肢を入力します。
入力欄はデフォルトで3つですが、必要に応じて増やすことが可能。
##STEP2 評価基準の選択
就職先選びの上で考慮する条件にチェックを付けます。
基準はデフォルトで出ているものの他に追加することが可能。
##STEP3 評価基準の重要度評価
STEP2で選んだ基準がそれぞれペアになっており、全ての組み合わせが表示されています。
7段階評価のボタンが用意されているので、以下の基準でどちらを重視するか選びます。
1 = 左側の基準を重視する
4 = 両方同じくらい
7 = 右側の基準を重視する
##STEP4 選択肢の評価
評価基準ごとに、STEP1入力した選択肢がそれぞれペアになって表示されています。
その基準においてどちらの選択肢が優れている(と思う)かをSTEP4と同様に7段階評価ボタンで評価します。
##結果
全て評価し終えてボタンを押すと、結果が表示されました!
階層分析によって企業ごとの評点が算出されており、総合評点が最も高い企業がベストチョイスになります。
各企業に各評価基準での評点が加算されていますが、重視する基準ほど多めに加算されているのがわかりますね。
##その他の機能
分析結果はTwitterでシェア可能。
また、アカウント作成しログイン状態で使うと分析結果を保存したり過去の入力データを再利用してより作業を簡略化できます。
↓ログイン時。過去の入力値がボタンで表示されます。
使用技術
Backend - Ruby on Rails 6.0.3
Frontend - Vue.js 2.6.12
UI - Vuetify 2.5.6, vue-chartjs 3.5.1, bootstrap 5.0.2
Infra' - AWS(EC2, RDS, Route53)
WebServer - nginx 1.20.0
AppServer - unicorn 6.0.0
DB - PostgreSQL 13.x
###段階評価ボタン
Vuetifyのv-btn-toggleで作成。
評点は$emitで上位のコンポーネントに渡し、表示順通りに配列に格納する形で集計。
[[Vue.js+Vuetify]段階評価ボタン ~評価項目の数と評価対象の数に応じて動的に表示する~]
(https://qiita.com/saitaman_zarathustra/items/ae60fa910e5d675baa5a)
ボタンのレスポンシブ対応
[Vuetify]v-btn-toggle内のボタン幅をレスポンシブ対応
###自動スクロール
window.scrollByでボタン間のy座標の差分だけ自動スクロールさせます。
ボタン間の距離が固定値でなかったので、両者のy座標を毎回取得するようにしました。
// ボタンを押したら次のボタンへと自動スクロール
autoScroll() {
const cur = event.currentTarget.getBoundingClientRect().top // そのボタンの上端のy座標
const nxtTarget = document.getElementById(`${次のボタンのID}`) // 自身の次のボタン(ターゲット)
const nxt = nxtTarget.getBoundingClientRect().top // ターゲットの上端のy座標
window.scrollBy(0, nxt-cur) //ターゲットと自身のy座標の差分だけスクロール
}
【追記】safariでのスムーススクロール対応
window.scrollByで自動スクロールさせる場合、chromeなどのブラウザではスムーズにスクロールしてくれますが、safariではアニメーションなしで急にスクロール後の画面になってしまいます。
safariでのスムーススクロール対応について追記します。
1 scroll-behavior-polyfillをインストール
% yarn add scroll-behavior-polyfill
import "scroll-behavior-polyfill"
2 window.scrollByの記述を以下に変更
window.scrollBy({
behavior: "smooth",
top: nxt-cur, // 現在のボタンと次のボタンのy座標の差分
left: 0
})
###フォームの追加
フォームでバインドしているalternativesの初期値を長さ3のnullの配列にしているためデフォルトでフォームが3つ表示されています。
この配列に新たにnullを加えるとフォームが追加されます。また履歴ボタンで追加するときはこの配列にその値を追加しています。
<template>
...
<input
v-for="(item, index) in alternatives"
:key="index"
v-model="alternatives[index]"
>
...
</template>
<script>
...
data() {
return {
alternatives: [null, null, null],
...
</script>
###チェックボックスの追加
デフォルトで表示されている項目がcriteriaで、チェックされた要素はselectedCriteriaに格納されます。
ユーザーオリジナルの項目を追加する際は、criteriaとselectedCriteriaの両方にその項目が追加されることでチェックが入った状態で項目が追加されます。
<template>
...
<v-checkbox
v-for="(item, index) in criteria"
:key="index"
v-model="selectedCriteria"
:value="item"
:label="item"
/>
...
</template>
<script>
...
data() {
return {
criteria: [
'労働時間',
'通勤時間',
'雇用の安定',
'仕事の裁量権',
'社会への貢献度',
...
],
selectedCriteria: [],
...
制作を終えて
こだわった点①
ユーザーをイラッとさせない快適に使える工夫を心がけました。
- ユーザーカスタムのチェックボックスは追加された時点でチェック済みの状態に
- 分析作業中にページ移動しても直前の入力値がキープされている
- ログインを要求するときはログインページにリダイレクトせずフォームをモーダルで表示
- ボタンを連続で押す場面での自動スクロール
こだわった点②
階層分析の知識がなくても引っかかりなく操作できるUI設計を目指しました。
- わかりにくい用語を別の表現で置き換える
- 評価ボタンの数字を、実際に加算する点数ではなくわかりやすい整数にする
- 分析結果のうちわかりにくいパラメータは任意表示にする
苦労した点① データの整形
入力値から評点を出すだけなら計算はそこまで煩雑ではありませんが、
- 表や円グラフ、棒グラフとして出力できる
- 開発者(自分)が識別できる
以上を満たす構造に整形する必要があり、それ用のメソッドたくさん作ってプラグインにしたりしました。Javascript力が試されました。
// 入力値
[[6,1,4], [3,2,5], [7,7,1]]
↑これを こうして↓
// 重要度のオブジェクト
[
{
name:'労働時間',
scoreString:{労働時間:'1', 通勤時間:'3', 収入:'1/5'},
score:[1, 3, 0.2],
geomean:0.8434,
weight:0.1883
},
{
name:'通勤時間',
scoreString:{労働時間:'1/3', 通勤時間:'1', 収入:'1/7'},
score:[0.3333, 1, 0.1428],
geomean:0.3624,
weight:0.0809
},
...
]
↓こうしたりなど。
// 結果のオブジェクト
[
{
name:'Goggle',
total:0.583,
multipledWeight:[
{criterion:'労働時間', value:0.157},
{criterion:'通勤時間', value:0.068},
{criterion:'収入', value:0.365}
]
},
{
name:'Appel',
total:0.416,
multipledWeight:[
{criterion:'労働時間', value:0.032},
{criterion:'通勤時間', value:0.020},
{criterion:'収入', value:0.365}
]
},
...
]
苦労した点② フロントエンド⇔データベース間でのネストされたオブジェクトのやりとり
フロントエンドで生成された分析結果Analysisには会社ごとの評点AlternativeResultと評価基準の重要度CriterionImportanceがそれぞれ複数個ネストされており、さらに会社ごとの評点の中に基準ごとの評点MultipledWeightが複数個ネストされています。
↓こんなのをバックエンドに送ってそれぞれのテーブルに保存する
// 分析結果のオブジェクト
{
criterionImportance: [
{
name:'労働時間',
scoreString:{労働時間:'1', 通勤時間:'3', 収入:'1/5'},
score:[1, 3, 0.2],
geomean:0.8434,
weight:0.1883
},
{
name:'通勤時間',
scoreString:{労働時間:'1/3', 通勤時間:'1', 収入:'1/7'},
score:[0.3333, 1, 0.1428],
geomean:0.3624,
weight:0.0809
},
...
],
alternativeResult: [
{
name:'Goggle',
total:0.583,
multipledWeight:[
{criterion:'労働時間', value:0.157},
{criterion:'通勤時間', value:0.068},
{criterion:'収入', value:0.365}
]
},
{
name:'Appel',
total:0.416,
multipledWeight:[
{criterion:'労働時間', value:0.032},
{criterion:'通勤時間', value:0.020},
{criterion:'収入', value:0.365}
]
},
...
]
}
フロント→DBへの保存
まず、これらのネストされたオブジェクトをどうデータベースに保存するかについて検討が必要でした。
- フロント側でバラす? →リクエストの回数が増えてパフォーマンスが落ちそう
- accepts_nested_attributes_forを使う? →非推奨
結果、FormObjectで複数リソースを同時に扱うときの手法を参考にしました。
非ActiveRecordインスタンスに子孫オブジェクトのデータを属性値として持たせ、そのインスタンスメソッド内でそれらの値にアクセスしそれぞれのモデルのレコードとして保存するという流れです。
[Vue→Rails]ネストされた状態の親子孫オブジェクト(一対多の関係あり)を全て同時にデータベースに保存する
def create
analysis = AnalysisObject.new(analysis_params)
if analysis.save
...
end
...
def analysis_params
params.require(:analysis).permit(
criterionImportance: [:name, :weight],
alternativeResult: [:name, :total, multipledWeight: [:criterion, :value]]
)
end
class AnalysisObject < ApplicationController
include ActiveModel::Model
include ActiveModel::Attributes
attribute :criterionImportance
attribute :alternativeResult
def save
ActiveRecord::Base.transaction do
analysis = current_user.analyses.create
self.criterionImportance.each do |cri|
analysis.criterion_importances.create(name: cri[:name], weight: cri[:weight])
end
self.alternativeResult.each do |alt|
alternative = analysis.alternative_results.create(name: alt[:name], total: alt[:total])
alt[:multipledWeight].each do |mul|
alternative.multipled_weights.create(criterion: mul[:criterion], value: mul[:value])
end
end
end
end
end
クラスの命名はAnalysisObjectでいいのか、、
DB→フロントへの読み出し
次に、別々のテーブルに入っているリソースを元のネストさせた形で取得するとき。
to_jsonメソッドのincludeオプションで親オブジェクトに子オブジェクトをネストさせることができるようです。
[Rails→Vue]一対多の関係にある親子オブジェクトをネストさせた状態で取得する
def show
importances = @analysis.criterion_importances
results = @analysis.alternative_results.map.to_json(include: :multipled_weight)
render json: { criterionImportances: importances, alternativeResults: JSON.parse(results) }
end
別のオブジェクトと同梱するために一度to_jsonしたresultsをまたRubyに戻すのが相当不格好ではある、、
他にも、attr_writerで親オブジェクトに仮想属性として子オブジェクトを含める方法が考えられましたが、JSON出力できなかったため断念しました。
おわりに
できる限りユーザーの手数を減らすことを意識しました。このアプリでかなり時短になるはずです(当社比)
ジョブハンターの皆さんぜひ使ってみてください!
#引用文献
[[Vue.js+Vuetify]段階評価ボタン ~評価項目の数と評価対象の数に応じて動的に表示する~]
(https://qiita.com/saitaman_zarathustra/items/ae60fa910e5d675baa5a)
[Vuetify]v-btn-toggle内のボタン幅をレスポンシブ対応
[Vue→Rails]ネストされた状態の親子孫オブジェクト(一対多の関係あり)を全て同時にデータベースに保存する
[Rails→Vue]一対多の関係にある親子オブジェクトをネストさせた状態で取得する
階層分析法