Question 161 · AI Capstone Project: Indian Language Detector · hard
Your Grade 9 AI capstone project builds a Naive Bayes classifier that detects whether a short Hinglish text snippet is Hindi or English, using two character-bigram features. From a training set with an equal number of Hindi and English snippets (so P(Hindi) = P(English) = 0.5), you estimate the following likelihoods: P('ka' present | Hindi) = 0.8, P('ka' present | English) = 0.2, P('th' present | Hindi) = 0.3, P('th' present | English) = 0.6. A new snippet, "usska kaam theek tha," contains both bigrams. Applying Naive Bayes' conditional-independence rule — multiplying the prior by each feature's likelihood for every class, then normalizing the two class scores so they sum to 1 — what does the classifier predict, and what is the resulting posterior probability?
Hindi wins the vote, posterior 2/3 (≈66.7%), from unnormalized scores 0.12 (Hindi) and 0.06 (English) divided by their sum, 0.18.
English overtakes Hindi at approximately 88.9% if the 'ka' bigram likelihoods for the two languages are accidentally swapped (0.2 used for Hindi, 0.8 for English).
Adding instead of multiplying the prior and the two likelihoods yields Hindi at approximately 55.2%, since 1.6 / 2.9 ≈ 0.552 rather than the correct 0.12 / 0.18.
Skipping the normalization step and reporting the raw product 0.12 directly as a probability yields Hindi at only 12%, not the correct 66.7%.
Answer: A. Hindi wins the vote, posterior 2/3 (≈66.7%), from unnormalized scores 0.12 (Hindi) and 0.06 (English) divided by their sum, 0.18.
ExplanationNaive Bayes assumes the two bigram features are conditionally independent given the class, so each class's unnormalized score is prior × likelihood('ka') × likelihood('th'). For Hindi: 0.5 × 0.8 × 0.3 = 0.12. For English: 0.5 × 0.2 × 0.6 = 0.06. These raw products are not yet probabilities — Naive Bayes converts them into one by dividing each by their sum, 0.12 + 0.06 = 0.18. That gives P(Hindi | data) = 0.12 / 0.18 = 2/3 ≈ 66.7% and P(English | data) = 0.06 / 0.18 = 1/3 ≈ 33.3%. Since 66.7% is the larger posterior, the classifier predicts Hindi with confidence 66.7%. The other outcomes correspond to real bugs students hit when coding this by hand: reading the 'ka' likelihood column from the wrong class (swapping 0.8 and 0.2) flips the prediction to English at ≈88.9%; replacing multiplication with addition — treating the score as prior + likelihood + likelihood instead of prior × likelihood × likelihood — produces a spurious 1.6/2.9 ≈ 55.2%; and forgetting the final normalization step, reporting the raw product 0.12 as though it were already a probability, understates Hindi's confidence as 12% instead of 66.7%. Watching out for these three failure modes — correct class-to-column mapping, multiplicative (not additive) combination of independent likelihoods, and always renormalizing at the end — is exactly what separates a working Naive Bayes language detector from a buggy one.
Question 162 · Full Stack Capstone: Building a Complete Indian Weather App · hard
Your capstone weather dashboard fetches conditions for three Indian cities before a road-trip planner shows the results. A teammate wrote this function to build it:
```js
async function getWeather(city, delayMs) {
console.log(`Fetching ${city}...`);
await new Promise(resolve => setTimeout(resolve, delayMs));
console.log(`${city} done`);
return `${city}: ${delayMs}ms`;
}
async function loadDashboard() {
const cities = [["Mumbai", 3000], ["Delhi", 1000], ["Chennai", 2000]];
console.time("total");
for (const [city, delayMs] of cities) {
await getWeather(city, delayMs); // <-- await INSIDE the loop
}
console.timeEnd("total");
}
loadDashboard();
```
Assuming each simulated API call takes exactly the delay shown, how long will `loadDashboard()` take to finish, and why?
About 6000ms, because putting await inside the for...of loop pauses execution on each iteration until that city's promise resolves, so the delays run one after another and add up (3000 + 1000 + 2000)
About 3000ms, because await inside a for...of loop still launches all three getWeather calls immediately, so they run in parallel and the total time equals the single largest delay
About 2000ms, because the JavaScript engine averages the three delays across the event loop before console.timeEnd fires
About 0ms, because console.time and console.timeEnd only measure synchronous code, so the awaited setTimeout calls run in the background without adding to the measured duration
Answer: A. About 6000ms, because putting await inside the for...of loop pauses execution on each iteration until that city's promise resolves, so the delays run one after another and add up (3000 + 1000 + 2000)
ExplanationThe key thing to trace is what `await` actually does inside a loop. When the engine hits `await` inside `getWeather`, it suspends `loadDashboard` at that exact line and does not move to the next loop iteration until the awaited promise settles. So the three calls do not overlap — Mumbai's 3000ms timer must finish before Delhi's `getWeather` call even starts, and Delhi's 1000ms timer must finish before Chennai's starts. The total wall-clock time is therefore the sum of the delays: 3000 + 1000 + 2000 = 6000ms, and the console log order is Mumbai fetching, Mumbai done, Delhi fetching, Delhi done, Chennai fetching, Chennai done — strictly in sequence.
This is a genuinely easy mistake to make because `Promise.all([getWeather("Mumbai",3000), getWeather("Delhi",1000), getWeather("Chennai",2000)])` really would start all three timers together and finish in ~3000ms (the largest single delay) — but that only happens when the promises are created and handed to `Promise.all` up front, not when `await` sits inside a sequential loop body. JavaScript's event loop doesn't reorder or average timers by duration either; `setTimeout` delays are honored as written, they're just not blocking the rest of the program the way a synchronous sleep would be. And `console.time`/`console.timeEnd` measure real elapsed wall-clock time between the two calls, including time spent suspended on `await` — they are not restricted to synchronous execution, which is exactly why this pattern is a common performance bug in real full-stack weather dashboards: fetching each city's data one at a time when they could all be requested in parallel with `Promise.all`.
Question 163 · JavaScript Fundamentals: Making Web Pages Interactive · hard
A grade 9 student builds an interactive quiz page with 3 answer buttons and writes this JavaScript to log which button was clicked:
```javascript
const buttons = document.querySelectorAll(".btn");
for (var i = 0; i < buttons.length; i++) {
buttons[i].addEventListener("click", function () {
console.log("Button " + i + " clicked");
});
}
```
There are exactly 3 buttons on the page (indices 0, 1, and 2). After the page finishes loading, the student clicks the second button (index 1). What does the console print, and why?
Button 1 clicked, because each click listener closes over its own copy of i, capturing the value it held at the moment addEventListener ran for that button.
Button 3 clicked, because var is function-scoped rather than block-scoped, so all three listeners share one i, which equals 3 by the time any click event fires.
Button 2 clicked, because the loop exits as soon as i reaches buttons.length - 1, so every listener reads that final in-range index.
A ReferenceError is thrown, because i falls out of scope once the for loop finishes and is undefined inside the click handler.
Answer: B. Button 3 clicked, because var is function-scoped rather than block-scoped, so all three listeners share one i, which equals 3 by the time any click event fires.
ExplanationIn JavaScript, a variable declared with var is function-scoped, not block-scoped, unlike let. That means the for loop does not create a fresh i for each pass -- all three addEventListener calls attach closures that reference the exact same i variable sitting in memory. None of those closures run during the loop itself; they only execute later, whenever a user actually clicks a button. By the time the loop condition i < buttons.length is checked and finally fails, i has already been incremented to 3 (since buttons.length is 3), and the loop exits leaving i permanently at that value. So when the student clicks the second button (index 1), the handler doesn't read the index the button was created with -- it reads whatever i currently equals, which is 3 for every single button. This is exactly why modern DOM event-handling code favors let over var inside loop counters: let creates a new, block-scoped binding of i on every iteration, so each closure correctly locks in its own index (0, 1, or 2) instead of all three sharing one leftover value.
Question 164 · SASS: Supercharged CSS with Variables and Nesting · hard
A student styling a train-ticket booking dashboard compiles this SCSS:
```scss
$color: blue;
.card {
$color: red;
&:hover {
color: $color;
}
.title {
color: $color;
}
}
.footer {
color: $color;
}
```
After compilation, what color is applied to `.card:hover`, `.card .title`, and `.footer`, respectively?
.card:hover and .card .title both render red, while .footer renders blue, because the $color assignment inside .card creates a variable scoped to that block and never overwrites the global $color.
Every selector — .card:hover, .card .title, and .footer — renders red, because assigning $color inside .card permanently overwrites the single global $color variable for the rest of the stylesheet.
Sass rejects the second $color: red declaration because a variable already exists in an outer scope, so all three selectors render blue using the original global value.
.footer renders red while .card:hover and .card .title render blue, because Sass looks up variables in the stylesheet's global scope first, ignoring any local reassignment inside nested rules.
Answer: A. .card:hover and .card .title both render red, while .footer renders blue, because the $color assignment inside .card creates a variable scoped to that block and never overwrites the global $color.
ExplanationSass gives every pair of curly braces its own variable scope. When $color: red; appears as the first statement inside .card { }, it does not overwrite the global $color: blue; instead it creates a new variable named $color that is local to the .card block (this has been Sass's behavior since version 3.4, precisely to stop nested reassignments from leaking outward — a reassignment only reaches the outer scope if you add the !global flag). Both &:hover and .title are nested inside .card, so when their color: $color; lines are compiled, Sass looks up the scope chain starting from where the rule is written and finds the local red before it ever needs to check the global scope. That's why the compiled CSS is .card:hover { color: red; } and .card .title { color: red; }. The .footer rule sits outside .card entirely, so its lookup for $color never sees the local red — it resolves directly to the untouched global value, giving .footer { color: blue; }. Only two of the three selectors end up red; the third stays blue.
Question 165 · Destructuring: Unpacking Objects and Arrays · hard
An IRCTC ticket-booking module in JavaScript destructures a booking object using nested patterns, renaming, and default values:
```js
const ticket = {
pnr: "2458671309",
passenger: { name: "Aditi", age: 14 },
fare: { base: 850, gst: 42 },
};
const {
passenger: { name: travellerName, age = 18 },
fare: { base, gst = 50, total = base + gst },
quota = "GENERAL",
} = ticket;
console.log(travellerName, age, total, quota);
```
What does this code print to the console?
Aditi 14 892 GENERAL
Aditi 18 892 GENERAL
Aditi 14 892 undefined
Aditi 14 850 GENERAL
Answer: A. Aditi 14 892 GENERAL
ExplanationWork through each binding in the pattern left to right, exactly as the JS engine does.
`passenger: { name: travellerName, age = 18 }` pulls `passenger.name` ("Aditi") into a differently-named variable `travellerName`, and pulls `passenger.age` (14) into `age`. Since `age` already exists on the object with value 14, its default of 18 is skipped entirely — defaults only fire when the extracted value is `undefined`, whether that's because the property is missing or because it's explicitly set to `undefined`.
`fare: { base, gst = 50, total = base + gst }` pulls `fare.base` (850) into `base`. `fare.gst` is 42, a real value, so the `= 50` default is skipped and `gst` becomes 42. `fare.total` does not exist anywhere on `ticket.fare`, so its default expression runs. Crucially, destructuring binds names in order, so by the time `total`'s default is evaluated, `base` (850) and `gst` (42) are already live local variables — `total` becomes 850 + 42 = 892, not some fixed fallback number.
`quota = "GENERAL"` sits at the top level of the pattern. `ticket` has no `quota` key at all, so accessing it also yields `undefined`, which triggers the default just as reliably as an explicitly-`undefined` property would — a missing key and an `undefined` value are indistinguishable to destructuring.
So the log statement prints `Aditi 14 892 GENERAL`. The variant with age 18 wrongly assumes defaults override any present value rather than only `undefined`; the variant with `undefined` for quota wrongly assumes defaults don't cover keys that are absent from the object; and the variant with total 850 wrongly treats the default expression as copying `base` alone instead of evaluating `base + gst`.
Question 166 · ES6 Classes: Object-Oriented Programming in JavaScript · hard
A Grade 9 student is prototyping a rupee-wallet balance counter in the browser and writes this class, then destructures two of its methods into standalone variables before calling them:
```js
class Counter {
count = 0;
increment() {
this.count++;
return this.count;
}
incrementArrow = () => {
this.count++;
return this.count;
};
}
const c = new Counter();
const { increment, incrementArrow } = c;
console.log(increment());
console.log(incrementArrow());
```
Tracing this exactly as the JavaScript engine executes it, what actually happens when this code runs?
Calling increment() first throws an uncaught TypeError before anything is printed, because destructuring c strips the method from its receiver, so this is undefined inside increment (class bodies run in strict mode); incrementArrow() on the next line is therefore never reached.
Both calls succeed and print 1 then 1, since every method declared inside a class body — plain method or arrow field alike — is auto-bound to its instance no matter how it is later invoked.
Since count lives on the shared instance c, both destructured calls succeed and print 1 then 2, with incrementArrow reading the value increment already wrote.
increment() runs fine and prints 1, but incrementArrow() throws, because arrow functions used as class fields lose access to this once they are separated from the object that declared them.
Answer: A. Calling increment() first throws an uncaught TypeError before anything is printed, because destructuring c strips the method from its receiver, so this is undefined inside increment (class bodies run in strict mode); incrementArrow() on the next line is therefore never reached.
ExplanationJavaScript class bodies always execute in strict mode, and regular methods like increment live on Counter.prototype — they are not tied to any particular instance until called with a receiver. Destructuring { increment, incrementArrow } = c pulls increment out as a bare function reference, severing its connection to c. Calling it as increment() supplies no receiver, so inside the function this is undefined (strict mode never falls back to the global object the way sloppy-mode calls do). The line this.count++ then tries to read .count off undefined, and JavaScript throws a TypeError immediately — while evaluating the argument to console.log, before console.log itself ever runs. That exception is uncaught, so execution halts right there; the second statement, which calls incrementArrow(), never executes at all. Had it been reached, incrementArrow would have worked correctly no matter how it was extracted, because arrow functions declared as class fields capture this lexically at the moment each instance is built — effectively giving every instance its own pre-bound copy of the function — unlike prototype methods, which only receive a this value when invoked through an explicit receiver such as c.increment(). This gap between auto-bound arrow class fields and receiver-dependent prototype methods is exactly why passing a class instance's methods around as callbacks (event handlers, .then() callbacks, array callbacks) is a classic source of "cannot read properties of undefined" bugs in real applications.
Question 167 · The Spread Operator (...): Copy, Merge, and Expand · hard
A student writing a trip-planner app traces this code by hand before running it:
```js
const trip = {
destination: "Manali",
costs: { hotel: 4000, food: 1500 },
days: [1, 2, 3]
};
const updatedTrip = { ...trip, destination: "Leh" };
updatedTrip.costs.hotel = 6000;
updatedTrip.days.push(4);
console.log(trip.costs.hotel, trip.days.length, trip.destination);
```
What does this `console.log` actually print, and why?
Because spread only copies primitives by value while nested objects and arrays stay shared by reference, the output is `6000 4 Manali` — the hotel cost and day count reflect the mutation, but destination is untouched.
Since `{...trip}` performs a full deep copy of every nested value, the output is `4000 3 Manali` — none of the mutations on updatedTrip's nested data reach trip at all.
As reassigning `destination` on updatedTrip also rewrites trip's own property, the output is `6000 4 Leh` — all three values change together.
Given that spread deep-copies nested objects but still shares nested arrays by reference, the output is `4000 4 Manali` — only the array mutation reaches trip.
Answer: A. Because spread only copies primitives by value while nested objects and arrays stay shared by reference, the output is `6000 4 Manali` — the hotel cost and day count reflect the mutation, but destination is untouched.
ExplanationThe spread operator `{...trip}` performs a **shallow** copy — it copies the object's own properties exactly one level deep. For a primitive like `destination` (a string), this means `updatedTrip` gets its own independent slot: overriding it to `"Leh"` never touches `trip.destination`, which stays `"Manali"`. But `costs` and `days` are reference types (an object and an array). Spread copies the *reference* to them, not the data they hold — so `updatedTrip.costs` and `trip.costs` point to the exact same object in memory, and `updatedTrip.days` and `trip.days` point to the exact same array. So when the code runs `updatedTrip.costs.hotel = 6000`, it mutates that one shared object, and `trip.costs.hotel` becomes `6000` too. Likewise, `updatedTrip.days.push(4)` mutates the shared array, so `trip.days.length` becomes `4`. The printed result is therefore `6000 4 Manali`. This is exactly why spread is often mistaken for a deep-copy tool — it isn't. To make nested data independent as well, you'd need to spread those levels explicitly, e.g. `{...trip, costs: {...trip.costs}, days: [...trip.days]}`.
Question 168 · Fetch API Deep-Dive: Making HTTP Requests · hard
A Grade 9 student in Bengaluru is building a browser tool that checks IRCTC PNR status using a REST API. Trace the code below for the case where the PNR does not exist and the server responds with HTTP status 404 and JSON body `{"error": "PNR not found"}`:
```js
fetch("https://api.example-irctc.in/pnr/8347512699")
.then(response => {
if (!response.ok) {
throw new Error(`Status: ${response.status}`);
}
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error("Error:", error.message));
```
What gets printed to the browser console?
It logs a TypeError from a rejected promise, since fetch() automatically rejects whenever the HTTP status represents a client or server error such as 404, the way axios does.
It logs "Error: Status: 404" to the console, since fetch() resolves for any HTTP response the server actually sends, so response.ok is false and the explicit throw inside the first .then() is caught by .catch().
It logs the parsed body {error: "PNR not found"} via the second .then(), because response.json() forwards a non-ok response's payload to the next handler regardless of response.ok.
The script crashes with an uncaught exception, because throwing an Error inside a .then() callback never creates a promise rejection -- only an explicit return Promise.reject() triggers .catch().
Answer: B. It logs "Error: Status: 404" to the console, since fetch() resolves for any HTTP response the server actually sends, so response.ok is false and the explicit throw inside the first .then() is caught by .catch().
ExplanationThe key nuance the fetch API tests here is what actually counts as a "failure" from the browser's point of view. fetch() only rejects its promise on network-level problems -- no internet connection, a DNS lookup failure, or a CORS block -- because those are cases where the browser never got a response at all. A 404 is not one of those cases: the server was reachable and sent back a complete, valid HTTP response (it just happens to be carrying a "not found" message). So fetch() resolves successfully, handing the first .then() a Response object where response.ok is false and response.status is 404.
Because response.ok is false, the code runs `throw new Error(\`Status: ${response.status}\`)`, producing an Error whose message is the string "Status: 404". A throw executed synchronously inside a .then() callback does not crash the script or need to be manually wrapped in Promise.reject() -- the Promise machinery automatically catches it and converts the chain's promise into a rejected one. That rejection skips the second .then(data => console.log(data)) entirely (so the raw JSON error body never reaches console.log) and lands directly in .catch(), where error.message is "Status: 404". The call `console.error("Error:", error.message)` then prints "Error: Status: 404" to the console.
The distractors describe real but mismatched behaviors: fetch() auto-rejecting on 4xx/5xx status codes is how libraries like axios behave, not native fetch, which is exactly why the manual `if (!response.ok) throw ...` check is standard practice; response.json() only ever runs if that check is passed and the value is explicitly returned, so it cannot be reached once the throw fires; and a synchronous throw inside a .then() callback is a well-defined part of the Promise spec that does produce a rejection, so the script does not crash uncaught.
Question 169 · WebSockets: Real-time Communication · hard
An IRCTC-style live train-status page keeps 5,000 users connected while a train is en route. With HTTP polling, each client's browser sends a fresh request every 3 seconds, and every request-response pair carries about 700 bytes of protocol overhead (headers, cookies, etc.), no matter how much actual status data is exchanged. With WebSockets, each client performs a single HTTP handshake that costs 400 bytes of one-time overhead, after which the server pushes a tiny frame (2 bytes of overhead per frame) only when the train's status genuinely changes — which happens about 4 times per minute on average. Over a 10-minute window, approximately how much overhead does a single client save by using WebSockets instead of polling?
About 139,520 bytes saved per client — 200 polling requests at 700 bytes each (140,000 bytes) minus one 400-byte WebSocket handshake plus 40 change-triggered frames at 2 bytes each (480 bytes).
Roughly 139,920 bytes saved per client, obtained by comparing the 140,000-byte polling total only against the 40 update frames' 80 bytes, without counting the WebSocket handshake's own 400-byte cost.
Close to 139,200 bytes saved per client, assuming the WebSocket connection still pushes a frame every 3 seconds like polling does, rather than only when the train's status actually changes.
Nearly 123,920 bytes saved per client, assuming every pushed update requires the connection to re-run its 400-byte handshake instead of reusing one persistent connection for all 40 updates.
Answer: A. About 139,520 bytes saved per client — 200 polling requests at 700 bytes each (140,000 bytes) minus one 400-byte WebSocket handshake plus 40 change-triggered frames at 2 bytes each (480 bytes).
ExplanationOver a 10-minute (600-second) window, HTTP polling issues one request every 3 seconds, so a single client sends 600 ÷ 3 = 200 requests. Each request-response pair carries about 700 bytes of overhead regardless of what data it carries, giving 200 × 700 = 140,000 bytes of pure overhead. A WebSocket client instead pays a single 400-byte handshake to open the connection, and after that the server only sends a tiny frame when the train's status genuinely changes — 4 times per minute for 10 minutes is 40 frames, each with 2 bytes of overhead, adding 80 bytes. Total WebSocket overhead is 400 + 80 = 480 bytes, so the saving is 140,000 − 480 = 139,520 bytes per client.
The wrong totals come from three specific misreadings of how WebSockets actually work. One drops the handshake's own 400-byte cost and compares polling only against the 80-byte frame total, which overstates the saving. Another assumes the server keeps pushing on the same fixed 3-second clock as polling instead of only when the status actually changes, undercounting how much is saved. The last, and most fundamental, mistake assumes every pushed update needs its own fresh handshake the way every polling request does — but the entire point of a WebSocket is that one handshake upgrades the connection a single time, and it then stays open for as many messages as needed without repeating that cost.
Question 170 · Service Workers: Offline and Progressive Apps · hard
A Grade 9 student builds a small PWA to practise checking IRCTC train seat availability offline. Their service worker is:
```js
const CACHE_NAME = 'irctc-tracker-v1';
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) =>
cache.addAll(['/', '/app.js', '/style.css'])
)
);
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
)
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => cached || fetch(event.request))
);
});
```
She edits `app.js` and bumps the constant to `CACHE_NAME = 'irctc-tracker-v2'`, deploys the new files, then simply presses Ctrl+R to refresh the open tab (she does not close it). The page still shows the old `app.js` behaviour. What is actually happening inside the browser at this moment?
The browser has already downloaded and installed the v2 service worker into the "waiting" state, but it cannot become the active worker while the tab remains open and controlled by the v1 worker; v2 only activates once every tab under v1's control is closed (or the page calls skipWaiting()/clients.claim())
The Fetch API's responses for app.js were stored in the browser's ordinary HTTP cache, which sits in front of the Cache Storage API and ignores CACHE_NAME entirely, so nothing served offline can change until that HTTP cache entry naturally expires
Because the fetch handler checks caches.match() before calling fetch(), the service worker permanently locks itself to whatever files were cached on the very first install and is architecturally incapable of ever fetching newer files again
The activate event's cleanup code runs and deletes the caches.keys() list before the install event's cache.open(CACHE_NAME).then(cache.addAll(...)) promise resolves, so the newly added v2 files are wiped out immediately and the browser silently falls back to serving v1 offline
Answer: A. The browser has already downloaded and installed the v2 service worker into the "waiting" state, but it cannot become the active worker while the tab remains open and controlled by the v1 worker; v2 only activates once every tab under v1's control is closed (or the page calls skipWaiting()/clients.claim())
ExplanationA service worker's own lifecycle is separate from the caches it manages. When the browser detects a byte-different sw.js, it installs the new version alongside the old one — the v2 worker runs its install handler, opens 'irctc-tracker-v2', and successfully caches the three files. But a freshly installed worker does not automatically take over: it sits in the "waiting" state as long as any open tab is still being controlled by the currently active worker (v1). A same-tab refresh does not remove that control, because the navigating tab is still counted as an existing client of v1 throughout the reload; the old worker keeps intercepting fetch events and keeps serving whatever caches.match() finds, which is still cache 'irctc-tracker-v1' since v1's activate handler never ran to delete it. Only when every tab controlled by v1 is fully closed (so zero clients remain) does the browser retire v1, activate v2, and let v2's activate handler delete the stale v1 cache — or the developer can force this immediately by calling self.skipWaiting() in the install handler and clients.claim() in the activate handler. The HTTP-cache explanation is wrong because Cache Storage (used here via the Cache API) is a separate, explicitly-versioned store that the developer fully controls with cache.delete(), independent of ordinary HTTP caching rules. The "permanent lock" claim is wrong because cache-first only means "prefer cache over network for a given request"; it says nothing about whether a new worker version can ever be installed with new files. The claim about activate deleting files before install finishes is wrong because the browser's service worker lifecycle strictly serializes these phases — activate for a given worker cannot begin until that same worker's install phase (and its waitUntil promise) has completed, and in this case v2's activate cannot run at all until v2 becomes the active worker in the first place.
Question 171 · Express.js: Building Web Applications · hard
A developer sets up this Express.js server, then sends a single GET request to `/order`:
```js
const express = require('express');
const app = express();
app.use((req, res, next) => {
console.log('A');
next();
});
app.get('/order', (req, res, next) => {
console.log('B');
next();
}, (req, res) => {
console.log('C');
res.send('Done');
});
app.use((req, res) => {
console.log('D');
});
app.listen(3000);
```
In what exact order do the letters get printed to the server console for this one request?
A, B, C — the path-less app.use() middleware runs first for every request, then the route's first handler runs and calls next(), which passes control to the second handler chained to the same route; that handler sends the response, so the request-response cycle ends there.
A, B, C, D — every middleware registered with app.use() runs for every incoming request regardless of whether a response has already been sent, so the final app.use() still fires after the route finishes.
A, D — app.get() only ever executes the first function passed to it, so calling next() inside that handler skips straight past any other function chained to the same route and hands control to the next app-level middleware.
A, B, D — calling next() inside a route handler always advances to the next app.use() middleware registered later in the file, not to the second handler chained to the same app.get() route.
Answer: A. A, B, C — the path-less app.use() middleware runs first for every request, then the route's first handler runs and calls next(), which passes control to the second handler chained to the same route; that handler sends the response, so the request-response cycle ends there.
ExplanationExpress processes a request by walking its middleware stack top to bottom, in the order things were registered — and next() only ever advances to whatever comes next in that same stack, not to some special "global middleware" category.
The first app.use() has no path, so it matches every request; it logs 'A' and calls next(). Control then reaches app.get('/order', ...), which was registered with two handler functions for the same route — Express treats these as a mini-chain executed in the order they were passed. The first one logs 'B' and calls next(); since there is nowhere else to go except the next function in that same route's chain, control passes to the second handler, which logs 'C' and calls res.send('Done'). Once res.send() fires, the response has been sent and the request-response cycle is complete — Express does not continue walking further down the stack afterward. The final app.use(), which would log 'D', was registered after the /order route and is only reached if a request falls through without a response being sent (for example, a request to some other path, or a handler that calls next() instead of ending the response). Because C ends the cycle with res.send() instead of calling next(), 'D' is never printed for this request.
So the console shows exactly A, B, C, in that order — nothing more, nothing less.
Question 172 · JWT Authentication: Secure Login Systems · hard
CampusPass, a login system built for a Delhi school's annual tech fest, issues each logged-in student a JWT signed with HS256. Priya, a Class 9 student, notices her token's middle segment is just base64url-encoded text — not encrypted — so she decodes it, edits `"role":"user"` to `"role":"admin"`, re-encodes only that segment, and pastes it back into the original token without touching the signature segment:
```
token = base64url(header) + "." + base64url(payload) + "." + signature
signature = HMAC-SHA256(base64url(header) + "." + base64url(payload), secretKey)
```
She submits this edited token to the server. Given the server correctly implements HMAC-SHA256 verification with the same secret key it used to sign the original token, what happens when it checks Priya's edited request?
The recomputed HMAC-SHA256 signature won't match Priya's old signature, since even a one-character change in the payload cascades into a completely different hash — so the server rejects the token.
Because HS256 tokens are encrypted with the server's private key, the tampered role value is invisible until decrypted, so the server unknowingly grants Priya admin access.
HMAC-SHA256 in a JWT only signs the header, not the payload, so editing the payload segment alone slips past signature verification undetected.
Base64url is itself a tamper-proofing scheme, so the decoder flags the edited payload as corrupted and blocks the request before signature verification even begins.
Answer: A. The recomputed HMAC-SHA256 signature won't match Priya's old signature, since even a one-character change in the payload cascades into a completely different hash — so the server rejects the token.
ExplanationHMAC-SHA256 in a JWT signs the exact string formed by concatenating the base64url-encoded header and payload with a dot between them — not just the header, and not the raw JSON before encoding. So when Priya edits the payload's role field and re-encodes just that segment, the string the server recomputes the signature over is now different from what was originally signed, and HMAC-SHA256 guarantees that even a single changed character produces an unrelated, unpredictable output. Since Priya left the original signature segment untouched, the server's freshly computed signature won't match it, and verification fails outright — the server rejects the token instead of granting admin access.
This also resolves two mix-ups baked into the wrong choices. First, HS256 tokens are not encrypted: base64url is a reversible encoding that anyone can decode without any key, which is exactly how Priya was able to read and edit the role field in the first place — there is no "private key decryption" step happening on read. Second, base64url decoding performs no tamper-detection of its own; a corrupted or edited payload still decodes cleanly into valid-looking JSON, so the decoder itself never flags anything as corrupted. The only mechanism actually catching Priya's edit is the HMAC-SHA256 signature comparison over the full header-and-payload string, which is why real systems must verify that signature on every request rather than trusting whatever role value sits in the decoded payload.
Question 173 · CORS: Enabling Cross-Origin Requests Safely · hard
An IRCTC-style ticket-booking frontend hosted at `https://book.irctc-demo.in` needs to fetch a logged-in user's bookings from a separate API server, so it sends the user's session cookie along with the request:
```js
fetch("https://api.irctc-demo.in/bookings", {
method: "GET",
credentials: "include" // attach the session cookie
});
```
The API server replies with these response headers:
```
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
```
What actually happens in the browser when this code runs?
The browser receives the server's response over the network but refuses to hand it to the JavaScript code, because the CORS specification forbids pairing a wildcard Access-Control-Allow-Origin with Access-Control-Allow-Credentials: true on a credentialed request; the server must echo back the exact calling origin instead of using *.
The fetch() call never leaves the browser at all, because the browser inspects the wildcard origin locally before sending anything and cancels the network request without ever contacting api.irctc-demo.in.
The response is delivered to the JavaScript exactly as sent, since Access-Control-Allow-Origin: * already grants every website full read access to the response regardless of whether the request carried cookies.
The browser silently strips the credentials: "include" option and resends the request without the session cookie, so the server treats the caller as logged out but the browser still lets the page read the returned data.
Answer: A. The browser receives the server's response over the network but refuses to hand it to the JavaScript code, because the CORS specification forbids pairing a wildcard Access-Control-Allow-Origin with Access-Control-Allow-Credentials: true on a credentialed request; the server must echo back the exact calling origin instead of using *.
ExplanationCORS is a restriction on what the browser lets JavaScript read, not a filter on what leaves the browser — so this GET request, cookie and all, genuinely reaches api.irctc-demo.in, and the server genuinely sends its response back across the network. The blocking happens afterward, when the browser checks the response headers before releasing the body to fetch()'s caller. Because credentials: "include" was used, the request is "credentialed," and the CORS spec explicitly disallows Access-Control-Allow-Origin: * in that case: a wildcard means "any website on the internet may read this," which is unsafe to combine with cookie-authenticated data like someone's train bookings. Access-Control-Allow-Credentials: true doesn't rescue the wildcard — it has no effect unless Access-Control-Allow-Origin names the exact origin. So the browser throws a CORS error and the fetch() promise rejects, even though the server did its job correctly. The fix on the backend is to read the incoming Origin header and reflect that exact value, e.g. Access-Control-Allow-Origin: https://book.irctc-demo.in, rather than using *.
Question 174 · XSS and CSRF: Protecting Against Common Attacks · hard
learnIndia.in, a CBSE exam-prep portal, protects its "Change Password" page with a CSRF token — a hidden form field whose value the server generates per session and validates on every password-change request:
```html
<form action="/change-password" method="POST">
<input type="hidden" name="csrf_token" value="a91f...e02c">
<input type="password" name="new_password">
</form>
```
A security audit also finds a stored XSS flaw in the site's "Class Discussion" comment box — it renders student comments back to the page without sanitizing embedded `<script>` tags:
```html
<div class="comment">{{ comment_text | safe }}</div>
```
Why does this stored XSS flaw let an attacker bypass the CSRF token protection and silently change a logged-in victim's password?
The Same-Origin Policy blocks any script — injected or not — from reading a CSRF token placed in the page by the server, so this stored XSS payload cannot actually defeat the password-change protection.
The injected script executes as part of the trusted learnIndia.in page in the victim's own browser, so it can read the CSRF token directly from the DOM and attach it to a forged password-change request, defeating the token check entirely.
CSRF tokens are tied to the visitor's IP address rather than to their session, so once the victim's browser has loaded the page once, any script on it can reuse the same token indefinitely without re-validation.
Stored XSS payloads run with elevated server-side privileges compared to ordinary page scripts, which lets them bypass the CSRF middleware checks that normal JavaScript on the page cannot bypass.
Answer: B. The injected script executes as part of the trusted learnIndia.in page in the victim's own browser, so it can read the CSRF token directly from the DOM and attach it to a forged password-change request, defeating the token check entirely.
ExplanationA CSRF token only works as a defense if an attacker's page cannot read its value — and the Same-Origin Policy is exactly what normally stops a script on evil-site.com from fetching learnIndia.in's HTML and pulling that hidden field out. Stored XSS breaks this guarantee in a way SOP was never designed to stop: the malicious `<script>` the attacker planted in the comment box is served back to the victim from learnIndia.in itself, so the browser treats it as first-party, same-origin code with full access to the page's DOM, cookies, and any active session. The script can simply read `document.querySelector('[name=csrf_token]').value`, then fire an authenticated `fetch('/change-password', {method:'POST', credentials:'include', body:...})` using that exact token — the server sees a perfectly valid, correctly-tokened request and processes it, even though the victim never intended to change their password. This is precisely why security engineers treat XSS as more dangerous than CSRF alone: a single unsanitized rendering point can quietly unlock every CSRF-protected form on the page. The token is not bound to the visitor's IP address (sessions and users routinely share or change IPs, especially on mobile data), and it does not expire or get reused merely because a script is present on the page — expiry is checked independently by the server. Nor does a stored payload gain any special "elevated" execution privileges; it runs with exactly the same DOM and network access as any other script the page would legitimately load, which is already more than enough to steal and replay the token.
Question 175 · Rate Limiting: Protecting APIs from Abuse · hard
An IRCTC-style ticket booking API protects its `/book-ticket` endpoint with token bucket rate limiting for each user account: bucket capacity = 20 tokens, refill rate = 4 tokens/second, and every accepted request consumes exactly 1 token. At time t = 0, a user's bucket is completely full (20/20 tokens) when a Tatkal-booking script fires a burst of 35 requests back-to-back, all effectively at t = 0.
How many of these 35 requests does the server accept, and how long after t = 0 must the script wait before its very next request can be accepted?
The bucket accepts the first 20 requests immediately, exhausting its capacity; the client can send its next request only after 0.25 seconds (250 ms), the time for one token to regenerate at 4 tokens/second.
The first 20 requests are accepted immediately, but the client must wait 5 seconds afterward — the time needed to refill the bucket back to its full 20-token capacity — before sending anything else.
Since the bucket started completely full, all 35 requests in the burst are accepted immediately, and the client can continue sending new requests with zero waiting time.
Because tokens refill at 4 per second, only 4 of the 35 requests are accepted in the first second, and the client must wait 1 full second before the next batch of 4 is allowed.
Answer: A. The bucket accepts the first 20 requests immediately, exhausting its capacity; the client can send its next request only after 0.25 seconds (250 ms), the time for one token to regenerate at 4 tokens/second.
ExplanationA token bucket holds a maximum of `capacity` tokens and gains `refillRate` new tokens every second; a request is accepted only if at least one token is present, in which case it consumes one token, and it is rejected (HTTP 429) otherwise. At t = 0 the bucket is full with 20 tokens, so exactly the first 20 requests in the burst each find a token available and are accepted, draining the bucket to 0. The remaining 15 requests arrive to an empty bucket and are rejected immediately — the bucket's capacity caps burst size, so it can never absorb all 35 at once. Once empty, the bucket needs only a single token — not a full refill to 20 — before the next request can succeed, and at a refill rate of 4 tokens/second that single token takes 1 / 4 = 0.25 seconds (250 ms) to arrive. Waiting for a full 20-token refill would take 20 / 4 = 5 seconds, but that overshoots what's needed: the very next request only requires 1 token, not a completely restocked bucket. The 4-accepted-then-1-second-wait picture instead describes a fixed-window counter that resets once per second, not a token bucket, which is exactly why it's a classic point of confusion — a token bucket lets bursts through up to its capacity and then throttles smoothly token-by-token, rather than admitting a flat 4 requests every clock second.
Question 176 · Database Indexing: Making Queries Lightning Fast · hard
An IRCTC database table holds exactly 1,048,576 PNR records, sorted by PNR number and organized with a B-tree index — a balanced index structure where every comparison against a stored key lets the search discard half of the currently remaining candidate records, exactly like a binary search on a sorted list. Without this index, the reservation system would have to scan the table row by row, checking up to all 1,048,576 records in the worst case to locate one specific PNR. Using the B-tree index instead, what is the maximum number of comparisons needed, in the worst case, to locate any single PNR record among the 1,048,576 stored?
20 comparisons, because repeatedly halving 1,048,576 exactly 20 times (since 2^20 = 1,048,576) narrows the candidates down to the one matching record
1,048,576 comparisons, because an index still has to compare the search key against every stored record before it can be sure of a match
524,288 comparisons, because the index only cuts the candidate records in half once before it must fall back to scanning the remaining half one by one
21 comparisons, because one extra comparison is needed after the candidate set has already been narrowed down to a single record
Answer: A. 20 comparisons, because repeatedly halving 1,048,576 exactly 20 times (since 2^20 = 1,048,576) narrows the candidates down to the one matching record
ExplanationEach comparison in a balanced B-tree (or binary search) index eliminates half of the remaining candidate records, so the search size shrinks as 1,048,576 → 524,288 → 262,144 → 131,072 → ... → 2 → 1. Counting those halving steps: since 2^20 = 1,048,576 exactly, it takes precisely 20 halvings — and therefore 20 comparisons — to narrow the full table down to the single matching PNR record. An unindexed linear scan has no such shortcut: it must check records one at a time, so its worst case is the full 1,048,576 comparisons, not a reduced figure. The 524,288 figure describes the average case for an unindexed linear scan (checking half the table before finding a match on average) — it is not how an index behaves, and confusing the two is a common mix-up between "index lookup" and "average-case brute-force search." The 21 figure is an off-by-one slip: once repeated halving has narrowed the candidates down to a single record, that record has already been found — no extra comparison beyond the 20 halving steps is needed. This logarithmic-versus-linear gap is exactly why indexes matter on large real-world tables: an IRCTC PNR lookup or a UPI transaction search that would take over a million comparisons unindexed can complete in about 20 comparisons with a proper index, which is the practical difference between an instant response and a system that visibly lags.
Question 177 · SQL Joins: Combining Data from Multiple Tables · hard
An IRCTC-style ticket booking system has two tables. BOOKINGS records train ticket bookings, and PAYMENTS records UPI payment transactions, both linked by passenger_id:
BOOKINGS
| booking_id | passenger_id |
|---|---|
| B101 | P1 |
| B102 | P1 |
| B103 | P2 |
| B104 | P3 |
PAYMENTS
| payment_id | passenger_id |
|---|---|
| U201 | P1 |
| U202 | P1 |
| U203 | P2 |
A developer runs:
```sql
SELECT *
FROM BOOKINGS
INNER JOIN PAYMENTS
ON BOOKINGS.passenger_id = PAYMENTS.passenger_id;
```
How many rows does this query return?
4 rows, since INNER JOIN keeps every row from BOOKINGS and fills in NULL values wherever a passenger_id has no matching row in PAYMENTS.
5 rows, since passenger P1's two bookings each pair with both of P1's payments (2 × 2 = 4 rows), P2's one booking pairs with P2's one payment (1 row), and P3's booking is dropped because P3 has no row in PAYMENTS.
3 rows, since only bookings B101, B102, and B103 have a matching passenger_id in PAYMENTS, and each matched booking produces exactly one row in the result.
7 rows, since INNER JOIN appends the rows of PAYMENTS to the rows of BOOKINGS, giving a combined table with 4 + 3 = 7 rows.
Answer: B. 5 rows, since passenger P1's two bookings each pair with both of P1's payments (2 × 2 = 4 rows), P2's one booking pairs with P2's one payment (1 row), and P3's booking is dropped because P3 has no row in PAYMENTS.
ExplanationAn INNER JOIN doesn't match one row to one row — for each value of passenger_id, it pairs every matching row in BOOKINGS with every matching row in PAYMENTS, so the row count multiplies within each group rather than adding. P1 appears twice in BOOKINGS (B101, B102) and twice in PAYMENTS (U201, U202), so the join produces 2 × 2 = 4 combined rows for P1: (B101,U201), (B101,U202), (B102,U201), (B102,U202). P2 appears once in each table, contributing 1 × 1 = 1 row. P3 appears in BOOKINGS but has no row in PAYMENTS at all, so INNER JOIN — which only keeps rows with a match on both sides — drops B104 entirely rather than keeping it with NULLs, since that padding behaviour belongs to a LEFT JOIN, not an INNER JOIN. Adding the groups together: 4 + 1 + 0 = 5 rows total. The trap here is treating a join like a simple lookup that returns at most one row per input row; whenever the join key has duplicates on both sides, the result set grows by multiplication, which is exactly why real-world queries on tables with repeated foreign keys can unexpectedly explode in size.
Question 178 · Linear Regression: Predicting Values from Data · hard
A student project team at a Bengaluru school is training a simple linear regression model to predict CBSE exam marks from hours studied, using data collected from 4 classmates:
| Hours studied (x) | Marks scored (y) |
|---|---|
| 2 | 40 |
| 4 | 45 |
| 6 | 60 |
| 8 | 65 |
They fit a least-squares regression line y = bx + a to this data, where b = (nΣxy − ΣxΣy) / (nΣx² − (Σx)²) and a = ȳ − b·x̄. According to this fitted line, what marks does the model predict for a classmate who studies for 10 hours?
75 marks
70 marks
52.5 marks
81.25 marks
Answer: A. 75 marks
ExplanationFor this data, n = 4, Σx = 2+4+6+8 = 20, Σy = 40+45+60+65 = 210, Σxy = (2·40)+(4·45)+(6·60)+(8·65) = 80+180+360+520 = 1140, and Σx² = 4+16+36+64 = 120. The slope is b = (4·1140 − 20·210) / (4·120 − 20²) = (4560 − 4200) / (480 − 400) = 360/80 = 4.5. The mean values are x̄ = 20/4 = 5 and ȳ = 210/4 = 52.5, so the intercept is a = ȳ − b·x̄ = 52.5 − 4.5·5 = 52.5 − 22.5 = 30, giving the fitted line y = 4.5x + 30. Substituting x = 10 gives y = 4.5(10) + 30 = 45 + 30 = 75 marks.
70 marks comes from extending only the slope between the last two points, (6, 60) and (8, 65) — a slope of (65−60)/(8−6) = 2.5 marks per hour — two more hours past x = 8 gives 65 + 2.5×2 = 70. This treats a local trend between adjacent points as if it were the overall regression slope, but the least-squares line is fitted using every point in the dataset, not just the two most recent ones.
52.5 marks is simply ȳ, the average of all four marks scored, which ignores the study-hours input entirely. Regression predicts a value based on x; using the historical average of y regardless of x defeats the purpose of fitting a line in the first place.
81.25 marks comes from assuming marks are directly proportional to hours studied — scaling the (8, 65) point up as (10/8)×65 = 81.25 — which wrongly forces the line through the origin (a = 0) instead of using the fitted intercept of 30. Real least-squares lines from noisy data almost never pass exactly through (0, 0).
Question 179 · Feature Engineering: Creating Better Input Data · hard
A real-estate startup is training a model to predict plot prices in a Bengaluru suburb. It first feeds the model two raw features, `length` and `breadth`, separately — but predictions are poor. A data scientist then engineers one new feature, `area = length × breadth`, and reruns the same simple model. Here is the training data:
| Plot | Length (m) | Breadth (m) | Price (₹ lakh) |
|------|-----------|-------------|-----------------|
| A | 10 | 60 | 30 |
| B | 20 | 30 | 30 |
| C | 30 | 40 | 60 |
| D | 15 | 20 | 15 |
Compute the area for each plot and compare it (and the raw length and breadth values) against price. Which statement correctly explains why the engineered `area` feature works so much better than the raw `length` or `breadth` features here?
Length alone is sufficient because plots C and D show that a longer length always means a higher price, so engineering an area feature adds nothing new here.
Breadth alone is the better single feature, since plot A has the largest breadth (60 m) and a higher price than plot D, so breadth already captures the price trend without any engineering.
Area is exactly proportional to price in all four rows — 600, 600, 1200, and 300 sq. m give ₹30, 30, 60, and 15 lakh, a constant ₹5,000 per sq. m — while sorting the plots by length alone (10 → 15 → 20 → 30 m) gives prices that go 30 → 15 → 30 → 60 lakh, falling and then rising, so length by itself isn't even a consistent (monotonic) predictor.
Area cannot be a useful feature here because all four plots have different areas, so a feature like perimeter (2 × (length + breadth)) should be engineered instead, since it changes smoothly across the four plots.
Answer: C. Area is exactly proportional to price in all four rows — 600, 600, 1200, and 300 sq. m give ₹30, 30, 60, and 15 lakh, a constant ₹5,000 per sq. m — while sorting the plots by length alone (10 → 15 → 20 → 30 m) gives prices that go 30 → 15 → 30 → 60 lakh, falling and then rising, so length by itself isn't even a consistent (monotonic) predictor.
ExplanationComputing area = length × breadth for each row gives A: 10×60=600, B: 20×30=600, C: 30×40=1200, D: 15×20=300 sq. m. Dividing price by area: 30/600 = 0.05, 30/600 = 0.05, 60/1200 = 0.05, 15/300 = 0.05 lakh per sq. m — exactly ₹5,000/sq. m every single time, with zero error. So price = 0.05 × area holds perfectly across all four plots, meaning a model trained on `area` alone can fit this data with a perfect straight line and no residual error.
Now check the raw features. Sorting by length: A(10 m, ₹30L) → D(15 m, ₹15L) → B(20 m, ₹30L) → C(30 m, ₹60L). As length rises from 10 to 15 m, price falls from 30 to 15 lakh; then from 15 to 20 m it rises back to 30 lakh; then to 60 lakh at 30 m. That fall-then-rise pattern means no single linear rule (or even a monotonic one) connects length to price — the same is true of breadth, which runs 60, 30, 40, 20 across A, B, C, D and shows no consistent trend with price either (e.g., breadth drops from 40 to 20 between C and D while price also drops, but breadth rises from 20 to 60 between D and A while price only rises to 30, not proportionally).
This is the core lesson of feature engineering: the *information* needed to predict price (physical area) is already implicit in the two raw columns, but a simple model can't reconstruct a multiplicative relationship like length × breadth on its own from two separate linear inputs. Engineering the product explicitly hands the model the exact quantity it needs, turning a messy, non-monotonic two-feature problem into a perfect one-feature linear fit. Claiming area "cannot be used" because areas differ across plots misreads the table — A and B in fact share the same area (600 sq. m) and, correspondingly, the same price (₹30 lakh), which is itself evidence that area, not length or breadth, is the quantity driving price.
Question 180 · Confusion Matrix: Evaluating Classification Models · hard
A bank's UPI fraud-detection model is tested on 10,000 real transactions. The confusion matrix below shows the results:
| | Predicted: Fraud | Predicted: Legitimate |
|------------------------|:----------------:|:----------------------:|
| **Actual: Fraud** | TP = 30 | FN = 20 |
| **Actual: Legitimate** | FP = 150 | TN = 9800 |
The bank's risk team wants to know: of all the transactions this model flags as fraud, what fraction are actually fraudulent — and what does that number mean for how much the fraud-review team should trust each alert?
Precision ≈ 17% — of every transaction the model flags as fraud (TP + FP = 180 total alerts), only 30 are genuine fraud, so roughly 5 out of 6 alerts sent to the review team are false alarms on legitimate transactions.
Precision ≈ 60% — since the model catches 30 out of the 50 actual fraud cases in the dataset, most fraud alerts issued by the model correspond to real, genuine fraud that customers should be warned about.
Precision ≈ 98% — because the model's overall accuracy across all 10,000 transactions is very high, the review team can trust that most fraud alerts it raises are correctly identifying real fraud.
Precision ≈ 83% — since 150 of the 180 flagged transactions are false alarms, this false-alarm share itself represents the model's precision, meaning the vast majority of alerts are trustworthy.
Answer: A. Precision ≈ 17% — of every transaction the model flags as fraud (TP + FP = 180 total alerts), only 30 are genuine fraud, so roughly 5 out of 6 alerts sent to the review team are false alarms on legitimate transactions.
ExplanationPrecision answers a specific question: "Of everything the model called positive (fraud), how much actually was?" It is defined as TP / (TP + FP), using only the flagged transactions — not the whole dataset and not the actual fraud count.
From the matrix, TP = 30 and FP = 150, so the model raised TP + FP = 180 total fraud alerts. Precision = 30 / 180 = 0.1667, which rounds to 17%. In plain terms: for every 6 alerts the fraud-review team receives, only about 1 is real fraud — the other 5 are legitimate UPI transactions wrongly flagged, which matters a lot for a team that has limited hours to manually review each alert.
The 60% figure is Recall (TP / (TP+FN) = 30/50), a different question entirely — "of all the real fraud that happened, how much did the model catch?" A model can have decent recall while still burying its review team in false alarms, which is exactly this case.
The 98% figure is overall Accuracy ((TP+TN)/10000 = 9830/10000), inflated by the 9800 correctly-ignored legitimate transactions. With fraud being rare (only 50 out of 10,000 cases), a model can score high accuracy while being nearly useless at its actual job of producing trustworthy fraud alerts — a classic accuracy-paradox trap with imbalanced data.
The 83% figure comes from computing FP / (TP+FP) = 150/180 instead of TP / (TP+FP) — swapping the numerator. That ratio is the false-alarm rate among alerts, the complement of precision, not precision itself; reading it as "mostly trustworthy" inverts what the number actually says.