Skip to main content

Series and DataFrames

Topic — Pandas has two data structures. A Series is one labelled column; a DataFrame is a table of them.

import pandas as pd

pd is the universal alias, as np is for NumPy. Pandas is built on top of NumPy — a Series wraps an ndarray and adds an index.

DimensionsAnalogy
Series1one column of a spreadsheet
DataFrame2the whole spreadsheet

Creating a Series​

From a list​

data = pd.Series([10, 20, 30, 40])
print(data)
Output
0 10
1 20
2 30
3 40
dtype: int64

Two things to read here. The left column is the index — pandas supplied 0, 1, 2, 3 automatically because you didn't give one. And the last line, dtype: int64, is not a data row — it's pandas telling you the element type, exactly like a NumPy dtype.

With a custom index​

data = pd.Series([10, 20, 30, 40], index=["a", "b", "c", "d"])
print(data)
Output
a 10
b 20
c 30
d 40
dtype: int64

The index is now labels instead of positions. This is the whole point of pandas over NumPy: your data carries its own labels around with it.

From a NumPy array​

import numpy as np

arr = np.array(["g", "e", "e", "k", "s"])
series = pd.Series(arr)
print(series)
Output
0 g
1 e
2 e
3 k
4 s
dtype: object

dtype: object is how pandas stores strings. Since pandas sits on NumPy, converting between the two is direct — see NumPy and pandas together.

From a dictionary​

population_dict = {
"Kerala": 395,
"Goa": 291,
"Assam": 2153,
"Delhi": 2020,
}
population = pd.Series(population_dict)
print(population)
Output
Kerala 395
Goa 291
Assam 2153
Delhi 2020
dtype: int64

The dictionary keys become the index, and the values become the data. Insertion order is preserved — the same dict ordering guarantee from Python 3.7.

This is often the most natural way to build a Series, because a dict already pairs labels with values.


Accessing Series data​

data = pd.Series([100, 200, 300], index=["A", "B", "C"])
print(data["B"]) # → 200 by label
print(data.iloc[1]) # → 200 by position
Output
200
200

Both give 200 here, but they're asking different questions — one by label, one by position.

danger

[] on a Series looks up the label, not the position

With a default index the two coincide, which hides the difference. Give the Series an integer index that isn't in order and it bites:

rev = pd.Series([10, 20, 30], index=[2, 1, 0])
print(rev)
print(rev[2]) # → 10 the item LABELLED 2, which is first
print(rev.iloc[2]) # → 30 the item at POSITION 2
Output
2 10
1 20
0 30
dtype: int64
10
30

rev[2] returns 10, not 30. Use .loc[] when you mean labels and .iloc[] when you mean positions — never bare [] for a single element. More on this in Selecting and Transforming.


Series attributes​

s = pd.Series([10, 20, 30, 40], index=["a", "b", "c", "d"])
print("values:", s.values)
print("index:", list(s.index))
print("dtype:", s.dtype)
print("shape:", s.shape, "size:", s.size, "ndim:", s.ndim)
print("name:", s.name)
Output
values: [10 20 30 40]
index: ['a', 'b', 'c', 'd']
dtype: int64
shape: (4,) size: 4 ndim: 1
name: None
AttributeGives
.valuesthe underlying NumPy array
.indexthe labels
.dtypeelement type
.shape(n,) — always 1-D
.sizenumber of elements
.ndimalways 1
.namethe Series' own name, if any

.shape and .ndim are the same attributes as on a NumPy array, and .values gets you straight back to one.

.name matters more than it looks: when a Series becomes a DataFrame column, its name becomes the column heading.


Dtype follows the data​

Same promotion rules as NumPy — one float makes it all float, anything non-numeric makes it object:

print(pd.Series([1, 2.5]).dtype) # → float64
print(pd.Series([1, "x"]).dtype) # → object
print(pd.Series(["a", "b"]).dtype) # → object
Output
float64
object
object

A Series is vectorised​

Everything from NumPy's element-wise operations carries over — and the index comes along for the ride:

s = pd.Series([10, 20, 30, 40], index=["a", "b", "c", "d"])
print(s * 2)
print(s[s > 20])
Output
a 20
b 40
c 60
d 80
dtype: int64
c 30
d 40
dtype: int64

s * 2 doubles every value and keeps the labels. s[s > 20] is boolean masking — and note it returns the matching rows with their original labels c and d, not renumbered. Labels surviving every operation is what makes pandas useful.


DataFrames​

A DataFrame is a dict of Series sharing one index. The usual way to build one is from a dict of lists, where keys become column names:

df = pd.DataFrame({"Name": ["Arun", "Bala"], "Marks": [85, 72]})
print(df)
print("ndim:", df.ndim, "shape:", df.shape)
Output
Name Marks
0 Arun 85
1 Bala 72
ndim: 2 shape: (2, 2)

.shape is (rows, columns).

Every column is a Series​

