C# Tips | コレクション・LINQ:重複カウント

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

はじめに:「重複カウント」は“どれがどれだけ被っているか”を一発で見抜く技

業務でデータを扱っていると、こういうことを知りたくなる場面がよく出てきます。

どの商品が何回注文されているか
どのユーザーが何回ログインしているか
どのエラーコードが何回発生しているか

つまり、「同じ値がいくつあるか=重複カウント」です。
C# の LINQ を使うと、この「重複カウント」をとてもシンプルに書けます。

ここでは、初心者向けに

重複カウントの基本パターン(GroupBy + Count)
「重複しているものだけ」を取り出す方法
オブジェクトの特定プロパティで重複カウントする方法
業務での具体例と、ユーティリティ化のアイデア

を、例題付きでかみ砕いて説明します。


基本形:GroupBy + Count で「値ごとの件数」を出す

まずは単純な文字列の重複カウント

using System;
using System.Linq;

var names = new[]
{
    "Alice",
    "Bob",
    "Alice",
    "Charlie",
    "Bob",
    "Alice"
};

var grouped = names
    .GroupBy(x => x)
    .Select(g => new
    {
        Name = g.Key,
        Count = g.Count()
    });

foreach (var x in grouped)
{
    Console.WriteLine($"{x.Name}: {x.Count}");
}
C#

出力はこうなります。

Alice: 3
Bob: 2
Charlie: 1

ここでの重要ポイントは、「GroupBy(x => x) で“同じ値ごとのグループ”を作り、g.Count() で“その値が何回出てきたか”を数えている」ということです。
GroupByKey が「グループの代表値」、Count() が「その値の出現回数」です。


「重複しているものだけ」を取り出す

Count > 1 で“1 回以上ではなく、2 回以上”に絞る

「全部の件数」ではなく、「重複しているものだけ知りたい」ことも多いです。
その場合は、Count > 1 でフィルタします。

var duplicates = names
    .GroupBy(x => x)
    .Select(g => new
    {
        Name = g.Key,
        Count = g.Count()
    })
    .Where(x => x.Count > 1);

foreach (var x in duplicates)
{
    Console.WriteLine($"{x.Name}: {x.Count}");
}
C#

出力はこうなります。

Alice: 3
Bob: 2

Charlie は 1 回しか出ていないので除外されています。

ここでの重要ポイントは、「“重複”とは“2 回以上出ているもの”だと定義して、Count > 1 で表現する」ということです。
この条件を変えれば、「3 回以上出ているもの」なども簡単に書けます。


オブジェクトの特定プロパティで重複カウントする

例:ユーザーのメールアドレス重複チェック

public class User
{
    public int Id { get; set; }
    public string Email { get; set; } = "";
}

var users = new[]
{
    new User { Id = 1, Email = "a@example.com" },
    new User { Id = 2, Email = "b@example.com" },
    new User { Id = 3, Email = "a@example.com" },
    new User { Id = 4, Email = "c@example.com" },
    new User { Id = 5, Email = "b@example.com" }
};
C#

「同じメールアドレスが何人に使われているか」を知りたいときは、Email をキーにして GroupBy します。

var emailCounts = users
    .GroupBy(u => u.Email)
    .Select(g => new
    {
        Email = g.Key,
        Count = g.Count()
    });

foreach (var x in emailCounts)
{
    Console.WriteLine($"{x.Email}: {x.Count}");
}
C#

出力はこうなります。

a@example.com: 2
b@example.com: 2
c@example.com: 1

「重複しているメールアドレスだけ知りたい」なら、さきほどと同じように Count > 1 で絞ります。

var duplicatedEmails = emailCounts
    .Where(x => x.Count > 1);

foreach (var x in duplicatedEmails)
{
    Console.WriteLine($"Duplicated: {x.Email} ({x.Count})");
}
C#

ここでの重要ポイントは、「GroupBy(u => u.Email) の“キー”を変えるだけで、どの項目で重複カウントするかを自由に切り替えられる」ということです。
商品コード、顧客 ID、日付、ステータスなど、業務で意味のある項目をキーにできます。


「重複しているレコードそのもの」を取り出す

GroupBy の中身(グループ)をそのまま使う

「どのメールアドレスが何回出ているか」だけでなく、
「そのメールアドレスを持つユーザーの一覧」も欲しいことがあります。

var duplicatedGroups = users
    .GroupBy(u => u.Email)
    .Where(g => g.Count() > 1);

foreach (var g in duplicatedGroups)
{
    Console.WriteLine($"Email: {g.Key}");

    foreach (var u in g)
    {
        Console.WriteLine($"  Id={u.Id}, Email={u.Email}");
    }
}
C#

出力はこうなります。

Email: a@example.com
  Id=1, Email=a@example.com
  Id=3, Email=a@example.com
Email: b@example.com
  Id=2, Email=b@example.com
  Id=5, Email=b@example.com

