1.4.6 · D5Python & Scientific Computing

Question bank — Pandas DataFrames and Series basics

1,298 words6 min readBack to topic

For hands-on drills see 1.4.07-Pandas-data-cleaningand-manipulation; for the array layer underneath see 1.4.01-NumPy-arrays-and-vectorization.

True or false — justify

A Series is just a Python dictionary with a nicer print.
False — a dict has no fixed order guarantee across all operations and no dtype; a Series keeps a numpy-backed, single-dtype array plus an ordered index, so it supports positional access and vectorised math a dict cannot.
Every column in a DataFrame must have the same dtype.
False — each column is its own Series so columns may differ (int, float, object). Only within a single column must the dtype be uniform.
Two Series added together line up by position, like numpy arrays.
False — they align by index label, not position. Non-matching labels produce NaN, so s1 + s2 can be longer than either input and full of holes if labels differ.
df['score'] returns a copy you can safely modify.
False — column selection usually returns a view onto the same data; assigning into it may or may not write back, which is exactly why pandas raises the SettingWithCopyWarning.
The row index of a DataFrame is always 0, 1, 2, ….
False — that default RangeIndex only appears when you supply no index. It can be strings, dates, or even duplicated values.
len(df) gives the number of columns.
False — len(df) is the number of rows (the length of the index). Use df.shape[1] or len(df.columns) for columns.
Filtering with a boolean Series keeps the rows where the condition is False.
False — it keeps rows where the boolean is True; the alignment is by index, and rows with NaN in the condition are treated as False.
Sorting a Series scrambles the link between labels and values.
False — the whole point is that labels stick. After sort_values() each value still carries its original index label to its new position.

Spot the error

df[1] to grab the second row.
Error — single-bracket integer indexing looks for a column named 1, not row position 1. For the second row use df.iloc[1].
df.loc[0:2] returns two rows (positions 0 and 1), just like list slicing.
Error — .loc slicing is label-based and inclusive of the end label, so df.loc[0:2] returns three rows (labels 0, 1, 2), unlike .iloc[0:2] which is exclusive.
df.iloc['s2', 'score'] to read Bob's score when the index is ['s1','s2','s3'].
Error — .iloc only accepts integer positions, not labels. Use df.loc['s2','score'] or df.iloc[1, 1].
s['Feb'] = 18000 where s has integer index but you meant month names.
Error — with an integer index, s['Feb'] would try to add a new label 'Feb', silently growing the Series instead of updating an existing month.
pd.Series([1,2,3], index=['a','b']).
Error — the index has fewer labels than values (2 vs 3). Pandas raises a length-mismatch ValueError; index and data must be the same length.
df[df['score'] > 80 and df['grade'] == 'A'].
Error — Python's and can't operate element-wise on Series. Use & with parentheses: df[(df['score'] > 80) & (df['grade'] == 'A')].
df.mean() on a DataFrame with a text column throws an error.
Error — by default mean skips non-numeric columns and just averages the numeric ones; it does not crash on the text column.

Why questions

Why does df['name'] return a Series but df[['name']] return a DataFrame?
A single string selects one column, so pandas hands back the 1-D Series; a list of names (even a list of one) asks for a sub-table, so you get a 2-D DataFrame.
Why do we ever need both .loc and .iloc — isn't one enough?
They answer different questions: .loc means "the row called X" (semantic labels), .iloc means "the row at position X" (order). When labels are non-integer these are unambiguous, but when the index is integers the two can disagree, so both must exist.
Why does adding two Series with partly-overlapping labels create NaN?
Alignment fills in every label present in either Series; where one side has no value for a label there is nothing to add, so pandas marks it missing (NaN) rather than guessing a zero.
Why is building a DataFrame from a dict-of-lists so common?
It mirrors "column-oriented" thinking — each key is a variable/feature and its list is that column's values, which is exactly the mental model used in 2.1.03-Feature-engineering-basics.
Why does list-of-dicts sometimes produce NaN columns?
Pandas takes the union of all keys across records as columns; any record missing a key gets NaN in that slot, so gaps in the source data surface as missing cells.
Why does chained assignment like df[df.score>80]['grade'] = 'A' fail to update df?
The first bracket makes a temporary filtered copy; the assignment edits that throwaway object, not df. Use a single .loc call: df.loc[df.score>80, 'grade'] = 'A'.
Why keep labels attached to data at all, instead of relying on row order?
So sorting, filtering, joining and merging can re-align by identity, eliminating off-by-one bugs — the value never loses track of which row it belongs to.

Edge cases

What is the dtype of a Series holding [1, 2, None]?
float64None/NaN cannot live in an integer array, so pandas up-casts the whole column to float so it can represent the missing value.
What does pd.Series([]) (empty) have for a dtype?
An empty Series has no data to infer from, so it defaults to object (older pandas) or raises a warning suggesting you specify dtype= explicitly.
If two rows share the same index label, what does df.loc['dup'] return?
A sub-DataFrame of all matching rows, not a single row — duplicate labels are allowed, so .loc can return more than one row for one label.
What happens to a boolean filter when the condition column contains NaN?
Comparisons with NaN yield False (actually the mask holds NaN→treated as False), so those rows are dropped — silently excluding missing-data rows unless you handle them first.
df.drop('col', axis=1) — does the original df change?
No — drop returns a new object by default (inplace=False); the original keeps its column unless you reassign or pass inplace=True.
What does df['col'] return if 'col' is not a column name?
A KeyError — unlike a dict's .get, single-bracket column access has no silent default and raises immediately. (See the 'col' gotcha of confusing string keys with variables.)
What is the shape of a DataFrame with 0 rows but 3 defined columns?
(0, 3) — the column structure exists even with no data, which is why appending rows or reading an empty CSV still yields usable, typed columns.
Recall One-line rescue for the top three traps

Alignment ::: Series/DataFrame math lines up by index label, never by position. loc vs iloc ::: .loc = by label (end-inclusive), .iloc = by integer position (end-exclusive). Chained assignment ::: Do writes in a single .loc[rows, cols] = value, never df[...][...] = value.