Thirty-seven percent of bookings never carry a passenger. Today we build the dashboard that finds out why.
$group stage| Time | Segment | What Happens |
|---|---|---|
| 0–10 min | The brief | The ops director's question, and what 37.71% actually costs |
| 10–20 min | Why Charts | Live data vs extract; where Charts sits against Excel, Tableau and Power BI |
| 20–32 min | The mental model | Data sources, encoding channels, and how they map to aggregation stages |
| 32–50 min | Build along — charts 1 & 2 | Status mix and revenue by vehicle type |
| 50–62 min | Build along — charts 3 & 4 | Demand by hour, and the cancellation-reason breakdown |
| 62–72 min | Chart choice | Matching chart type to question; the four that are almost always wrong |
| 72–84 min | Traps | Small samples, missing fields, truncated axes — with live examples from this data |
| 84–90 min | Wrap | Assemble the dashboard; set up for the Lecture 16 deep-dive |
For four lectures you have been running queries against bdma.rides because they were on a worksheet. Today the worksheet is replaced by a person.
The brief. You are three weeks into a role at a ride-hailing operator in Delhi NCR. The operations director stops you after a meeting and says: “Our completion rate is sitting around sixty percent and finance thinks we're leaving money on the table. Nobody can tell me where it's going. You have the data. I have a board meeting on Friday — give me somewhere to start.”
That is a real brief, and it is typical in exactly the way that matters: it names a symptom, not a question. Before you open any tool, the first job is to convert it into something answerable.
“Where is the money going?” cannot be plotted. Decompose it until each piece can be:
| Question | Shape of the answer | Chart that answers it |
|---|---|---|
| How do bookings actually end? | A breakdown of one categorical field | Donut or bar |
| Which failures are most common? | A ranked list within one category | Horizontal bar |
| Does failure depend on time of day? | A measure across an ordered dimension | Column + line combo |
| Which products carry the value? | A ranked measure by category | Horizontal bar |
| Where does it happen? | A ranked list with a secondary rate | Table or bar |
Each row of that table becomes one chart. Nothing else gets built. This discipline — no chart without a question written down first — is the single most useful habit in this module, and it is also the first thing the project rubric checks.
Begin by establishing the magnitude, because it determines how much effort the problem deserves. From the aggregations you wrote in Lecture 14:
| Measure | Value | Why it matters |
|---|---|---|
| Total bookings (2024) | 15,000 | The denominator for everything below |
| Completed | 9,343 (62.29%) | Only these earn revenue |
| Failed | 5,657 (37.71%) | The problem, quantified |
| Revenue realised | ₹22,02,449 | What the operation actually earned |
| Average completed fare | ₹235.73 | The multiplier for lost bookings |
| Estimated lost revenue | ₹13,33,539 | 5,657 × ₹235.73 — the headline number |
Be honest about what “lost revenue” means. ₹13,33,539 assumes every failed booking would have completed at the average fare. It would not have. Some customers rebooked successfully within minutes and are already counted in the 9,343. Some trips would have been short and cheap. The number is an upper bound, and you must say so out loud — because the first thing a competent CFO will do is attack it. A defended upper bound survives that attack; an undefended point estimate does not.
Even halved, the figure is roughly ₹6.7 lakh against ₹22 lakh of realised revenue. That is large enough to justify the work. Establishing this in the first five minutes is what separates an analysis from a hobby.
You could export a CSV and build this in Excel in an afternoon. Many analysts would. It is worth being precise about what you lose when you do.
| Dimension | CSV → Excel | Atlas Charts |
|---|---|---|
| Freshness | Frozen at export. Someone must remember to redo it. | Queries the live collection on each load |
| Nested data | route.pickup must be flattened by hand first | Dot notation works natively |
| Size ceiling | About a million rows, and painful well before that | Aggregation runs server-side; the browser receives results only |
| Reproducibility | Manual steps live in one person's memory | The chart is the specification |
| Sharing | Email a file; now there are nine versions | One URL, access controlled |
| Ad-hoc exploration | Excellent — genuinely better than Charts | Fine, but more structured |
The honest summary. Excel is better for exploring something once. Charts is better for anything that must stay true next month. The failure mode we are designing against is not “Excel is bad” — it is the quarterly report that silently reflects data from March.
Atlas Charts is not competing with Tableau or Power BI on features, and you should not present it as though it were. It wins on exactly one axis: it is already connected to your data.
For a team already on Atlas that needs an operational dashboard this week, Charts is usually the right call. For a finance function blending ERP, CRM and warehouse data, it is not. Saying which situation you are in is the analysis; the tool follows.
Atlas Charts has three concepts. Once they click, the interface stops being a maze.
A data source is a collection, a view, or a collection with a pipeline attached. It is defined once in Charts → Data Sources and reused by every chart that needs it.
The important option is the one people miss: a data source can carry its own aggregation pipeline. Everything you learned in Lecture 14 is available here. If a chart needs a derived field — booking hour, say — you do not fight the chart builder. You attach a pipeline to the source and the field simply exists:
[
{ $addFields: {
hour: { $toInt: { $substr: ["$booked_at", 11, 2] } },
failed: { $cond: [{ $eq: ["$status", "Completed"] }, 0, 1] }
} }
]
Two lines, and every chart built on that source can now encode by hour or average the failure flag. Recall from Lecture 12 that booked_at is stored as an ISO string in this dataset, which is why we slice characters 11–12 rather than calling $hour. If you prefer the date operators, convert first with { $toDate: "$booked_at" }.
Push work into the source, not the chart. A derived field defined once in the source stays consistent across every chart on the dashboard. The same expression pasted into six charts will diverge the first time someone edits five of them.
A chart in Charts is built by dragging fields onto channels. Each channel is a visual property, and each one maps directly onto something you already know how to write by hand:
| Channel | Visual role | Aggregation equivalent |
|---|---|---|
| X / Category | What the bars or slices are | The _id of a $group |
| Y / Value | How tall or large each one is | The accumulator — $sum, $avg, $count |
| Series / Colour | Splits each category into sub-bars | A compound _id with two keys |
| Filter | Which documents are considered | $match, placed first |
| Sort & limit | Ranking and top-N | $sort then $limit |
This equivalence is the point of teaching aggregation before visualisation. Charts is a graphical front end to a pipeline. When a chart shows a number you did not expect, you debug it by asking what pipeline would produce this? — and then you go and run that pipeline in mongosh to check.
Filters exist at three levels, and knowing which one you used matters more than it sounds:
$match in the source pipeline. Applies everywhere, invisible on the chart.An undisclosed filter is how dashboards lie. A chart titled “Average Booking Value” that quietly filters to status: "Completed" is not wrong — ₹235.73 is the correct average for completed rides — but a reader who assumes it covers all 15,000 bookings has been misled. Put the filter in the title: “Average Booking Value (Completed Rides Only)”. This is marked in both Assignment 3 and the project rubric.
Build these along with the lecturer. Each one states its question first, then its configuration, then the answer it gives.
Type: Donut. Label: status. Arc: COUNT.
Five categories, parts of a whole, and the whole is meaningful. This is one of the few cases where a donut genuinely beats a bar chart — the reader's question is “what share?”, not “which is bigger?”.
The result, which should match your Lecture 14 pipeline exactly:
| Status | Bookings | Share |
|---|---|---|
| Completed | 9,343 | 62.29% |
| Cancelled by Driver | 2,698 | 17.99% |
| No Driver Found | 1,036 | 6.91% |
| Cancelled by Customer | 1,022 | 6.81% |
| Incomplete | 901 | 6.01% |
What it tells you immediately: driver cancellations are 17.99% — more than two and a half times customer cancellations at 6.81%. Add “No Driver Found” at 6.91% and roughly 25% of all bookings fail on the supply side. That single comparison reframes the brief: this is not customers changing their minds, it is the operator failing to deliver a car. Every subsequent chart should be chosen to test that hypothesis.
Type: Horizontal bar. Y: vehicle_type. X: SUM of booking_value. Sort: descending.
Horizontal, not vertical, because the category labels are words — “Premier Sedan” reads cleanly on a horizontal axis and becomes a rotated mess on a vertical one.
| Vehicle type | Revenue | Bookings | Avg fare | Completion |
|---|---|---|---|---|
| Premier Sedan | ₹5,36,855 | 1,954 | ₹442.22 | 62.13% |
| Go Sedan | ₹4,91,473 | 2,854 | ₹278.77 | 61.77% |
| Uber XL | ₹4,31,927 | 1,231 | ₹558.77 | 62.79% |
The insight hiding in the ordering: Premier Sedan earns the most revenue from only 1,954 bookings, while Go Sedan takes 2,854 bookings to earn less. Uber XL earns ₹4.3 lakh from 1,231 bookings. Ranking by volume and ranking by revenue give different answers — and a failed Uber XL booking costs ₹558.77 against ₹278.77 for a Go Sedan. If you can only fix cancellations for one product, the arithmetic says start where each failure is most expensive, not where failures are most numerous.
A dashboard that only ever counts things will mislead you. Count answers “how often”. Value answers “how much”. They rank differently, and the decision almost always follows value.
Type: Column chart with a line series. X: hour (the field derived in the source pipeline). Y (columns): COUNT. Y (line): MEAN of failed, formatted as a percentage.
Two measures on different scales against a shared ordered dimension. Volume as columns, rate as a line, and the line gets its own axis.
| Hour | Bookings | Failure rate | Reading |
|---|---|---|---|
| 18:00 | 1,373 | 37.14% | Busiest hour of the day |
| 19:00 | 1,246 | — | Evening peak continues |
| 09:00 | 1,172 | — | Morning commute |
| 08:00 | 1,076 | — | Morning commute |
| 17:00 | 1,062 | 39.92% | Worst failure rate of any high-volume hour |
This is the finding of the session. The busiest hour is 18:00, and the instinct of every operations team is to throw resources at the peak. But 17:00 — one hour earlier, still carrying 1,062 bookings — fails at 39.92% against the peak's 37.14%. Supply is thinnest just before demand peaks, not during it. Drivers have not yet repositioned for the evening rush.
Volume is not risk. The hour with the most bookings is not the hour with the most problems. A dashboard that plots only volume would have sent the operations director to 18:00 and the intervention would have underperformed. This is precisely the kind of finding that justifies building the dashboard at all.
Type: Horizontal bar. Y: cancellation.reason. X: COUNT. Filter: cancellation.by = "driver".
Note the dot notation on a nested field — no flattening step, which is exactly the advantage Charts has over a CSV export.
| Driver cancellation reason | Count | Share |
|---|---|---|
| Customer related issue | 729 | 27.02% |
| More than permitted people in there | 666 | 24.68% |
| Personal & Car related issues | 652 | 24.17% |
| The customer was coughing/sick | 651 | 24.13% |
A genuinely awkward result. Four reasons, all between 24% and 27%. There is no dominant cause. This is worth sitting with, because the temptation is to pick the top bar and call it the answer — but a 27% leader among four near-equal options is not a finding, it is a flat distribution.
The honest read: there is no single fix available. Either you attack all four, or you accept that the reason codes themselves are too coarse to act on and recommend better instrumentation. “The data cannot answer this; here is what we would need to collect” is a legitimate and often valuable recommendation. We develop it properly in Lecture 16.
Chart choice is not aesthetic. Each type encodes a specific relationship, and using the wrong one makes the reader work to undo your encoding.
| The question | Use | Because |
|---|---|---|
| Which is biggest? | Bar (horizontal if labels are words) | Length on a common baseline is the most accurately judged visual channel |
| What share of the whole? | Donut — with at most 5–6 slices | Angle reads share directly; beyond six slices it becomes unreadable |
| How does it move over time? | Line | Slope encodes rate of change, which the eye reads instantly |
| Two measures, one dimension? | Combo column + line, dual axis | Keeps different units on their own scales |
| Exact values matter? | Table | When the reader will quote the number, do not make them estimate it |
| Where, geographically? | Map | Only when position itself carries meaning |
| Density across two dimensions? | Heatmap | Hour × day-of-week demand is the canonical case |
Each of these is live in the course dataset right now. They are not hypothetical.
Sort the hourly failure rates from Chart 3 by rate instead of by hour, and the top of the list is not 17:00. It is 03:00, at 43.48%.
03:00 carries 46 bookings. Twenty of them failed. One more cancellation moves that rate by more than two percentage points. It is noise wearing the costume of a finding — and a dashboard that ranks purely by rate will send the operations director to staff the night shift.
The fix is a volume floor, applied in the source pipeline after the grouping stage:
{ $group: {
_id: "$hour",
bookings: { $sum: 1 },
failed: { $sum: { $cond: [{ $eq: ["$status", "Completed"] }, 0, 1] } }
} },
{ $match: { bookings: { $gte: 1000 } } },
{ $addFields: {
failure_rate: { $round: [{ $multiply: [{ $divide: ["$failed", "$bookings"] }, 100] }, 2] }
} },
{ $sort: { failure_rate: -1 } }
Now 17:00 tops the list legitimately. The threshold of 1,000 is a judgement call, not a law — but you must state it and be able to defend it. “We excluded hours below 1,000 bookings because rates below that are unstable” is a defensible sentence. Silently excluding them is not.
Recall from Lecture 12 that booking_value is absent on cancelled and no-driver-found bookings. MongoDB's $avg skips missing fields silently — it does not treat them as zero, and it does not warn you.
| What you built | What it computes | Result |
|---|---|---|
$avg over all 15,000 documents | Mean over the ~10,244 that have the field | Looks like a fleet average, is not |
$avg after $match: { status: "Completed" } | Mean over exactly 9,343 completed rides | ₹235.73 — defensible |
Both produce a plausible-looking number. Only the second has a definition you can state in one sentence. Filter explicitly to the population you mean, and name that population in the chart title.
Completion rates by vehicle type run from 61.77% to 62.79% — a spread of one percentage point across the fleet. Plot that on an axis from 61% to 63% and Uber XL's bar towers over Go Sedan's. The chart is technically accurate and completely misleading.
Start value axes at zero. The honest conclusion here is that completion rate barely varies by vehicle type, which is itself informative: the failure problem is not product-specific, so a product-specific fix will not work. A truncated axis would have hidden that conclusion behind a dramatic-looking bar chart.
The common thread. All three traps produce charts that are defensible line by line and wrong as a whole. The defence is the same in each case: state the population, state the threshold, start at zero, and sanity-check every chart against a pipeline you ran by hand in mongosh.
Open Charts. In Atlas, select your project and click Charts in the left sidebar. On first use it provisions automatically — no separate signup.
Add the data source. Go to Data Sources → Add Data Source, pick your cluster, and select bdma.rides. Confirm it reports 15,000 documents. If it does not, re-run the mongoimport from Lecture 12 before continuing.
Attach the pipeline. Open the source's options, choose Pipeline, and paste the $addFields stage from Section 3. Save. The fields hour and failed now appear in the field list of every chart built on this source.
Create the dashboard. Dashboards → Add Dashboard. Name it Ride Operations — Failure Analysis. A name that states the subject beats “Dashboard 1” the moment a second dashboard exists.
Build the four charts from Section 4, in order. After each one, verify the number against the corresponding mongosh pipeline from Lecture 14. If the chart and the shell disagree, the chart has a filter you forgot about — find it before you build the next chart.
Title every chart with its question. Not “Status” but “How do bookings end? (All 15,000 bookings, 2024)”. The title carries the question and the population; the chart carries the answer.
Arrange for reading order. Top-left is read first, so put the status donut there — it frames everything else. Hourly failure goes top-right. Revenue and cancellation reasons go below. Readers scan in a Z; lay the argument out along it.
Compare against the reference. Open the course dashboard alongside yours. Every number should match exactly — the dataset is seeded, so there is no room for “close enough”. A mismatch is a bug in your filter, and finding it now is the whole point of the exercise.
$group._id, value is the accumulator, filter is $match. Debug a wrong chart by writing the pipeline it implies and running it.status: "Completed" filter turns a correct average into a misleading one.Build it yourself before Lecture 16. The Atlas Charts build guide gives you the exact aggregation behind every panel on the reference dashboard, the chart type to pick, and which field belongs on which encoding channel. Work through it with your own cluster — the lab in this session assumes you arrive with the dashboard already standing up.
Four charts and a finding are not yet an answer. Next session we take the 17:00 insight apart, work out what the flat cancellation-reason distribution really means, and turn the dashboard into the one-page recommendation the operations director asked for on Friday. Bring the dashboard you built today — we edit it live.