The Marks Sheet That Silently Attached Data to the Wrong Students
Ms. Rao builds her Class XII internal marks sheet as a DataFrame named marks from a list of 40 dictionaries, so the index is the default 0 to 39 and the columns are Roll, Name, Math, CS and Eng. Roll numbers 7 and 23 were absent, so their Math and CS cells hold NaN. She then creates practical = pd.Series([18, 17, 19], index=[1, 2, 3]) and runs marks['Practical'] = practical, intending these marks for Roll 1, 2 and 3, which sit at positions 0, 1 and 2. No error appears, but the department notice later shows the practical marks attached to the wrong three students and NaN in every other row. Next, marks[marks['Math'] > 80 and marks['CS'] > 80] raises a ValueError. She then writes sub = marks[0:10], adds sub['Grade'] = 'A' and receives a SettingWithCopyWarning, although Grade does appear in sub. Finally she plans to replace every NaN with 0 so that the Total column and the class average come out clean. The sheet must be defensible if a parent demands the result.
1. Explain why the practical marks landed on the wrong students and how Ms. Rao should have assigned them.
Read the worked answer
Pandas aligns an assigned Series by index label, not by position. practical carries the labels 1, 2 and 3, and marks has labels 0 to 39, so the value 18 went to the row labelled 1 (Roll 2), 17 to the row labelled 2 (Roll 3) and 19 to the row labelled 3 (Roll 4). Roll 1, at label 0, received nothing. Labels that do not exist in the frame would be ignored, and labels with no value become NaN - which is why 37 rows are blank. No warning is raised because pandas assumes the alignment is what you wanted.
Two correct fixes:
- Pass plain data instead of a labelled Series:
marks['Practical'] = [18, 17, 19] + [np.nan]*37- a list is assigned positionally and must match the row count. - Keep the Series but give it the frame's own labels:
practical = pd.Series([18,17,19], index=marks.index[:3]), or better, build a full-length Seriespd.Series(data, index=marks.index).
The lesson is that index alignment is a feature, not a bug; it silently protects against order changes but only if the labels are meaningful. When they are not, positional assignment or an explicit .loc write per roll number is safer.
2. Why does `marks['Math'] > 80 and marks['CS'] > 80` fail, and what is the correct expression?
Read the worked answer
Each comparison produces a boolean Series of 40 values, not a single True or False. Python's and needs one truth value from each operand, so it asks the first Series whether it is true - and pandas refuses, raising ValueError: The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all(). Python's and also short-circuits, which cannot work element-wise on 40 rows at once.
The element-wise operator is &, with each comparison wrapped in its own parentheses, because & binds tighter than >:
marks[(marks['Math'] > 80) & (marks['CS'] > 80)]
Omission of the brackets makes Python try to evaluate 80 & marks['CS'] and the expression breaks or gives nonsense. The same rules apply to | for OR and ~ for NOT; you must never use and, or or not on Series. To also treat absences correctly, add a not-null test: marks[(marks['Math'] > 80) & (marks['CS'] > 80) & marks['Math'].notna()], since NaN comparisons return False and would silently exclude the absentees from any count she reports.
3. Explain the SettingWithCopyWarning and advise whether Ms. Rao should fill the NaN marks with 0.
Read the worked answer
marks[0:10] is a slice, and pandas cannot tell whether it returned a view on marks or a copy. Writing sub['Grade'] = 'A' is chained assignment - a get followed by a set - so pandas warns that the change may not reach the parent frame. It is a warning, not an error, which is why Grade appears in sub but the parent is unreliable. The correct pattern is to make the intent explicit: sub = marks.iloc[0:10].copy() (or .loc[...] .copy()), then assign; for a single conditional write on the parent, use marks.loc[marks['Math'] > 80, 'Grade'] = 'A' directly, which never chains.
On the NaN question, filling with 0 is the wrong default. It records that 40 students wrote a test and three genuinely scored zero, which drags the class mean down and misrepresents an absence as a performance. marks['Math'] + marks['CS'] already yields NaN for those rows, and marks['Total'].mean() skips NaN by default (skipna=True), giving a fair average over 38 students. If a numeric Total is needed for display or export, fill only a separate column (.fillna(0)) and state in the report that absentees are excluded from averages, not scored as zero.