Exercise 2: Why .loc[2] and .iloc[2] Can Diverge, and When to Prefer Each — Possible Solution ==================================================================== WHAT EACH ONE ACTUALLY SELECTS BY, PER THIS CHAPTER ------------------------------ Per this chapter's own compare-table: ".loc selects by Label (the index value itself)... .iloc selects by Integer position (0-based, like a list)." These are fundamentally different concepts that simply happen to coincide in one common case. WHY THEY MATCH ON A FRESH DATAFRAME ------------------------------ Per this chapter's own warn-box, "for a fresh DataFrame with a default 0, 1, 2, ... index, .loc[2] and .iloc[2] happen to return the same row." A freshly created DataFrame is automatically given a default index that literally counts 0, 1, 2, 3... in the same order as the rows' own physical positions. In that specific situation, the row whose LABEL is 2 (what .loc looks for) and the row at POSITION 2 (what .iloc looks for) are, by coincidence of the default index's own construction, always the exact same row. WHY THEY DIVERGE LATER ------------------------------ Per this chapter, this "hides the difference until, later, some rows get filtered or sorted and the index labels no longer line up with position. At that point .loc[2] and .iloc[2] can silently return two completely different rows." Filtering a DataFrame (per this chapter's own boolean-filtering section) removes some rows but keeps the SURVIVING rows' original labels unchanged — so a row originally labeled 5 that survives a filter still carries label 5, even though it might now be sitting at physical position 2 within the filtered result. Sorting has the same effect: labels travel with their rows, but physical position changes. Once that happens, .loc[2] still means "the row labeled 2" (which might no longer exist, or might be somewhere else entirely), while .iloc[2] still means "whatever row is now third in physical order" — two genuinely different, unrelated things that simply used to overlap by coincidence. THE GENERAL RULE FOR WHICH TO PREFER ------------------------------ Per this chapter's own closing recommendation, "preferring .loc for label-based work and .iloc only when position genuinely is what's meant avoids this trap entirely." In practice: if the intent is "give me the row I know by this specific identifier," use .loc, since that intent survives filtering/sorting correctly. If the intent is genuinely positional ("give me the first row," "give me the last three rows"), .iloc is the correct, intentional choice — the danger is only in using .iloc out of habit when .loc's own label-based semantics are what's actually meant. WHY THIS WORKS AS AN ANSWER ------------------------------ It explains precisely why the two accessors coincide on a fresh, default-indexed DataFrame (the default index happens to equal physical position), why filtering/sorting breaks that coincidence (labels persist with their rows while position changes), and states the chapter's own general rule for choosing between them going forward.