NCAA bracket C# source code results

Just about a month ago, I posted some C# source code about making picks for an NCAA bracket. The code is basically broken into 3 parts:

  • Retrieving all team information from a config file.
  • For each match-up between two teams, pick the winner given an algorithm.
  • Display the result bracket.

The code was built to be modular enough so that any step could be expanded upon at a later date. The most obvious step that needs building is (2). The code as it is now only looks at the two teams' ranks - though it doesn't just take the lower rank (it's random but favors the lower rank), but still.

Anyways, now that the entire championship is complete and all the results are out there, I can run it a few more times and see how the bracket generator holds up when compared against the actual winners. Given that in most NCAA pools, the Final Four and on is really what it comes down to, I figured I would just run the program N times and check the following: 1) Did it nail all four teams in the Final Four? 2) The two teams in the finals? 3) The final winner.

Instead of using another config file, I just quickly built the final four, finals, and champion data manually in the code:

        private List GetWinners()
        {
            Team mich = new Team("Michigan State", 2);
            Team conn = new Team("Connecticut", 1);
            Team vill = new Team("Villanova", 3);
            Team nc = new Team("North Carolina", 1);

            Round four = new Round();
            four.Teams.Add(mich);
            four.Teams.Add(conn);
            four.Teams.Add(vill);
            four.Teams.Add(nc);

            Round two = new Round();
            two.Teams.Add(mich);
            two.Teams.Add(nc);

            Round one = new Round();
            one.Teams.Add(nc);

            List winners = new List();
            winners.Add(four);
            winners.Add(two);
            winners.Add(one);
            return winners;
        }

Here's the code for checking if one set of teams is equal to another set, i.e. if the teams in my Final Four picks match the actual Final Four results:

        private bool RoundsEqual(Round r1, Round r2)
        {
            int equalCount = 0;

            foreach (Team t1 in r1.Teams)
            {
                foreach (Team t2 in r2.Teams)
                {
                    if (String.Compare(t1.Name, t2.Name, true) == 0)
                    {
                        equalCount++;
                        break;
                    }
                }
            }

            return (equalCount == r1.Teams.Count);
        }

One more quick thing. I moved all the code from last week into one class: BracketProcessor. For the last entry, all the code was just in the page CreateBracket.aspx. But now I wanted a new page BracketResultsReport.aspx. So I moved into all into a separate object, called that from both pages, left the method DisplayBracket on CreateBracket, and included this new code on the BracketResultsReport page. The code from last post is all the same; only change is that I made sure that the GetFirstRound method is only called once, since on the new page, it would be called several times.

