Java Tips | 実務ユーティリティ:データ集計

Java Java
スポンサーリンク
スポンサーリンク

なぜ「データ集計ユーティリティ」が業務で必須になるのか

業務システムでは、売上データ、アクセスログ、ユーザー行動履歴、在庫情報、エラーログなど、さまざまな「生データ」が日々蓄積されます。しかし、そのままではただの数字やレコードの羅列でしかなく、意思決定や改善に使える「情報」にはなりません。

そこで必要になるのが データ集計(aggregation) です。

  • 日別・月別・年別の売上集計
  • ユーザーごとのアクセス回数集計
  • 商品ごとの購入回数・合計金額集計
  • エラーコードごとの発生回数集計

こうした集計処理を、毎回バラバラに書いていると、ロジックが重複し、バグや抜け漏れが起きやすくなります。そこで「業務・実務で使える Java のデータ集計ユーティリティ」として、再利用可能な形にまとめておくことが重要になります。

ここから、プログラミング初心者の方にも分かりやすいように、ステップバイステップでデータ集計の考え方とユーティリティコードを解説していきます。

データ集計の基本発想を整理する

「生データ」から「集計結果」への変換を意識します

まず、データ集計の基本構造をシンプルに整理します。

  1. 入力データ(生データ)
    • 例:List<SaleRecord>(売上レコードの一覧)
    • 例:List<AccessLog>(アクセスログの一覧)
  2. 集計キー(グルーピングの軸)
    • 例:日付、ユーザーID、商品ID、エラーコードなど
  3. 集計値(集計したい内容)
    • 例:件数、合計金額、平均値、最大値、最小値など
  4. 出力データ(集計結果)
    • 例:Map<LocalDate, Long>(日付ごとの件数)
    • 例:Map<String, BigDecimal>(商品IDごとの売上合計)

この「入力 → グルーピング → 集計 → 出力」という流れを、ユーティリティとして汎用的に扱えるようにしていきます。

ステップ1 シンプルな「件数集計」ユーティリティから始める

例:ユーザーごとのアクセス回数を集計する

まずは、最も基本的な「件数集計」をユーティリティとして実装してみます。

想定するデータモデルは次のようなものです。

public class AccessLog {
    private final String userId;
    private final String path;

    public AccessLog(String userId, String path) {
        this.userId = userId;
        this.path = path;
    }

    public String getUserId() {
        return userId;
    }

    public String getPath() {
        return path;
    }
}
Java

これを使って、「ユーザーごとのアクセス回数」を集計するユーティリティを作ります。

import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class AggregationUtils {

    /**
     * ユーザーIDごとのアクセス回数を集計します。
     */
    public static Map<String, Long> countByUser(List<AccessLog> logs) {
        Map<String, Long> result = new HashMap<>();

        if (logs == null || logs.isEmpty()) {
            return result;
        }

        for (AccessLog log : logs) {
            String userId = log.getUserId();
            // すでに存在する件数を取り出し、1加算して戻す
            long current = result.getOrDefault(userId, 0L);
            result.put(userId, current + 1);
        }

        return result;
    }
}
Java

使い方の例です。

import java.util.List;
import java.util.Map;

public class AggregationExample {

    public static void main(String[] args) {
        List<AccessLog> logs = List.of(
                new AccessLog("user1", "/home"),
                new AccessLog("user1", "/products"),
                new AccessLog("user2", "/home"),
                new AccessLog("user1", "/cart")
        );

        Map<String, Long> countByUser = AggregationUtils.countByUser(logs);

        System.out.println("ユーザーごとのアクセス回数: " + countByUser);
        // 出力: {user1=3, user2=1}
    }
}
Java

ここで重要なのは、「Mapを使って、キーごとの集計値を蓄積していく」という基本パターンを体で覚えることです。

  • Map<キー, 集計値> という形が、集計結果の基本形になります。
  • getOrDefault を使うことで、「初期値0から加算する」処理をシンプルに書けます。

