Entering Results & Calculating Correct Score vs. Correct Result
Premier League Predictor: Django & MySQL
Chapter 6 · Entering Results & Calculating Correct Score vs. Correct Result
Every fixture since Chapter 4 has sat with home_score/away_score null. Every prediction since Chapter 5 has sat with points_awarded null too — a field that chapter already added to the model in anticipation of this one. This chapter fills in the real result and, at the same moment, scores everything predicted against it — with real, confirmed point values: 40 points for a correct score, 10 points for a correct result.
A Pure Scoring Function, Shared by Every Source
User, expert, AI, and every individual guest are all scored by the exact same rule — there's no special case per source, because the rule genuinely doesn't need one. The logic itself has nothing to do with Django or MySQL at all; it's plain Python, sitting in its own module:
The exact-match check runs first, deliberately: every correct score is automatically a correct result too, since the outcome is derived straight from the scoreline — but a correct result doesn't imply a correct score in the other direction. Checking the stronger condition first and returning immediately is what stops a correctly predicted 2-1 from being scored as a mere 10-point correct result instead of the full 40.
Entering a Result: One POST, Then a Real Batched Rescore
A small POST view, matching the @require_POST convention already established for every mutation in this course since Chapter 4:
prediction.save() inside the for loop itself — one individual UPDATE statement per prediction, N round trips to MySQL for N predictions. Prediction.objects.bulk_update(predictions, ['points_awarded']) instead collapses that into far fewer queries: internally, Django batches the objects (100 per batch by default) and compiles each batch into a single real SQL statement built around a CASE WHEN id = ... THEN ... END expression, updating every row in the batch in one round trip rather than one per row — the same category of "gets this for free from the ORM" payoff update_or_create() already delivered in Chapter 5, applied here to bulk rescoring instead of a single upsert.
bulk_update() never calls each object's save() method, and it sends no pre_save/post_save signals. That's genuinely fine here — nothing in this app currently listens for those signals on Prediction — but it's a real, honest caveat worth knowing before reaching for bulk_update() in a codebase that does hang logic off save signals, since that logic would silently never run for a bulk-updated row.
enter_result re-fetches and recomputes points_awarded for every prediction on the fixture every single time it's called — not just the first time. Fixing a mistyped scoreline genuinely re-scores every user, expert, guest, and AI prediction correctly against the corrected result, rather than leaving stale points sitting in the database from the wrong one. A fixture with zero predictions recorded simply loops over an empty list and calls bulk_update() with nothing to update — no special case needed.
The points_awarded Field Was Already Waiting
Chapter 5 added points_awarded to the Prediction model specifically for this chapter — models.PositiveSmallIntegerField(null=True, blank=True), null until a real result exists. Chapter 5's own list_predictions view already selects it too, in its .values('id', 'source', 'guest_name', 'predicted_home_score', 'predicted_away_score', 'points_awarded') call — nothing about that view needs to change now that the field actually has values in it.
Resolving Chapter 5's Deferred Question: Average the Points, Not the Scorelines
Chapter 5 left one thing open: multiple guests predicting the same fixture produce multiple real scorelines that don't average into another valid scoreline — 3-0 and 1-0 average to 2-0, which happens to look plausible by coincidence, but 3-0 and 0-2 average to "1.5-1," which isn't a result anything could actually finish.
Prediction row now gets a real points_awarded from the identical score_prediction() function used for user, expert, and AI — 40, 10, or 0, same as anyone else. A fixture with three guests that week now has three separate, real point values. Averaging those three numbers is completely well-defined, in a way averaging their three original scorelines never could be. Chapter 8's own prediction league table is where this average actually gets computed and folded into the guest predictor's own season total — one averaged figure per fixture, deliberately, so a fixture with three guests doesn't count three times as much toward the season total as a fixture with only one.
A Worked Example: Five Predictions Against a Real 2-1 Result
Say fixture 42 already has five real predictions recorded from Chapter 5 — user, expert, two separately-named guests, and AI. enter_result is called with {"home_score": 2, "away_score": 1}, and GET /fixtures/42/predictions/ comes back:
| Source | Predicted | Outcome | points_awarded |
|---|---|---|---|
| user | 2-1 | exact match | 40 |
| expert | 1-1 | draw vs. real home win | 0 |
| guest — Micah Richards | 3-0 | home win, right outcome | 10 |
| guest — Jamie Carragher | 1-0 | home win, right outcome | 10 |
| ai | 2-0 | home win, right outcome | 10 |
The user's exact 2-1 earns the full 40. The expert's 1-1 is a draw — a different outcome from the real home win, so it earns nothing despite being close on paper. Both guests and the AI predicted a home win without the exact score, so each earns 10. The guest average for this fixture is therefore (10 + 10) / 2 = 10, the figure Chapter 8 would fold into the guest predictor's own season total for this one fixture.
Recording a Result From the Frontend
A small form, reusing the same csrftoken established in Chapter 4:
Where This Course Is Headed
The real league table — points, goal difference, wins/draws/losses, built from every fixture this chapter has now filled in a real result for (Chapter 7); the prediction league table, aggregating every source's own points_awarded across the season, including the guest-averaging-by-fixture this chapter just resolved (Chapter 8); and promotion/relegation, operating on SeasonTeam from Chapter 2 (Chapter 9).
Hands-On Exercises
Explain why score_prediction checks for an exact scoreline match before checking the outcome, and what would go wrong (in terms of points actually awarded) if the outcome check ran first instead.
📄 View solutionExplain why this chapter resolves the guest-averaging problem by averaging each guest's own points rather than averaging their raw predicted scorelines, using a concrete example of two guest scorelines that cannot be meaningfully averaged directly.
📄 View solutionEnter an incorrect result for a real fixture with at least two predictions already recorded via enter_result, confirm their points_awarded values, then call enter_result again with the corrected result and confirm every prediction's points_awarded genuinely changed via bulk_update() rather than only being scored the first time.
📄 View solutionChapter 6 Quick Reference
- Real point values (confirmed) — 40 points for a correct score, 10 points for a correct result, 0 otherwise
- score_prediction() — one pure function, shared by every source, exact-match checked before outcome
- enter_result — a POST view filling in the real scoreline, flipping status to "played," and rescoring every prediction on the fixture from scratch
- bulk_update() — a real Django ORM built-in batching many row updates into a single CASE WHEN statement, instead of one save() per row
- Honest caveat — bulk_update() skips save() and fires no pre_save/post_save signals
- Correcting a result — genuinely re-scores everything, not just the first entry
- Guest averaging, resolved — average each guest's own points per fixture, not their raw scorelines, which don't average into anything valid
- Next chapter: The real league table — points, goal difference, wins/draws/losses