Skip to content

Python Pandas - Series

A Pandas Series is a fundamental one-dimensional labeled array structure. It’s capable of holding data of various types (integer, string, float, Python objects, etc.). Think of it as a single column in a spreadsheet or a more powerful version of a Python list or a NumPy array, with an associated index.

The axis labels collectively form the index, providing meaningful names or identifiers for each data point.

You create a Series using the pd.Series() constructor:

pandas.Series(data=None, index=None, dtype=None, name=None, copy=False)

Key parameters:

ParameterDescription
dataThe data to store in the Series. Can be a Python list, NumPy array, dictionary, scalar value, or another Series.
indexThe index labels for the data. Must be hashable and have the same length as data. If None (default) and data is array-like, a RangeIndex (0, 1, 2, …) is created. If data is a dict, keys are used as index unless index is specified.
dtypeDesired data type for the Series (e.g., 'int64', 'float64', 'object', 'category', 'datetime64[ns]). If None, the type is inferred from the data.
nameAn optional name attribute for the Series.
copyIf True, always copy the input data. Default is False (may copy or use a reference depending on input).

You can create a Series from various inputs:

  • Python list or NumPy array
  • Python dictionary
  • Scalar value (requires an index)

You can create an empty Series, but you usually need to specify the dtype to avoid ambiguity.

# Import pandas
import pandas as pd
# Create an empty Series (specify dtype)
s_empty = pd.Series(dtype='object')
print(s_empty)

Output:

Series([], dtype: object)

(Note: The default dtype for a completely empty Series without specified dtype might vary across Pandas versions or contexts, often defaulting to float64 historically, but object is safer when explicitly creating an empty one intended for non-numeric data later.)

If data is a list or NumPy array and no index is provided, a default integer index [0, 1, ..., n-1] is created.

import pandas as pd
import numpy as np
data_list = ['a', 'b', 'c', 'd']
data_array = np.array(['a', 'b', 'c', 'd'])
# Create Series from list (default index)
s_from_list = pd.Series(data_list)
print("Series from list (default index):")
print(s_from_list)

Output:

Series from list (default index):
0 a
1 b
2 c
3 d
dtype: object

You can provide a custom index:

# Create Series from array with custom index
custom_index = [100, 101, 102, 103]
s_custom_index = pd.Series(data_array, index=custom_index)
print("\nSeries from array (custom index):")
print(s_custom_index)

Output:

Series from array (custom index):
100 a
101 b
102 c
103 d
dtype: object

If data is a dictionary, its keys are used as the index labels by default (in insertion order for Python 3.7+, or sorted order in older Python/Pandas versions). The dictionary values become the Series data.

import pandas as pd
data_dict = {'x': 10.0, 'y': 20.0, 'z': 30.0}
# Create Series from dict (keys become index)
s_from_dict = pd.Series(data_dict)
print("Series from dict (default index):")
print(s_from_dict)

Output:

Series from dict (default index):
x 10.0
y 20.0
z 30.0
dtype: float64

If you provide an index argument when creating from a dictionary, Pandas will align the dictionary values based on the provided index labels. Missing labels will result in NaN values.

# Create Series from dict with explicit index
explicit_index = ['y', 'z', 'w', 'x'] # 'w' is not in dict keys
s_dict_explicit_index = pd.Series(data_dict, index=explicit_index)
print("\nSeries from dict (explicit index):")
print(s_dict_explicit_index)

Output:

Series from dict (explicit index):
y 20.0
z 30.0
w NaN
x 10.0
dtype: float64

Observe: The order follows explicit_index. The value for ‘w’ is NaN because ‘w’ was not a key in data_dict.

If data is a single scalar value (like a number or string), you must provide an index. The scalar value will be repeated for each label in the index.

import pandas as pd
# Create Series from a scalar
s_from_scalar = pd.Series(5, index=[0, 1, 2, 3])
print("Series from scalar:")
print(s_from_scalar)

Output:

Series from scalar:
0 5
1 5
2 5
3 5
dtype: int64

You can access data in a Series using methods similar to NumPy arrays and Python dictionaries, primarily using index labels or integer positions.

Best Practice: For clarity and consistency, especially when dealing with integer indexes that could be confused with positions, it’s recommended to use .loc[] for label-based access and .iloc[] for integer position-based access, even with Series.

import pandas as pd
s = pd.Series([10, 20, 30, 40, 50], index=['a', 'b', 'c', 'd', 'e'])
print("Sample Series:")
print(s)

Use .iloc[] with integer indices (0-based).

# Retrieve the first element (position 0)
print("\nFirst element (position 0):")
print(s.iloc[0])
# Retrieve the first three elements (positions 0, 1, 2)
print("\nFirst three elements (slice 0:3):")
print(s.iloc[0:3]) # or s.iloc[:3]
# Retrieve the last three elements (slice from position -3 onwards)
print("\nLast three elements (slice -3:):")
print(s.iloc[-3:])

Output:

Sample Series:
a 10
b 20
c 30
d 40
e 50
dtype: int64
First element (position 0):
10
First three elements (slice 0:3):
a 10
b 20
c 30
dtype: int64
Last three elements (slice -3:):
c 30
d 40
e 50
dtype: int64

Use .loc[] with index labels.

# Retrieve a single element by label 'a'
print("\nElement with label 'a':")
print(s.loc['a'])
# Retrieve multiple elements using a list of labels
print("\nElements with labels 'a', 'c', 'd':")
print(s.loc[['a', 'c', 'd']])
# Retrieve a range of elements using label slicing (inclusive)
print("\nElements from label 'b' to 'd' (inclusive):")
print(s.loc['b':'d'])

Output:

Element with label 'a':
10
Elements with labels 'a', 'c', 'd':
a 10
c 30
d 40
dtype: int64
Elements from label 'b' to 'd' (inclusive):
b 20
c 30
d 40
dtype: int64

Direct [] indexing on a Series primarily attempts label lookup first. If the index contains integers, this can sometimes be ambiguous. Slicing with integers usually refers to position.

# Direct indexing (attempts label lookup first)
print("\nDirect access label 'a':", s['a'])
# Direct slicing (usually positional)
print("\nDirect slice [:3]:")
print(s[:3])
# If a label is not found, it raises a KeyError
try:
print(s['f'])
except KeyError as e:
print(f"\nError accessing label 'f': {e}")

Output:

Direct access label 'a': 10
Direct slice [:3]:
a 10
b 20
c 30
dtype: int64
Error accessing label 'f': 'f'

While direct [] access is common for Series, using .loc and .iloc explicitly can make your code more readable and less prone to errors, especially when dealing with integer labels or transitioning between Series and DataFrames.