0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Analyticsに「最高・最低・平均付近コース」を追加するまでの設計と思考整理

0
Last updated at Posted at 2026-02-20

はじめに

オリジナルプロダクトLogi-Balance の Analytics 機能に
「最高負荷」「最低負荷」「平均値付近」の3コースをカード表示する機能を追加しました。

実装自体はそれほど難しくありませんでしたが、
設計の考え方・責務の分離・平均の意味の違い でかなり思考が整理されました。

今回はその学びをまとめます。


🎯 やりたかったこと

単日・週次・月次それぞれで

  • 最高負荷コース
  • 最低負荷コース
  • 平均値付近コース

を表示させたい。


まず考えたこと

単日の場合、データはこうなっています。

{
  "Aコース" => 370,
  "Bコース" => 420,
  "Cコース" => 310
}

このハッシュから

  • 最大値
  • 最小値
  • 平均に最も近い値

を取り出せばいい。


Serviceオブジェクトで分離する

Controllerにロジックを書くと肥大化するため、
AnalyticsHighlightService を作成しました。

class AnalyticsHighlightService
  def initialize(scores)
    @scores = scores
  end

  def call
    return { highest: nil, lowest: nil, closest: nil, average: 0 } if @scores.empty?

    values = @scores.values.map(&:to_f)
    avg = values.sum / values.size

    max_name, max_score = @scores.max_by { |_, score| score }
    min_name, min_score = @scores.min_by { |_, score| score }
    closest_name, closest_score = @scores.min_by { |_, score| (score - avg).abs }

    {
      highest: {
        name: max_name,
        score: max_score,
        diff: max_score - avg
      },
      lowest: {
        name: min_name,
        score: min_score,
        diff: min_score - avg
      },
      closest: {
        name: closest_name,
        score: closest_score,
        diff: closest_score - avg
      },
      average: avg
    }
  end
end

💡 重要な設計ポイント

このServiceは

「ハッシュを受け取って比較する機械」

です。

単日専用ではありません。


🔁 単日・週次・月次の違い

単日

@daily_scores = {
  "Aコース" => 370,
  "Bコース" => 420
}

そのままServiceへ。


週次

週次では series という構造になっているため、

weekly_totals = series.each_with_object({}) do |s, h|
  name = s[:name]
  total = s[:data].values.sum
  h[name] = total
end

こうして

{
  "Aコース" => 週間合計,
  "Bコース" => 週間合計
}

というハッシュに変換してからServiceへ渡します。


月次も同じ

月次も合計を出してServiceへ渡すだけ。


🔥 大きな学び①

ロジックは同じでも「渡すデータ」が違う

Serviceの中身は単日と同じ。

違うのは、

  • 単日 → 1日分の合計
  • 週次 → 1週間分の合計
  • 月次 → 1ヶ月分の合計

入力データの単位が違うだけ。

これが理解できたのは大きな収穫でした。


🔥 大きな学び②

平均には「意味の違い」がある

週次には実は2つの平均が存在します。

① グラフ用平均

@avg_value

1日 × コース数で割った平均。

② カード用平均

@highlights[:average]

1週間合計 ÷ コース数。

意味が違います。

最初ここで混乱しました。


🔥 大きな学び③

ServiceはControllerを知らない

Serviceは

  • 単日か
  • 週次か
  • 月次か

を知りません。

ただ

{ コース名 => 数値 }

を受け取るだけ。

これは責務分離としておそらく重要な設計。


🎨 Viewでカード表示

<div class="highlight-cards">
  <% if @highlights.present? %>

    <div class="highlight-card highest">
      <p class="highlight-label">最高負荷</p>
      <p class="highlight-course"><%= @highlights[:highest][:name] %></p>
      <p class="highlight-score"><%= @highlights[:highest][:score].round %></p>
    </div>

  <% end %>
</div>

Controllerで

@highlights = AnalyticsHighlightService.new(weekly_totals).call

していればそのまま表示されます。


💭 混乱したポイント

  • 「単日と同じロジックじゃない?」
  • 「週次専用の計算が必要?」
  • 「平均の意味はどれ?」

→ 結論:ロジックは同じでいい。


🎯 今回の設計のまとめ

  • Controllerはデータ整形
  • Serviceは純粋な計算
  • Viewは表示のみ

この分離ができたのが一番の成長でした。


Conclusion

今回の実装で一番の学びは

「処理を書くこと」より
「処理の意味を整理すること」の方が重要

ということ。

特に「平均」の意味が複数あることに気づけたのは大きな収穫でした。

今後は、

  • 差分を%表示する
  • 偏差が一定以上なら警告色にする
  • ランキング形式にする

など発展させていきたいと思います。


※本記事は学習メモとして作成。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?