LoginSignup
5
5

【個人開発】初心者🔰が診断ロジックを1から実装した話📕

Last updated at Posted at 2024-03-15

はじめに

先日、ユーザーの疲労タイプを診断して、診断結果にて漫画+アロマをお勧めするWebアプリを作成しました。
文字あり.jpg

このサービスの肝になる診断ロジックの構築が大変だったので、記録しておこうと思います。
今回の診断ロジックは、回答によってポイントを加算し集計する方法で実装を行いました。

ER図

漫画の処方箋-ER図.jpeg

1. 診断ロジックを考える(実装前)

1 はじめに決めたこと

  • 質問の数(10個)と内容
  • Yes・Noの回答形式
  • 疲れタイプ(診断結果)は6タイプ
    • 人間関係
    • 不眠
    • イライラ
    • エネルギー不足
    • ホルモンバランスの乱れ
    • 育児疲れ

2 疲れタイプに加算するポイント決める

下記の図では、問8に対して、

  • Yesと回答した場合:「エネルギー不足に3点」「ホルモンバランスの乱れに2点」「不眠に1点」が加算される
  • Noと答えた場合:「すべてのタイプは加算なし(0点)」
    Image from Gyazo

3 集計する

各質問の回答から加算されたポイントを集計します。
最終的に、ポイントが一番高かった疲れタイプを診断結果とします。
最終ポイントが同点のタイプが複数あった際は、その中からランダムで結果を決めます。
Image from Gyazo

4 条件を追加

問10で子供がいないと答えた人は、「育児疲れ」タイプを除外して集計する。
Image from Gyazo

ここまで考えるのも結構大変でした🥲

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アプリで是非遊んでみてください📚🙌

Image from Gyazo

最後に

実装前の診断ロジックを固めるまでが、一番大変でした。
つくりたいものを形にするのは難しいですね😅

最後はイメージを形にすることができ、満足しています✨

5
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5