print(type(df["Name"]).__name__) # → Series
Output
Series

This is the connection worth holding on to: selecting one column from a DataFrame gives you a Series. Everything on this page then applies to it — .values, .dtype, masking, arithmetic. And the Series' name is the column name.


Series vs DataFrame​

SeriesDataFrame
Dimensions12
.ndim12
.shape(n,)(rows, cols)
Hasone indexan index and columns
Holdsone dtypeone dtype per column
Built fromlist, dict, ndarraydict of lists, list of dicts, ndarray, CSV
You get one bydf["col"]pd.read_csv(...)

The dtype row is the important difference. A Series, like an ndarray, is homogeneous. A DataFrame lets each column have its own type — which is why it can hold a real table with names, ages and marks side by side, and why NumPy can't.


Common Mistakes​

#MistakeWrongRight
1Reading dtype: as a data rowit's metadatathe row count is .size
2s[2] on a non-default indexlooks up the labels.iloc[2] for position
3Expecting a dict's values to become the indexkeys become the indexthat's the design
4pd.Series(10, 20, 30)TypeErrorpd.Series([10, 20, 30])
5Confusing .values with .index.values is the data.index is the labels
6Expecting df["col"] to be a DataFrameit's a Seriesdf[["col"]] for a DataFrame
7Assuming a filter renumberslabels are preserved.reset_index(drop=True)

Summary​

TaskCall
Series from a listpd.Series([...])
Series with labelspd.Series([...], index=[...])
Series from a dictpd.Series({...}) — keys become the index
Series from an arraypd.Series(arr)
Read by label / positions.loc[k] / s.iloc[i]
Underlying arrays.values
DataFrame from a dictpd.DataFrame({"col": [...]})
One columndf["col"] → Series

Key takeaways

  • A Series is 1-D with an index; a DataFrame is 2-D with an index and columns
  • The trailing dtype: line when you print a Series is metadata, not data
  • Dictionary keys become the index (Series) or the column names (DataFrame)
  • s["x"] looks up a label — use .iloc[] when you mean a position
  • Labels survive every operation, including filtering
  • A Series holds one dtype; a DataFrame holds one per column — that's why tables need it
  • df["col"] is a Series, so everything here applies to columns too

See also: Loading and Saving Data · Selecting and Transforming for .loc and .iloc in depth · Arrays and Attributes for the NumPy layer underneath


Run It Yourself​

pandas_series.py
import pandas as pd
import numpy as np

print("default index")
print(pd.Series([10, 20, 30, 40]))

print("\ncustom index")
s = pd.Series([10, 20, 30, 40], index=["a", "b", "c", "d"])
print(s)

print("\nfrom a dict — keys become the index")
print(pd.Series({"Kerala": 395, "Goa": 291, "Assam": 2153}))

print("\nfrom a numpy array")
print(pd.Series(np.array(["g", "e", "e", "k", "s"])))

print("\nattributes")
print(f" {'values':<8} {s.values}")
print(f" {'index':<8} {list(s.index)}")
print(f" {'dtype':<8} {s.dtype}")
print(f" {'shape':<8} {s.shape}")

print("\nvectorised, labels preserved")
print(s * 2)
print(s[s > 20])

print("\na DataFrame column is a Series")
df = pd.DataFrame({"Name": ["Arun", "Bala"], "Marks": [85, 72]})
print(df)
print(f" df.shape {df.shape}")
print(f" type(df['Name']) {type(df['Name']).__name__}")
Output
default index
0 10
1 20
2 30
3 40
dtype: int64

custom index
a 10
b 20
c 30
d 40
dtype: int64

from a dict — keys become the index
Kerala 395
Goa 291
Assam 2153
dtype: int64

from a numpy array
0 g
1 e
2 e
3 k
4 s
dtype: object

attributes
values [10 20 30 40]
index ['a', 'b', 'c', 'd']
dtype int64
shape (4,)

vectorised, labels preserved
a 20
b 40
c 60
d 80
dtype: int64
c 30
d 40
dtype: int64

a DataFrame column is a Series
Name Marks
0 Arun 85
1 Bala 72
df.shape (2, 2)
type(df['Name']) Series

Practice Questions​

From the Unit 1 question bank. Tags and marks are explained on the Python index.

Unit 1 § K — Pandas​

K1. [PROG] Create a pandas Series from [10, 20, 30, 40]. Then create the same Series with a custom index ["a", "b", "c", "d"]. Print both. [3]

K2. [OUT] What does printing a Series look like — and what is that extra last line? [3]

import pandas as pd
s = pd.Series([10, 20, 30])
print(s)

K3. [PROG] Create a Series from the dictionary {"Kerala": 395, "Goa": 291, "Assam": 2153}. What becomes the index? [3]

K11. [THEORY] What is the difference between a Series and a DataFrame? How many dimensions does each have? [3]

Viva​

  1. What does df.shape return, and in what order?
  2. What is the difference between df["A"] and df[["A"]]?