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.
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 underneathprint(closes.index) # -> RangeIndex(start=0, stop=5, step=1) <- the labels, made for youprint(closes.dtype) # -> float64 <- one type covers the whole Seriesprint(len(closes)) # -> 5 <- how many values, not how many labels
.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 2print(closes.iloc[2]) # -> 184.9 <- .iloc counts to position 2print(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 9print(week2.loc[5]) # -> 185.4 <- the value labelled 5print(week2.iloc[0]) # -> 185.4 <- the first valueprint(week2.loc[9]) # -> 190.5print(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 weekendaapl = pd.Series([185.40, 187.20, 184.90, 188.10, 190.50], index=dates) # each close carries its own dayprint(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: float64print(aapl.index)# -> DatetimeIndex(['2026-01-02', '2026-01-05', '2026-01-06', '2026-01-07',# -> '2026-01-08'],# -> dtype='datetime64[ns]', freq=None)
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 haveprint(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 positionprint(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 indexesprint(total.isna().sum()) # -> 3 <- dates with no pairprint(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
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 orderprint((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?
TipShow answer
import pandas as pda = 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: float64print(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.
NoteTwo Series with the same dates
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 datesprint(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