What is a pandas Series?

Build a Series of closes, select by label and by position, and see arithmetic line up two price series on their dates.

A Series is a column of values with a label on each one. The values are a numpy array. The labels are the index, and they travel with the values through every operation you run.

In this lesson I build a Series of closing prices, select from it by label and by position, swap the labels for dates, and add two price series together to see them line up on those dates.

Step 1. Values, and a label on each one

Hand pd.Series a list and it numbers the values for you, 0 upwards.

import pandas as pd

closes = pd.Series([185.40, 187.20, 184.90, 188.10, 190.50])   # five made-up closes

print(closes)
# -> 0    185.4
# -> 1    187.2
# -> 2    184.9
# -> 3    188.1
# -> 4    190.5
# -> dtype: float64
0    185.4
1    187.2
2    184.9
3    188.1
4    190.5
dtype: float64

The left column is the index, the right column is the data. One dtype covers the whole Series, because a Series holds one type of thing.

The two halves are also available on their own.

print(closes.values)    # -> [185.4 187.2 184.9 188.1 190.5]
print(closes.index)     # -> RangeIndex(start=0, stop=5, step=1)
print(closes.dtype)     # -> float64
print(len(closes))      # -> 5
[185.4 187.2 184.9 188.1 190.5]
RangeIndex(start=0, stop=5, step=1)
float64
5

.values is the numpy array from Lesson 12. .index is the labels.

.loc[...] selects by label. .iloc[...] selects by position.

print(closes.loc[2])     # -> 184.9
print(closes.iloc[2])    # -> 184.9
print(closes.iloc[-1])   # -> 190.5
184.9
184.9
190.5

Both gave 184.9 because here the label and the position happen to be the same number. Set the labels yourself and they part company.

week2 = pd.Series([185.40, 187.20, 184.90, 188.10, 190.50], index=[5, 6, 7, 8, 9])

print(week2.loc[5])      # -> 185.4   <- the value labelled 5
print(week2.iloc[0])     # -> 185.4   <- the first value
print(week2.loc[9])      # -> 190.5
print(week2.iloc[4])     # -> 190.5
185.4
185.4
190.5
190.5

week2.iloc[5] raises IndexError, because there are only five values and position 5 does not exist. week2.loc[5] reads the first one.

Step 2. Dates as the labels

pd.to_datetime turns a list of date strings into a DatetimeIndex. Pass it as index= and each close carries its own trading day.

dates = pd.to_datetime(["2026-01-02", "2026-01-05", "2026-01-06",
                        "2026-01-07", "2026-01-08"])

aapl = pd.Series([185.40, 187.20, 184.90, 188.10, 190.50], index=dates)

print(aapl)
# -> 2026-01-02    185.4
# -> 2026-01-05    187.2
# -> 2026-01-06    184.9
# -> 2026-01-07    188.1
# -> 2026-01-08    190.5
# -> dtype: float64

print(aapl.index)
# -> DatetimeIndex(['2026-01-02', '2026-01-05', '2026-01-06', '2026-01-07',
# ->                '2026-01-08'],
# ->               dtype='datetime64[us]', freq=None)
2026-01-02    185.4
2026-01-05    187.2
2026-01-06    184.9
2026-01-07    188.1
2026-01-08    190.5
dtype: float64
DatetimeIndex(['2026-01-02', '2026-01-05', '2026-01-06', '2026-01-07',
               '2026-01-08'],
              dtype='datetime64[us]', freq=None)

Now ask for a day by writing the day.

print(aapl.loc["2026-01-06"])   # -> 184.9
print(aapl.iloc[0])             # -> 185.4   <- still counts positions
184.9
185.4

A slice of labels gives a stretch of days.

print(aapl.loc["2026-01-05":"2026-01-07"])
# -> 2026-01-05    187.2
# -> 2026-01-06    184.9
# -> 2026-01-07    188.1
# -> dtype: float64
2026-01-05    187.2
2026-01-06    184.9
2026-01-07    188.1
dtype: float64

Slicing by label keeps the end date. List slicing in Lesson 5 stopped before the end position, and .iloc still does.

Step 3. Arithmetic lines up on the index

Here is a second stock. It trades on 5, 6 and 7 January like AAPL, misses 2 and 8 January, and adds 9 January.

msft = pd.Series([410.00, 405.50, 412.30, 415.00],
                 index=pd.to_datetime(["2026-01-05", "2026-01-06",
                                       "2026-01-07", "2026-01-09"]))

print(len(aapl), len(msft))   # -> 5 4
5 4

Adding them gives the value of one AAPL share plus one MSFT share, day by day.

total = aapl + msft

print(total)
# -> 2026-01-02      NaN
# -> 2026-01-05    597.2
# -> 2026-01-06    590.4
# -> 2026-01-07    600.4
# -> 2026-01-08      NaN
# -> 2026-01-09      NaN
# -> dtype: float64
2026-01-02      NaN
2026-01-05    597.2
2026-01-06    590.4
2026-01-07    600.4
2026-01-08      NaN
2026-01-09      NaN
dtype: float64

Five values plus four values gave six. pandas took the union of the two indexes, added the values that share a date, and put NaN on the three dates where only one stock had a price. NaN means not a number: the sum is unknown there, not zero.

print(len(total))                  # -> 6
print(total.isna().sum())          # -> 3    <- dates with no pair

print(total.dropna())
# -> 2026-01-05    597.2
# -> 2026-01-06    590.4
# -> 2026-01-07    600.4
# -> dtype: float64
6
3
2026-01-05    597.2
2026-01-06    590.4
2026-01-07    600.4
dtype: float64

The order the values sit in does not matter, only the labels. Reverse MSFT and the sum is identical.

msft_reversed = msft.iloc[::-1]

print((aapl + msft).equals(aapl + msft_reversed))   # -> True
True

Strip the labels off and the same addition goes wrong. Position 0 of AAPL is 2 January and position 0 of MSFT is 5 January, so numpy pairs prices from different days.

print(aapl.values[:4] + msft.values)   # -> [595.4 592.7 597.2 603.1]
[595.4 592.7 597.2 603.1]

595.4 is AAPL on 2 January plus MSFT on 5 January. The Series knew those dates did not match. The array did not.

Your turn

Build a with closes 100.0, 102.0, 101.0 on 2, 3 and 4 March 2026, and b with closes 50.0 and 51.0 on 3 and 4 March 2026. Add them. How many rows come back, and where is the NaN?

import pandas as pd

a = pd.Series([100.0, 102.0, 101.0],
              index=pd.to_datetime(["2026-03-02", "2026-03-03", "2026-03-04"]))

b = pd.Series([50.0, 51.0],
              index=pd.to_datetime(["2026-03-03", "2026-03-04"]))

print(a + b)
# -> 2026-03-02      NaN
# -> 2026-03-03    152.0
# -> 2026-03-04    152.0
# -> dtype: float64

print(len(a + b))   # -> 3    <- the union of three dates and two dates

Three rows. The NaN sits on 2 March, the one date b does not have.

When both indexes match, nothing is dropped and no NaN appears.

shares = pd.Series([10, 10, 10, 10, 10], index=aapl.index)

print(aapl * shares)
# -> 2026-01-02    1854.0
# -> 2026-01-05    1872.0
# -> 2026-01-06    1849.0
# -> 2026-01-07    1881.0
# -> 2026-01-08    1905.0
# -> dtype: float64
2026-01-02    1854.0
2026-01-05    1872.0
2026-01-06    1849.0
2026-01-07    1881.0
2026-01-08    1905.0
dtype: float64

Alignment is running here too. It just has nothing to reconcile.