C# Tips | コレクション・LINQ:平均算出

C# C#
スポンサーリンク

はじめに:「平均算出」は“感覚をつかみやすい数字に整える技”

合計は「全体の大きさ」を教えてくれますが、
平均は「1件あたりどれくらいか?」という“感覚”を教えてくれます。

平均単価
平均購入金額
平均対応時間

こういう数字は、業務の“重さ”や“効率”を測るうえで、とても重要です。
C# / LINQ では Average を使うことで、for 文を書かずに平均を出せます。

ここから、初心者向けに

  • Average の基本
  • オブジェクトのプロパティ平均
  • null や 0 件との付き合い方
  • GroupBy と組み合わせた「○○ごとの平均」

を順番にかみ砕いて説明していきます。


基本:数値コレクションに対する Average

int の平均を出す

まずは一番シンプルな例です。

using System;
using System.Collections.Generic;
using System.Linq;

var numbers = new List<int> { 1, 2, 3, 4, 5 };

double avg = numbers.Average();

Console.WriteLine(avg); // 3
C#

Average() は、「合計 ÷ 件数」を内部でやってくれるメソッドです。
戻り値が double になっていることに注目してください。

ここでの重要ポイントは、「整数のリストでも、平均は小数(double)で返ってくる」ということです。
(1 + 2 + 3 + 4 + 5) / 5 = 3 なので今回は整数ですが、
{ 1, 2 } なら平均は 1.5 になります。

var nums = new List<int> { 1, 2 };

double avg = nums.Average(); // 1.5
C#

double や decimal の平均

doubledecimal のリストに対しても、同じように使えます。

var values = new List<double> { 1.2, 3.4, 5.6 };

double avg = values.Average();
C#

decimal の場合は、戻り値も decimal になります。

var prices = new List<decimal> { 100.5m, 200.0m, 300.5m };

decimal avgPrice = prices.Average();
C#

金額など、誤差を抑えたい場合は decimal を使うのが定番です。


オブジェクトのプロパティ平均:Average(selector)

売上明細の「平均単価」を出す

業務では、単純な数値のリストよりも、
「オブジェクトの中の特定のプロパティの平均」を出したいことが多いです。

売上明細クラスを用意します。

public class Sale
{
    public string Item { get; set; } = "";
    public int Quantity { get; set; }
    public int UnitPrice { get; set; } // 単価
}
C#

データを作ります。

var sales = new List<Sale>
{
    new Sale { Item = "A", Quantity = 2, UnitPrice = 1000 },
    new Sale { Item = "B", Quantity = 1, UnitPrice = 1500 },
    new Sale { Item = "C", Quantity = 3, UnitPrice = 2000 },
};
C#

平均単価を出します。

double avgUnitPrice = sales.Average(s => s.UnitPrice);

Console.WriteLine(avgUnitPrice); // 1500
C#

ここでの重要ポイントは、「Average(s => s.UnitPrice) の中で“何を平均したいか”を指定している」ことです。
Average() は「そのまま平均」、
Average(x => x.プロパティ) は「プロパティを取り出してから平均」というイメージです。

計算式をその場で書いて平均する

例えば、「1件あたりの金額(数量 × 単価)の平均」を出したい場合は、こう書けます。

double avgAmountPerSale = sales.Average(s => s.Quantity * s.UnitPrice);
C#

「各行の金額」をその場で計算し、それらの平均を取っています。


0 件・null との付き合い方

空コレクションに対する Average は例外になる

SumCount と違って、Average は「空のコレクション」に対して呼ぶと例外になります。

var empty = new List<int>();

double avg = empty.Average(); // InvalidOperationException
C#

なぜかというと、「合計 ÷ 件数」で件数が 0 だと、数学的に定義できないからです。
業務的にも、「0 件の平均」は意味が曖昧ですよね。

ここでの重要ポイントは、「平均を取る前に“件数が 0 かどうか”を確認する」ことです。

double? avg = null;

if (numbers.Any())
{
    avg = numbers.Average();
}
C#

「データがあるときだけ平均を計算し、ないときは null にしておく」という設計がよく使われます。

null かもしれないコレクション

コレクション自体が null のこともあります。

List<int>? numbers = GetNumbersOrNull();
C#

この場合も、いきなり numbers.Average() と書くと例外になります。

double? avg = null;

if (numbers != null && numbers.Any())
{
    avg = numbers.Average();
}
C#

あるいは、拡張メソッドとして「安全な平均」を用意してもよいです。

