January 13, 2026

Selecting Top Players Across 3 Months in Java (Map Aggregation + Streams)

When match data is split across multiple months, the cleanest way to compute “best players” is to:

  1. Aggregate all records into a per-player summary (matches + total score, month-wise counts)

  2. Filter by eligibility rules

  3. Sort by average score

  4. Pick the top K

This post walks through a simple, interview-friendly Java solution using a HashMap + a Streams pipeline.


Problem

You’re given match performance data for the last 3 months:

  • month1.csv

  • month2.csv

  • month3.csv

Each record is:

(playerId, score)

Each record represents one match played by that player.


Eligibility Rules (as used in the code below)

A player is eligible if:

  1. Played at least 4 matches overall (across all months)

  2. Played at least 1 match in each month

  3. Their average score across all matches is greater than 50

  4. From eligible players, return the TOP 3 by average score (descending)

Want the original stricter rules (like 20 overall and 5 per month)?
This solution is parameterized, so you’ll only change the thresholds.


Example Input

month1:
(P1, 60), (P2, 45), (P3, 70)

month2:
(P1, 55), (P1, 56), (P2, 50), (P3, 75)

month3:
(P1, 65), (P2, 55), (P3, 68), (P3, 69)

Expected Output

["P3", "P1"]

Why?

  • P1 scores: 60, 55, 56, 65 → average = 59.0 ✅ (played all months + 4 matches)

  • P2 scores: 45, 50, 55 → average = 50.0 ❌ (must be > 50 and also only 3 matches)

  • P3 scores: 70, 75, 68, 69 → average = 70.5 ✅

Sorted by average score: P3, then P1


Approach

Step 1: Aggregate into a per-player summary

We build a map:

Map<String, PlayerScoreSummary> summaryByPlayer

Each player summary tracks:

  • matchesByMonth[m]

  • scoreByMonth[m]

This makes it easy to check:

  • “played each month”

  • “total matches”

  • “average score”

Step 2: Filter + sort + limit

After aggregation:

  • filter players that fail rules

  • sort by average score descending

  • take top K


Final Java Solution (Single File, Runnable)

import java.util.*;
import java.util.stream.Collectors;

public class TopPlayersSelectorBlog {

    // ---------------- SAMPLE INPUT ----------------
    static List<PlayerScore> month1 = Arrays.asList(
            new PlayerScore("P1", 60),
            new PlayerScore("P2", 45),
            new PlayerScore("P3", 70)
    );

    static List<PlayerScore> month2 = Arrays.asList(
            new PlayerScore("P1", 55),
            new PlayerScore("P1", 56),
            new PlayerScore("P2", 50),
            new PlayerScore("P3", 75)
    );

    static List<PlayerScore> month3 = Arrays.asList(
            new PlayerScore("P1", 65),
            new PlayerScore("P2", 55),
            new PlayerScore("P3", 68),
            new PlayerScore("P3", 69)
    );

    public static void main(String[] args) {
        List<List<PlayerScore>> months = List.of(month1, month2, month3);

        // Rules (easy to change)
        int minTotalMatches = 4;
        int minMatchesEachMonth = 1;
        double minAvgScore = 50.0;   // must be strictly greater than 50
        int topK = 3;

        List<String> selected = selectTopPlayers(
                months,
                minTotalMatches,
                minMatchesEachMonth,
                minAvgScore,
                topK
        );

        System.out.println("Selected Players: " + selected);
        // Expected: [P3, P1]
    }

    /**
     * Returns topK playerIds by average score (descending),
     * after applying eligibility rules.
     */
    static List<String> selectTopPlayers(
            List<List<PlayerScore>> months,
            int minTotalMatches,
            int minMatchesEachMonth,
            double minAvgScoreExclusive,
            int topK
    ) {
        int monthCount = months.size();
        Map<String, PlayerScoreSummary> summaryByPlayer = new HashMap<>();

        // 1) Aggregate (single pass over each month)
        for (int m = 0; m < monthCount; m++) {
            for (PlayerScore ps : months.get(m)) {
                summaryByPlayer
                        .computeIfAbsent(ps.playerId, id -> new PlayerScoreSummary(id, monthCount))
                        .addMatch(m, ps.score);
            }
        }

        // 2) Filter + sort + limit + map to playerId
        return summaryByPlayer.values().stream()
                .filter(s -> s.totalMatches() >= minTotalMatches)
                .filter(s -> s.matchesEachMonthAtLeast(minMatchesEachMonth))
                .filter(s -> s.averageScore() > minAvgScoreExclusive)
                .sorted(
                        Comparator.comparingDouble(PlayerScoreSummary::averageScore).reversed()
                                // optional tie-breaker for stability
                                .thenComparing(PlayerScoreSummary::totalMatches, Comparator.reverseOrder())
                                .thenComparing(s -> s.playerId)
                )
                .limit(topK)
                .map(s -> s.playerId)
                .collect(Collectors.toList());
    }

    // ---------------- DATA MODELS ----------------

    static class PlayerScore {
        final String playerId;
        final int score;

        PlayerScore(String playerId, int score) {
            this.playerId = playerId;
            this.score = score;
        }
    }

    static class PlayerScoreSummary {
        final String playerId;
        final int[] matchesByMonth;
        final int[] scoreByMonth;

        PlayerScoreSummary(String playerId, int monthCount) {
            this.playerId = playerId;
            this.matchesByMonth = new int[monthCount];
            this.scoreByMonth = new int[monthCount];
        }

        void addMatch(int monthIndex, int score) {
            matchesByMonth[monthIndex]++;
            scoreByMonth[monthIndex] += score;
        }

        int totalMatches() {
            int sum = 0;
            for (int c : matchesByMonth) sum += c;
            return sum;
        }

        int totalScore() {
            int sum = 0;
            for (int s : scoreByMonth) sum += s;
            return sum;
        }

        double averageScore() {
            int matches = totalMatches();
            if (matches == 0) return 0.0;
            return (double) totalScore() / matches;  // avoid integer division
        }

        boolean matchesEachMonthAtLeast(int minMatchesEachMonth) {
            for (int c : matchesByMonth) {
                if (c < minMatchesEachMonth) return false;
            }
            return true;
        }
    }
}

Complexity

Let N be the total number of match records across all months.

  • Aggregation: O(N)

  • Sorting eligible players: O(P log P) where P is number of players

  • Space: O(P) for the per-player summaries

In interviews, this is typically considered optimal and clean.


Common Pitfalls (and how this code avoids them)

1) Integer division bugs

If you do totalScore / totalMatches with integers, you truncate.
We use:

(double) totalScore / totalMatches

2) Missing months

If a player never appears in a month, their matchesByMonth[m] stays 0, and they fail the per-month filter.

3) Hardcoding month fields (scoreA/scoreB/scoreC)

Instead of scoreA, scoreB, scoreC, we use arrays so the code is:

  • less repetitive

  • easy to scale from 3 months to N months


How to Switch to the Original Stricter Rules

Just change the parameters:

int minTotalMatches = 20;
int minMatchesEachMonth = 5;
double minAvgScore = 50.0;
int topK = 3;

Everything else stays the same.


Interview Add-ons (If You Want to Impress)

  • Read CSVs (BufferedReader) → convert to List<PlayerScore>

  • Handle malformed rows safely

  • If the dataset is massive:

    • stream file line-by-line

    • aggregate in a map without storing all rows

  • Add unit tests for:

    • “exactly 50 average should fail”

    • “missing one month should fail”

    • “fewer than 3 eligible players”