ステップ2 「汎用的なグルーピング+件数集計」ユーティリティに進化させる

任意のキーでグルーピングできるようにします

先ほどの countByUser は「ユーザーID固定」でしたが、実務では「日付ごと」「パスごと」「商品IDごと」など、さまざまなキーで集計したくなります。

そこで、「任意のキーを取り出す関数」を受け取る汎用ユーティリティに進化させます。

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

public class AggregationUtils {

    /**
     * 任意のキーで件数を集計する汎用ユーティリティです。
     *
     * @param items 集計対象のリスト
     * @param keyExtractor 集計キーを取り出す関数(例: AccessLog::getUserId)
     * @param <T> 入力要素の型
     * @param <K> 集計キーの型
     * @return キーごとの件数を表す Map
     */
    public static <T, K> Map<K, Long> countByKey(List<T> items, Function<T, K> keyExtractor) {
        Map<K, Long> result = new HashMap<>();

        if (items == null || items.isEmpty()) {
            return result;
        }

        for (T item : items) {
            K key = keyExtractor.apply(item);
            long current = result.getOrDefault(key, 0L);
            result.put(key, current + 1);
        }

        return result;
    }
}
Java

使い方の例です。

import java.util.List;
import java.util.Map;

public class AggregationGenericExample {

    public static void main(String[] args) {
        List<AccessLog> logs = List.of(
                new AccessLog("user1", "/home"),
                new AccessLog("user1", "/products"),
                new AccessLog("user2", "/home"),
                new AccessLog("user1", "/cart")
        );

        // ユーザーIDごとの件数
        Map<String, Long> countByUser =
                AggregationUtils.countByKey(logs, AccessLog::getUserId);

        // パスごとの件数
        Map<String, Long> countByPath =
                AggregationUtils.countByKey(logs, AccessLog::getPath);

        System.out.println("ユーザーごとのアクセス回数: " + countByUser);
        System.out.println("パスごとのアクセス回数: " + countByPath);
    }
}
Java

ここで深掘りしたいポイントは、「Function<T, K> を使うことで、集計ロジックを汎用化できる」ということです。

  • 集計の「軸」(キー)だけを差し替えられるようになるため、同じユーティリティをさまざまな場面で再利用できます。
  • これが「業務・実務で使えるユーティリティ」に育てるうえで非常に重要な設計です。

ステップ3 「合計値集計」ユーティリティを作る

例:商品ごとの売上合計を集計する

次に、「件数」ではなく「合計値」を集計するユーティリティを作ります。

まず、売上レコードのモデルを定義します。

import java.math.BigDecimal;

public class SaleRecord {
    private final String productId;
    private final BigDecimal amount;

    public SaleRecord(String productId, BigDecimal amount) {
        this.productId = productId;
        this.amount = amount;
    }

    public String getProductId() {
        return productId;
    }

    public BigDecimal getAmount() {
        return amount;
    }
}
Java

これを使って、「商品IDごとの売上合計」を集計するユーティリティを作ります。

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

public class AggregationUtils {

    /**
     * 任意のキーで BigDecimal の合計値を集計するユーティリティです。
     *
     * @param items 集計対象のリスト
     * @param keyExtractor 集計キーを取り出す関数(例: SaleRecord::getProductId)
     * @param valueExtractor 集計値を取り出す関数(例: SaleRecord::getAmount)
     */
    public static <T, K> Map<K, BigDecimal> sumByKey(
            List<T> items,
            Function<T, K> keyExtractor,
            Function<T, BigDecimal> valueExtractor
    ) {
        Map<K, BigDecimal> result = new HashMap<>();

        if (items == null || items.isEmpty()) {
            return result;
        }

        for (T item : items) {
            K key = keyExtractor.apply(item);
            BigDecimal value = valueExtractor.apply(item);

            BigDecimal current = result.getOrDefault(key, BigDecimal.ZERO);
            result.put(key, current.add(value));
        }

        return result;
    }
}
Java

使い方の例です。