With all that in place, the rest of the code is pretty straightforward:

        private const int TEST_COUNT = 1000000;

        protected void Page_Load(object sender, EventArgs e)
        {
            BracketProcessor processor = new BracketProcessor();
            List winners = GetWinners();
            int[] correctCounts = new int[] { 0, 0, 0 };

            for (int i = 0; i < TEST_COUNT; i++)
            {
                List rounds = processor.BuildBracket();
                int roundOffset = rounds.Count - 3;

                for (int j = 0; j < 3; j++)
                {
                    if (RoundsEqual(winners[j], rounds[j + roundOffset]))
                        correctCounts[j]++;
                }
            }

            Response.Write(String.Format("Total tests: {0}
", TEST_COUNT.ToString())); Response.Write(String.Format("Final Four: {0}
", correctCounts[0].ToString())); Response.Write(String.Format("Finals: {0}
", correctCounts[1].ToString())); Response.Write(String.Format("Winner: {0}", correctCounts[2].ToString())); }

I double-checked this would work, then set the count up to a million. I ran it a few times and got pretty consistent results:

Total tests: 1000000
Final Four: 6238
Finals: 31904
Winner: 185449

Total tests: 1000000
Final Four: 6252
Finals: 32005
Winner: 185466

Total tests: 1000000
Final Four: 6225
Finals: 32114
Winner: 185839

With percents, that's roughly:
Final Four: 0.6%
Finals: 3.2%
Winner: 18.5%

Not too shabby, especially with an admittedly rudimentary algorithm to pick a winner. That's about 1 in 5 for picking the winner. With the structure in place, the PickWinner method could be beefed up to take in more stats, including further results of this NCAA championship.

halgardner's picture

I'm sure if you factored in

I'm sure if you factored in percentage of upsets over the past 25 years, example being that in the first round at least one 12 seed upsets a number 5 seed given the divisions placed in those rankings, the same with the 7 and 10 seeds, you might bring all of your results up a few percentage points. If not to predict the winner, to at least raise the likelihood of predicting the final four.

-Hal

Blake's picture

Quick update

Here are the results with the new check:

Total tests: 1000000
Final Four: 6187
Finals: 31742
Winner: 185497

Total tests: 1000000
Final Four: 6294
Finals: 32330
Winner: 185193

Total tests: 1000000
Final Four: 6154
Finals: 31726
Winner: 185419

I'll post the source code in a bit, but looks like, at least with the 2009 data, the FirstRoundUpsetValidator had little effect.

Blake's picture

Adding IRoundValidators

Yeah, the difference in the results is pretty negligible. Here are the code updates. I took out the indent in the code to make it a little more readable (after reviewing this post on my iPhone and seeing the navs get pushed waaay out).

Here's the IRoundValidator and the first use of it, FirstRoundUpsetValidator which enforces the rules Hal mentioned: in the first round, one team of each rank 10 and rank 12 has to win.

public interface IRoundValidator
{
    bool IsValidRound(Round round);
}

public class FirstRoundUpsetValidator : IRoundValidator
{
    public bool IsValidRound(Round round)
    {
        if (round.Number != 2)
            return true;

        int[] reqRanks = new int[] { 10,12 }; //ordered
        bool[] reqFound = new bool[reqRanks.Length];

        for (int i = 0; i < reqRanks.Length; i++)
            reqFound[i] = false;

        foreach (Team team in round.Teams)
        {
            for (int i = 0; i < reqRanks.Length; i++)
            {
                if (team.Rank == reqRanks[i])
                {
                    reqFound[i] = true;
                    break;
                }
                
                if (team.Rank > reqRanks[i])
                    break;
            }
        }

        foreach (bool found in reqFound)
        {
            if (!found)
                return false;
        }

        return true;
    }
}

Then, in BracketProcessor...

  • Add a [lazy-loaded] collection of IRoundValidators
  • When creating new rounds in BuildBracket, verify each round. If it's not valid, try again. I gave this a cap, so basically if it can't build a good round 2 after 100 tries, just take the 100th one and go with it, just as a protection in case someone comes along and builds a (1 != 1) Validator.
  • Add property Number to the Round object. Then I just added nextRound.Number = round.Number + 1; to GetNextRound
  • Note: I'm never validating the first round since that's coming straight from the config.
private const int VALIDATE_ROUND_MAX = 100;

private List m_RoundValidators = null;
private List RoundValidators
{
    get
    {
        if (m_RoundValidators == null)
        {
            m_RoundValidators = new List();
            m_RoundValidators.Add(new FirstRoundUpsetValidator());
        }

        return m_RoundValidators;
    }
}

private List BuildBracket(Round firstRound)
{
    List rounds = new List();
    Round round = firstRound;
    rounds.Add(round);

    int prevCount = 0;

    while (round.Teams.Count > 1 && round.Teams.Count != prevCount)
    {
        prevCount = round.Teams.Count;

        for (int i = 0; i < VALIDATE_ROUND_MAX; i++)
        {
            round = GetNextRound(round);

            if (IsValidRound(round))
                break;
        }

        rounds.Add(round);
    }

    return rounds;
}

So yeah, I added the RoundValidators and it's good to have the structure, but as you can tell from the results above, the difference is pretty negligible.

Blake's picture

Interesting...

Interesting. That would be a new type of check - not just team by team as it is now, but for the whole first round - basically "now that Round One has been selected, validate the results." Then, probably loop through a collection of IValidators. If the round is invalid, try again. A little scary for performance since it might make several first round picks without getting a valid one, but it's a good new level to introduce to the structure.

As far as the actual validation step, the code already does take upsets into account - if a team with rank 2 is up against a team with rank 5, a random number is selected from 1 to 7. 1-5 is team one, 6-7 is team two. It favors the first team, but given all the games in round one, there'll be a few upsets. The logic you mentioned would basically be "The program already takes upsets into account, but given trends in the years past, double check that the upsets follow this pattern."

I'm a little hesitant to implement it given that it's not the rawest data out there... but let's give it a shot! I've got all the data, we'll see if the checks would help this year's picks. If anything, it would give me a good chance to build in that validation level. Plus the Round object currently just consists of a list of teams; with this change, I'll add RoundNumber to that.

I'll add these checks...........
- In the first round, a 12 rank has to upset a 5 rank
- In the first round, a 10 rank has to upset a 7 rank
.......... and run it again.

Since this is my first enhancement, the first piece of logic I'll have to decide if I want in or not, I may have to get more data to work with. I may get data from previous years and build separate config files for those. Hmm... then I actually will have to include the final four winners in the config, not just build them in the code as I've done here. <can of worms>

Post new comment

The content of this field is kept private and will not be shown publicly.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.

More information about formatting options

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
Image CAPTCHA
Copy the characters (respecting upper/lower case) from the image.