はじめに
先日、ユーザーの疲労タイプを診断して、診断結果にて漫画+アロマをお勧めするWebアプリを作成しました。
このサービスの肝になる診断ロジックの構築が大変だったので、記録しておこうと思います。
今回の診断ロジックは、回答によってポイントを加算し集計する方法で実装を行いました。
ER図
1. 診断ロジックを考える(実装前)
1 はじめに決めたこと
- 質問の数(10個)と内容
- Yes・Noの回答形式
- 疲れタイプ(診断結果)は6タイプ
- 人間関係
- 不眠
- イライラ
- エネルギー不足
- ホルモンバランスの乱れ
- 育児疲れ
2 疲れタイプに加算するポイント決める
下記の図では、問8に対して、
3 集計する
各質問の回答から加算されたポイントを集計します。
最終的に、ポイントが一番高かった疲れタイプを診断結果とします。
最終ポイントが同点のタイプが複数あった際は、その中からランダムで結果を決めます。
4 条件を追加
問10で子供がいないと答えた人は、「育児疲れ」タイプを除外して集計する。
ここまで考えるのも結構大変でした🥲
2. 診断ロジックを実装
ユーザーが各質問に回答し結果ボタンをクリックする👆
全体コード
def create
answers = answer_params
# ポイントを集計するハッシュを初期化
@faigue_points = Hash.new(0)
answers.each do |index, answer|
if answer != "true" && answer != "false"
flash.now[:danger] = "回答が不十分のエラーです。"
return redirect_to diagnostics_path
end
# ユーザーの回答を取得
user_answers = UserAnswer.where(question_id: index.to_i, answer: answer) # 条件に一致したレコードをとってくる
user_answers.each do |user_answer|
# ポイントを加算
@faigue_points[user_answer.fatigue_type_id] += user_answer.point
end
end
# 質問IDが10で回答がfalseの場合の条件分岐(子供がいないと回答した時に、育児疲れタイプにならないようにする。)
if answers[10] == 'false'
@faigue_points = @faigue_points.keys.reject { |id| id == FatigueType.find_by(title: "育児疲れ").id }
end
# 同点の場合、ランダムで疲労タイプを選択
max_fatigue_ids = @faigue_points.sort_by { |_id, points| points }.reverse.to_h.keys
if max_fatigue_ids.size > 1 && @faigue_points[max_fatigue_ids[0]] == @faigue_points[max_fatigue_ids[1]]
max_fatigue_ids = max_fatigue_ids.select { |id| @faigue_points[id] == @faigue_points[max_fatigue_ids[0]] }
max_fatigue_id = max_fatigue_ids.sample
else
max_fatigue_id = max_fatigue_ids[0]
end
@your_fatigue = FatigueType.find(max_fatigue_id)
if current_user
current_user.fatigue_type_id = @your_fatigue.id
current_user.save!
else
session[:fatigue_type_id] = @your_fatigue.id
end
redirect_to result_diagnostics_path(your_fatigue_id: max_fatigue_id)
end
詳細説明
1
def create
# ①ユーザーから送られてきた回答をanswersに格納
answers = answer_params
# ②ポイントを集計するための箱を作成(ハッシュを初期化)
@faigue_points = Hash.new(0)
# ③回答をeachで回して、質問ID(index)と回答Yes/No(answer)に入れる
answers.each do |index, answer|
#違うパラメーターが送信された際の対策として、Yes,No以外受けつけない条件を書きました
if answer != "true" && answer != "false"
flash.now[:danger] = "回答が不十分のエラーです。"
return redirect_to diagnostics_path
end
# ④UserAnswerモデルから質問IDと回答に一致するレコードを取得してuser_answersに格納
user_answers = UserAnswer.where(question_id: index.to_i, answer: answer)
# ⑤user_answersをeachで回してuser_answerに入れる
user_answers.each do |user_answer|
# ⑥②で作成した箱に、疲労タイプごとにポイントを集計し加算
@faigue_points[user_answer.fatigue_type_id] += user_answer.point
end
end
private
# ユーザーから送られてきた回答
def answer_params
params.require(:answers)
end
⑥を紐解く。
例えば、@faigue_pointsハッシュが以下のようになっていたとします。
@faigue_points = {
1 => 10, # 疲労タイプID 1 のポイントは10
2 => 5 # 疲労タイプID 2 のポイントは5
}
そして、user_answerが疲労タイプID 1 の回答で、ポイントが3だった場合、次のようになります。
@faigue_points[1] += 3
その結果、@faigue_points ハッシュは次のように更新されます。
@faigue_points = {
1 => 13, # 疲労タイプID 1 のポイントは10 + 3 = 13
2 => 5 # 疲労タイプID 2 のポイントは変化なし
}
2
質問IDが10で回答がfalseの場合の条件分岐
(子供がいないと回答した時に、育児疲れタイプを集計から除外する)
# rejectメソッドを使用してブロック内の条件を満たすIDを取り除き、@faigue_pointsに格納
if answers[10] == 'false'
@faigue_points = @faigue_points.keys.reject { |id| id == FatigueType.find_by(title: "育児疲れ").id }
end
# ⑦加算ポイントを降順にsortし、ポイントが最大の疲労タイプIDを取得する。
max_fatigue_ids = @faigue_points.sort_by { |_id, points| points }.reverse.to_h.keys
# ⑧同点の場合は、ランダムで疲労タイプを選択
if max_fatigue_ids.size > 1 && @faigue_points[max_fatigue_ids[0]] == @faigue_points[max_fatigue_ids[1]]
max_fatigue_ids = max_fatigue_ids.select { |id| @faigue_points[id] == @faigue_points[max_fatigue_ids[0]] }
max_fatigue_id = max_fatigue_ids.sample
else
# ⑨一番ポイントが高い疲労タイプのIDを取得しmax_fatigue_idに格納
max_fatigue_id = max_fatigue_ids[0]
end
⑧を紐解く
疲労タイプが複数あった場合と
if max_fatigue_ids.size > 1 &&
1番目の疲労タイプと2番目の疲労タイプが同じポイントを持っている場合に
@faigue_points[max_fatigue_ids[0]] == @faigue_points[max_fatigue_ids[1]]
max_fatigue_idsからポイントが最大の疲労タイプと同じポイントを持つ疲労タイプIDだけを選択
max_fatigue_ids = max_fatigue_ids.select { |id| @faigue_points[id] == @faigue_points[max_fatigue_ids[0]] }
その中からランダムに1つを選んで max_fatigue_id に代入します。
max_fatigue_id = max_fatigue_ids.sample
3
最後は診断結果を引き継げるように実装
# FatigueTypeモデルから一番ポイントが大きい疲労タイプを取得し、your_fatigueに格納
@your_fatigue = FatigueType.find(max_fatigue_id)
# 現ユーザーが診断をやり直した場合、更新
if current_user
current_user.fatigue_type_id = @your_fatigue.id
current_user.save!
else
# 現ユーザーじゃなかった場合はセッションで疲れタイプを保存
session[:fatigue_type_id] = @your_fatigue.id
end
redirect_to result_diagnostics_path(your_fatigue_id: max_fatigue_id)
end
大まかな実装は以上となります。
診断イメージが掴めない場合は、
ログインなしで診断ができるので、Webアプリで是非遊んでみてください📚🙌
最後に
実装前の診断ロジックを固めるまでが、一番大変でした。
つくりたいものを形にするのは難しいですね😅
最後はイメージを形にすることができ、満足しています✨