import java.math.BigDecimal;
import java.util.List;
import java.util.Map;

public class AggregationSumExample {

    public static void main(String[] args) {
        List<SaleRecord> sales = List.of(
                new SaleRecord("P001", new BigDecimal("1000")),
                new SaleRecord("P001", new BigDecimal("2000")),
                new SaleRecord("P002", new BigDecimal("500")),
                new SaleRecord("P001", new BigDecimal("300"))
        );

        Map<String, BigDecimal> sumByProduct =
                AggregationUtils.sumByKey(sales, SaleRecord::getProductId, SaleRecord::getAmount);

        System.out.println("商品ごとの売上合計: " + sumByProduct);
        // 出力: {P001=3300, P002=500}
    }
}
Java

ここで重要なのは、「集計キーと集計値の両方を関数として受け取ることで、どんなデータ構造にも対応できる汎用ユーティリティになる」ということです。

  • キー:商品ID、ユーザーID、日付など
  • 値:金額、ポイント、数量など

を自由に組み合わせて集計できるようになります。

ステップ4 Streamを使った「グルーピング+集計」の書き方を身につける

Collectors.groupingBy と Collectors.summingInt / summingLong / reducing が強力です

Java 8以降では、Stream と Collectors を使うことで、グルーピング+集計を非常にコンパクトに書けます。

まず、件数集計の例です。

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class AggregationStreamUtils {

    /**
     * ユーザーIDごとのアクセス回数を Stream で集計します。
     */
    public static Map<String, Long> countByUser(List<AccessLog> logs) {
        if (logs == null || logs.isEmpty()) {
            return Map.of();
        }

        return logs.stream()
                   .collect(Collectors.groupingBy(
                           AccessLog::getUserId,
                           Collectors.counting()
                   ));
    }
}
Java

次に、売上合計の例です。

import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class AggregationStreamUtils {

    /**
     * 商品IDごとの売上合計を Stream で集計します。
     */
    public static Map<String, BigDecimal> sumByProduct(List<SaleRecord> sales) {
        if (sales == null || sales.isEmpty()) {
            return Map.of();
        }

        return sales.stream()
                    .collect(Collectors.groupingBy(
                            SaleRecord::getProductId,
                            Collectors.reducing(
                                    BigDecimal.ZERO,
                                    SaleRecord::getAmount,
                                    BigDecimal::add
                            )
                    ));
    }
}
Java

ここで深掘りしたいポイントは、groupingBy が「グルーピングの軸」、counting や reducing が「集計の方法」を表しているということです。

  • groupingBy(キー関数, 集計方法) という形で、「どの軸でグルーピングし、どう集計するか」を宣言的に書けます。
  • reducing(初期値, 値関数, 集計関数) という形で、合計・最大・最小などの集計を柔軟に表現できます。

ステップ5 実務でよくあるデータ集計ユースケース

1. 日別売上集計

売上レコードに日付が含まれている場合、「日別売上合計」を集計するのは非常に一般的です。

import java.math.BigDecimal;
import java.time.LocalDate;

public class DatedSaleRecord {
    private final LocalDate date;
    private final BigDecimal amount;

    public DatedSaleRecord(LocalDate date, BigDecimal amount) {
        this.date = date;
        this.amount = amount;
    }

    public LocalDate getDate() {
        return date;
    }

    public BigDecimal getAmount() {
        return amount;
    }
}
Java

これを使って、日別売上合計を集計します。

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class DailySalesAggregation {

    public static Map<LocalDate, BigDecimal> sumByDate(List<DatedSaleRecord> sales) {
        if (sales == null || sales.isEmpty()) {
            return Map.of();
        }

        return sales.stream()
                    .collect(Collectors.groupingBy(
                            DatedSaleRecord::getDate,
                            Collectors.reducing(
                                    BigDecimal.ZERO,
                                    DatedSaleRecord::getAmount,
                                    BigDecimal::add
                            )
                    ));
    }
}
Java