public static class AverageExtensions
{
    public static double? SafeAverage(this IEnumerable<int>? source)
    {
        if (source == null) return null;

        var list = source as ICollection<int> ?? source.ToList();
        return list.Count == 0 ? (double?)null : list.Average();
    }
}
C#

使い方はこうです。

double? avg = numbers.SafeAverage();
C#

ここでの重要ポイントは、「平均は“0 件のときどうするか”を必ず設計に含める」ことです。
0 件を 0 とみなすのか、null とみなすのかは、業務ルール次第です。

null 許容型(int? など)の平均

int? の平均も取れます。

public class WorkLog
{
    public string User { get; set; } = "";
    public int? Hours { get; set; } // null のこともある
}
C#
var logs = new List<WorkLog>
{
    new WorkLog { User = "A", Hours = 5 },
    new WorkLog { User = "B", Hours = null },
    new WorkLog { User = "C", Hours = 3 },
};

double? avgHours = logs.Average(x => x.Hours);

Console.WriteLine(avgHours); // 4
C#

null は無視され、5 と 3 の平均で 4 になります。
全件 null の場合は、結果も null です。

ここでの重要ポイントは、「int? の Average は戻り値も double? になる」ことです。
「平均が存在しない」状態を null で表現できます。


GroupBy と組み合わせて「○○ごとの平均」を出す

顧客ごとの平均購入金額

グルーピングと平均は、業務集計でよく組み合わされます。

public class Order
{
    public string Customer { get; set; } = "";
    public int Amount { get; set; }
}
C#
var orders = new List<Order>
{
    new Order { Customer = "A", Amount = 1000 },
    new Order { Customer = "A", Amount = 3000 },
    new Order { Customer = "B", Amount = 2000 },
};
C#

顧客ごとの平均購入金額を出します。

var avgByCustomer = orders
    .GroupBy(o => o.Customer)
    .Select(g => new
    {
        Customer = g.Key,
        Average  = g.Average(x => x.Amount)
    });

foreach (var x in avgByCustomer)
{
    Console.WriteLine($"{x.Customer}: 平均 {x.Average} 円");
}
C#

ここでの重要ポイントは、「g.Average(...) は“そのグループの中だけ”の平均を計算している」ことです。
GroupBy で「かたまり」を作り、その中で Average を使う、という流れです。

日付ごとの平均金額

日付単位の平均もよくあります。

public class SaleWithDate
{
    public DateTime Date { get; set; }
    public int Amount { get; set; }
}
C#
var dailyAvg = salesWithDate
    .GroupBy(s => s.Date.Date)
    .Select(g => new
    {
        Date    = g.Key,
        Average = g.Average(x => x.Amount)
    })
    .OrderBy(x => x.Date);
C#

「日付ごとの平均金額」を出し、日付順に並べています。


実務で意識してほしいこと

「何の平均か」が分かる名前にする

avg だけだと、「何の平均?」が分かりません。

avgAmount, avgUnitPrice, avgHours, avgPerCustomer のように、
「何を平均した結果なのか」が分かる名前をつけてください。

double avgUnitPrice   = sales.Average(s => s.UnitPrice);
double avgAmountPerOrder = orders.Average(o => o.Amount);
C#

これだけで、コードの意図がかなり読みやすくなります。

平均は「外れ値」に敏感だと意識する

平均は、極端に大きい値・小さい値に引っ張られます。

例えば、
1, 2, 3, 1000 の平均は 251.5 ですが、
「ほとんどは 1〜3 なのに、平均だけ見ると 251.5」という、
直感とズレた数字になります。

実務では、

  • 平均だけでなく、件数や合計も一緒に見る
  • 場合によっては中央値(Median)も検討する

といった視点も大事になります(中央値は LINQ に標準ではないので、自作が必要です)。


まとめ:「平均算出ユーティリティ」は“業務の感覚を数字にするレンズ”

平均算出の本質は、
「バラバラな明細を、“1件あたりどれくらいか”という感覚的な数字に変換する」ことです。

押さえておきたいポイントは、

数値コレクションは Average()、オブジェクトは Average(x => x.プロパティ)
空コレクションに対する Average は例外になるので、Any() などで 0 件をチェックする
null 許容型の平均は null を無視し、戻り値も null になり得る
「○○ごとの平均」は GroupByg.Average(...) の流れで書く
変数名で「何の平均か」をはっきりさせる

このあたりが自然に書けるようになると、
「とりあえず合計だけ出している」状態から一歩進んで、
“業務の状態をより立体的に理解できる平均値”を、C# と LINQ でスマートに扱えるようになります。

タイトルとURLをコピーしました