ここでの重要ポイントは、「GroupBy の結果は“キー+そのキーに属する要素の列”なので、g の中身をそのまま列挙できる」ということです。
「重複しているグループごとに、その中身を全部見る」という処理が簡単に書けます。


実務での具体例 1:ログのエラーコード別発生回数

どのエラーがどれだけ出ているかを集計する

public class LogEntry
{
    public DateTime Time { get; set; }
    public string Level { get; set; } = "";
    public string ErrorCode { get; set; } = "";
}

var logs = new[]
{
    new LogEntry { Time = DateTime.Now, Level = "Error", ErrorCode = "E001" },
    new LogEntry { Time = DateTime.Now, Level = "Error", ErrorCode = "E002" },
    new LogEntry { Time = DateTime.Now, Level = "Error", ErrorCode = "E001" },
    new LogEntry { Time = DateTime.Now, Level = "Warn",  ErrorCode = "W001" },
    new LogEntry { Time = DateTime.Now, Level = "Error", ErrorCode = "E001" },
};
C#
var errorCounts = logs
    .Where(x => x.Level == "Error")
    .GroupBy(x => x.ErrorCode)
    .Select(g => new
    {
        ErrorCode = g.Key,
        Count = g.Count()
    })
    .OrderByDescending(x => x.Count);

foreach (var x in errorCounts)
{
    Console.WriteLine($"{x.ErrorCode}: {x.Count}");
}
C#

ここでの重要ポイントは、「Where で対象を絞ってから GroupBy する」という流れです。
「Error レベルだけ」「特定期間だけ」など、業務条件を先にかけてから重複カウントします。


実務での具体例 2:注文ごとの商品別数量集計

商品コードごとに「何回注文されたか」を数える

public class OrderLine
{
    public string ItemCode { get; set; } = "";
    public int Quantity { get; set; }
}

var lines = new[]
{
    new OrderLine { ItemCode = "A", Quantity = 1 },
    new OrderLine { ItemCode = "B", Quantity = 2 },
    new OrderLine { ItemCode = "A", Quantity = 3 },
    new OrderLine { ItemCode = "C", Quantity = 1 },
    new OrderLine { ItemCode = "B", Quantity = 1 },
};
C#

「何回注文に登場したか(行数ベース)」を数えるなら、単純に Count() です。

var itemCounts = lines
    .GroupBy(x => x.ItemCode)
    .Select(g => new
    {
        ItemCode = g.Key,
        Count = g.Count()
    });
C#

「合計数量」を知りたいなら、Count() ではなく Sum(x => x.Quantity) を使います。

var itemQuantities = lines
    .GroupBy(x => x.ItemCode)
    .Select(g => new
    {
        ItemCode = g.Key,
        TotalQuantity = g.Sum(x => x.Quantity)
    });
C#

ここでの重要ポイントは、「GroupBy のあとに何を集計するかは自由」ということです。
Count() は「行数」、Sum は「数量」、MaxMin も同じパターンで書けます。


重複カウントをユーティリティ化する

「CountBy」的な拡張メソッドを作る

毎回 GroupBy(...).Select(...) と書くのが長いと感じたら、
「重複カウント専用」の拡張メソッドを 1 本用意しておくと便利です。

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

public static class CountByExtensions
{
    public static IEnumerable<(TKey Key, int Count)> CountBy<TSource, TKey>(
        this IEnumerable<TSource> source,
        System.Func<TSource, TKey> keySelector)
    {
        return source
            .GroupBy(keySelector)
            .Select(g => (Key: g.Key, Count: g.Count()));
    }
}
C#

使い方はこうです。

var counts = names.CountBy(x => x);

foreach (var x in counts)
{
    Console.WriteLine($"{x.Key}: {x.Count}");
}
C#

ユーザーのメールアドレスならこうです。

var emailCounts = users.CountBy(u => u.Email);
C#

ここでの重要ポイントは、「CountBy のような名前にすることで、“ここで重複カウントしている”ことが一目で分かる」ということです。
GroupBy + Count のパターンを、チーム共通のユーティリティとしてしまうイメージです。


まとめ:「重複カウントユーティリティ」は“偏りや異常を一瞬であぶり出す道具”

重複カウントの本質は、

同じ値がどれだけ出ているかを数えることで、
データの偏りや異常、重複登録などを一瞬で見抜く

ことです。

押さえておきたいポイントを整理すると、

基本形は GroupBy(key).Select(g => new { Key = g.Key, Count = g.Count() })
「重複だけ」は Count > 1 で絞る
オブジェクトの特定プロパティで重複カウントするには、GroupBy(u => u.Email) のようにキーを変える
グループの中身をそのまま列挙すれば、「重複しているレコードそのもの」も取れる
CountBy のような拡張メソッドにしておくと、業務コードがかなり読みやすくなる

ここまで腹落ちしていれば、
「なんとなく for で辞書を回して数える」段階から卒業して、
“LINQ でサッと重複カウントして、データの状態を一瞬で把握する”ことができるようになります。

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