2. エラーコードごとの発生回数集計

ログにエラーコードが含まれている場合、「エラーコードごとの発生回数」を集計することで、どのエラーが多いかを把握できます。

public class ErrorLog {
    private final String errorCode;

    public ErrorLog(String errorCode) {
        this.errorCode = errorCode;
    }

    public String getErrorCode() {
        return errorCode;
    }
}
Java
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class ErrorAggregation {

    public static Map<String, Long> countByErrorCode(List<ErrorLog> logs) {
        if (logs == null || logs.isEmpty()) {
            return Map.of();
        }

        return logs.stream()
                   .collect(Collectors.groupingBy(
                           ErrorLog::getErrorCode,
                           Collectors.counting()
                   ));
    }
}
Java

ステップ6 設計上の重要ポイントを深掘りする

1. 集計キーの設計

データ集計では、「何を軸に集計するか」が非常に重要です。

  • ユーザー単位で見るのか
  • 日付単位で見るのか
  • 商品単位で見るのか
  • エラーコード単位で見るのか

集計キーの設計は、そのまま「業務で何を知りたいか」に直結します。ユーティリティ側では、Function<T, K> を使って柔軟にキーを指定できるようにしておくと、さまざまな集計ニーズに対応できます。

2. 集計値の型と精度

金額や数量を扱う場合、型と精度が重要です。

  • 金額は BigDecimal を使う(double は誤差が出るため避ける)
  • 件数は long を使う(将来的な件数増加に備える)

ユーティリティ側で適切な型を採用しておくことで、後から「型が足りない」「精度が足りない」といった問題を避けられます。

3. nullや空リストの扱い

ユーティリティでは、null や空リストの扱いを統一しておくことが重要です。

  • null の入力は「空」として扱う
  • 戻り値の Map は常に非nullにする

これにより、呼び出し側は「nullチェック」を意識せずに済み、コードがシンプルになります。

セキュリティ・品質の観点から見たデータ集計

データ集計は、セキュリティや品質の観点からも重要な意味を持ちます。

  • 異常検知 エラーコードごとの発生回数や、ユーザーごとのアクセス回数を集計することで、異常なパターン(急増・偏り)を検知しやすくなります。
  • 監査・ログ分析 ログを集計することで、「どの操作がどれだけ行われているか」「どのユーザーがどれだけアクセスしているか」を把握でき、監査や不正検知に役立ちます。
  • 品質改善 エラーや失敗の集計結果をもとに、ボトルネックや問題箇所を特定し、改善につなげることができます。

まとめ データ集計ユーティリティで身につけてほしい感覚

データ集計ユーティリティは、業務システムの「売上分析」「ログ分析」「エラー分析」「ユーザー行動分析」などを支える基本かつ重要なテクニックです。そこには次のようなポイントがあります。

  • データ集計は、「入力 → グルーピング →集計 → 出力」という流れで考えると整理しやすいことです。
  • Map<キー, 集計値> という形が、集計結果の基本形であり、これを体で覚えておくと実務で役立つことです。
  • Function<T, K> や Function<T, BigDecimal> を使って、集計キーと集計値を柔軟に指定できる汎用ユーティリティを作ることで、さまざまな集計ニーズに対応できることです。
  • Stream+groupingBy+counting/reducing を使うことで、グルーピング+集計を宣言的かつコンパクトに書けるようになり、読みやすさと保守性が向上することです。
  • 集計キーの設計、集計値の型と精度、nullや空リストの扱いを意識することで、安全で扱いやすいデータ集計処理を設計できることです。
  • セキュリティ・品質の観点からも、データ集計は異常検知、監査、品質改善に大きく貢献する重要な仕組みであり、業務システムの信頼性を支えるテクニックになることです。

こうした感覚を「データ集計ユーティリティ」として形にしておくことで、システム全体で「生データを意味のある情報に変換する」文化が自然に根付き、売上・ログ・エラー・行動のすべてが分かりやすく、改善しやすいものへと整っていきます。

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