PYTHON / PANDAS
Series and DataFrame fundamentals
Build Series and DataFrames from scratch, read their index and dtype metadata, and predict how label alignment fills or drops values.
What you will learn
- Create a Series with explicit index labels and a name, then read .dtype and .shape
- Build a DataFrame from a dict of Series and explain the resulting index
- Predict where NaN appears when two differently indexed Series are combined
- Distinguish a one-dimensional Series from a one-column DataFrame
Understanding Series and DataFrame fundamentals
A Series is two arrays travelling together: the values and the index. The values live in a single typed buffer, which is why a Series has one dtype rather than one type per element, and the index is a real labelled axis, not decoration. Because the labels are part of the object, `temps["tue"]` is a lookup in the index, not a position offset, and `temps.name` survives most operations so the Series can later become a named DataFrame column.
A DataFrame is a collection of Series that all share one row index. Each column keeps its own dtype, so one column can be float64 while its neighbour is int64 or object; there is no single element type for the table. When you construct a DataFrame from a dict of Series, pandas does not stack them positionally, it takes the union of their indexes and reindexes every column onto that union, inserting NaN wherever a column had no value for a label.
That same reindexing rule governs arithmetic: `a + b` matches labels, not positions, so reordering a Series changes nothing about the result while renaming a label changes everything. Labels present in only one operand produce NaN, which is also why an int64 column silently becomes float64 after alignment, since NaN is a float. Once you internalise that the index is the join key for every binary operation, most surprising NaN columns stop being surprising.
import pandas as pd
temps = pd.Series([18.5, 21.0, 19.75], index=["mon", "tue", "wed"], name="celsius")
print(temps)
print(temps.dtype, temps.shape, temps.name)
rain = pd.Series({"tue": 0.0, "wed": 4.2, "thu": 1.1}, name="mm")
week = pd.DataFrame({"celsius": temps, "mm": rain})
print(week)
print(week.dtypes)A Series is values plus a labelled index, a DataFrame is Series sharing one index, and every combination of them matches on labels rather than position.
Worked examples
Labels win over positions
Adding two Series whose indexes are reversed gives a different answer than adding the raw arrays.
import pandas as pd
a = pd.Series([1, 2, 3], index=["x", "y", "z"])
b = pd.Series([10, 20, 30], index=["z", "y", "x"])
print(a + b)
print(a.to_numpy() + b.to_numpy())Example explained
Line 1`a + b` pairs label x with label x, so 1 meets 30 and the result at x is 31.
Line 2The result index is the sorted union of both indexes, which is why the order is x, y, z and not z, y, x.
Line 3`to_numpy()` throws the labels away, so numpy pairs by position and 1 meets 10 instead.
Line 4Both results are int64 here because no label was missing, so no NaN forced a float upcast.
A column is a Series
Pulling one column out of a DataFrame yields a one-dimensional Series that keeps the frame's index and its own name.
import pandas as pd
rows = [{"city": "Oslo", "pop": 709000}, {"city": "Bergen", "pop": 289000}]
df = pd.DataFrame(rows, index=["a", "b"])
col = df["pop"]
print(type(col).__name__, col.index.tolist(), col.dtype)
print(df.shape, len(df.columns))
print(df["city"].str.upper())Example explained
Line 1A list of dicts becomes one row per dict, with dict keys turned into column names.
Line 2`df["pop"]` returns a Series carrying the frame's index ['a', 'b'], not a fresh 0-based one.
Line 3`df.shape` is (rows, columns), so a DataFrame is always two-dimensional even with one column.
Line 4`.str.upper()` works because `df["city"]` is a 1-D object Series, and it keeps the name `city`.
Important notes
Index labels are not required to be unique; aligning two Series that both repeat a label produces a cartesian expansion of the matches, so the result can be longer than either input.
Alignment that introduces even one NaN upcasts an int64 column to float64, because NaN is a floating point value.
Common mistakes
Assuming `a + b` lines values up by position: if the two Series were built or ordered differently you get silently wrong sums plus NaN rows, with no error to warn you.
Passing plain lists of unequal length to `pd.DataFrame`, as in `pd.DataFrame({"a": [1, 2, 3], "b": [1, 2]})`, which raises `ValueError: All arrays must be of the same length`; dicts of Series pad with NaN instead because they align on labels.
Treating `df["pop"]` and `df[["pop"]]` as the same thing: the first is a 1-D Series with `.str` and scalar-friendly behaviour, the second is a 2-D DataFrame, so accessor calls on it fail with an AttributeError.
Try it yourself
Change, predict, then run
Build one Series of prices indexed by the product codes ["p1", "p2", "p3"] and one Series of stock counts indexed by ["p2", "p3", "p4"], write down where you expect NaN before running anything, then combine them into a single DataFrame and check your prediction against the printed frame and `.dtypes`.
Open the Python workspaceCheck your understanding
`a = pd.Series([1, 2], index=["a", "b"])` and `b = pd.Series([10, 20], index=["b", "a"])`. What does `a + b` produce?
- a is 21 and b is 12, because matching happens on index labels
- a is 11 and b is 22, because the values are added in the order they were written
- NaN for both labels, because the two indexes are in different orders
- A ValueError, because the indexes are not identically ordered
Show answer
Binary operations reindex both operands onto the union of their labels, so label a pairs 1 with 20 and label b pairs 2 with 10. The positional answer 11 and 22 is what numpy arrays would give, but a Series carries its index into the operation, so order of construction is irrelevant. Nothing is NaN and nothing raises, because both labels exist on both sides.