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]   <- the numpy array underneath
print(closes.index)     # -> RangeIndex(start=0, stop=5, step=1)   <- the labels, made for you
print(closes.dtype)     # -> float64   <- one type covers the whole Series
print(len(closes))      # -> 5   <- how many values, not how many labels
[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   <- .loc looks up the label 2
print(closes.iloc[2])    # -> 184.9   <- .iloc counts to position 2
print(closes.iloc[-1])   # -> 190.5   <- -1 counts back from the end
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])   # same values, labels 5 to 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",    # strings in, DatetimeIndex out
                        "2026-01-07", "2026-01-08"])                 # five trading days, no weekend

aapl = pd.Series([185.40, 187.20, 184.90, 188.10, 190.50], index=dates)   # each close carries its own day

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[ns]', 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[ns]', freq=None)

Now ask for a day by writing the day.

print(aapl.loc["2026-01-06"])   # -> 184.9   <- the label is a date now
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"])   # a label slice keeps both ends
# -> 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 AAA, misses 2 and 8 January, and adds 9 January.

msft = pd.Series([410.00, 405.50, 412.30, 415.00],                    # four closes for a second stock
                 index=pd.to_datetime(["2026-01-05", "2026-01-06",    # three dates shared with AAA
                                       "2026-01-07", "2026-01-09"]))  # and one AAA does not have

print(len(aapl), len(msft))   # -> 5 4   <- different lengths, on purpose
5 4

Adding them gives the value of one AAA share plus one CCC share, day by day.

total = aapl + msft   # matched up on the dates, not on position

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   <- the union of the two indexes
print(total.isna().sum())          # -> 3    <- dates with no pair

print(total.dropna())              # keeps only the dates both stocks traded
# -> 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 CCC and the sum is identical.

msft_reversed = msft.iloc[::-1]   # same four prices, rows in reverse order

print((aapl + msft).equals(aapl + msft_reversed))   # -> True   <- labels decided it, not order
True

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

print(aapl.values[:4] + msft.values)   # -> [595.4 592.7 597.2 603.1]   <- paired by position, dates lost
[595.4 592.7 597.2 603.1]

595.4 is AAA on 2 January plus CCC 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],                                            # three days of closes
              index=pd.to_datetime(["2026-03-02", "2026-03-03", "2026-03-04"]))

b = pd.Series([50.0, 51.0],                                                     # two of those three days
              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)   # borrows AAA's exact dates

print(aapl * shares)                                         # value of ten shares each day
# -> 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.