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"]]?