Series & DataFrames
Pandas gives you two objects. A Series is a one-dimensional labelled array — think of one column. A DataFrame is a table of Series that share a common index — think of a spreadsheet sheet with named columns.
Creating them
import pandas as pd
s = pd.Series([10, 20, 30], index=["a", "b", "c"])
df = pd.DataFrame({
"name": ["Asha", "Ravi", "Meera"],
"city": ["Delhi", "Pune", "Delhi"],
"sales": [120000, 95000, 143000],
})
df.head()
df.shape, df.columns, df.dtypes
Read files: pd.read_csv, pd.read_excel, pd.read_json, pd.read_sql.
Every column is a Series, and df["sales"] hands you that Series. Anything you learn about Series applies to
columns, which is why the two concepts are taught together.
The index is the point
The index is not just row numbers; it is a label that pandas uses to align data automatically:
q1 = pd.Series([120, 95, 143], index=["Delhi", "Pune", "Nagpur"])
q2 = pd.Series([130, 150, 88], index=["Pune", "Delhi", "Nagpur"])
print(q1 + q2) # Delhi 270, Nagpur 231, Pune 245 — matched by label, not position
NumPy would have added these element by element and given three wrong numbers. Alignment is the single biggest
practical difference between pandas and plain arrays. The same rule applies when you assign a Series to a column:
values land by index, and unmatched labels become NaN.
Set a meaningful index when rows have a natural key:
df = df.set_index("name")
df.loc["Ravi", "sales"] # 95000
df = df.reset_index() # back to a normal column
Inspecting a new table
| Call | Tells you |
|---|---|
df.shape |
Rows and columns |
df.head(10) / df.tail() |
First / last rows |
df.info() |
Dtypes, non-null counts, memory |
df.describe() |
Numeric summary (add include="all" for text) |
df.columns / df.dtypes |
Column names and types |
df["city"].value_counts() |
Frequency of each value |
df["city"].unique() / nunique() |
Distinct values, and how many |
df.sample(5) |
A random peek, safer than head on sorted data |
Run info() first, every time. An object dtype on a column that should be numeric means text is hiding in it.
Useful Series operations
sales = df["sales"]
sales.sum(), sales.mean(), sales.median(), sales.std()
sales.rank(ascending=False)
sales.pct_change()
sales.astype("float64")
df["sales_lakh"] = (sales / 100000).round(2) # add a derived column
Building DataFrames other ways
rows = [{"item": "Kaju katli", "kg": 4}, {"item": "Soan papdi", "kg": 9}]
pd.DataFrame(rows) # from a list of dicts (API responses)
pd.DataFrame(arr, columns=["a", "b"]) # from a NumPy array
Common mistakes
- Confusing
df["sales"](a Series) withdf[["sales"]](a one-column DataFrame). - Expecting position-based addition and getting index alignment instead.
- Forgetting that most methods return a new object:
df.drop(columns="x")does not changedfunless you assign. - Leaving a meaningless index after filtering; use
reset_index(drop=True).
Practice
Build a DataFrame of eight employees with name, city, department and monthly salary. Print info(),
describe() and value_counts() for city. Set name as the index, look up one person's salary with loc, add
an annual-salary column, then reset the index.