Glue DataFrames together with pd.concat, top to bottom by default and side by side with axis=1.
pd.concat takes a list of DataFrames and returns one DataFrame. By default it stacks them vertically: the rows of the second frame go under the rows of the first.
In this lesson I stack two small frames, fix the index that comes out of it, stack two months of prices into one table, and then put two frames side by side with axis=1.
Step 1. Two frames, one on top of the other
Both frames have the same two columns, so the result has those two columns and all four rows.
import pandas as pda = pd.DataFrame({"Ticker": ["AAA", "CCC"], "Close": [186.66, 416.98]}) # the first frameb = pd.DataFrame({"Ticker": ["DDD", "FFF"], "Close": [125.18, 248.40]}) # the second, same columnsboth = pd.concat([a, b]) # a list of frames goes in, one frame comes outprint(both)# -> Ticker Close# -> 0 AAA 186.66# -> 1 CCC 416.98# -> 0 DDD 125.18# -> 1 FFF 248.40print(both.shape) # -> (4, 2)print(both.index.tolist()) # -> [0, 1, 0, 1] <- each frame kept its own labels
prices.csv holds simulated daily closes for three tickers. I split it into January and February so there are two frames to stack.
prices = pd.read_csv("prices.csv", parse_dates=["Date"]) # Date arrives as a real datejan = prices[prices["Date"].dt.month ==1] # January rows onlyfeb = prices[prices["Date"].dt.month ==2] # February rows onlyprint(jan.shape, feb.shape) # -> (63, 4) (57, 4)print(jan.tail(2))# -> Date Ticker Close Volume# -> 61 2026-01-30 CCC 442.68 3378706# -> 62 2026-01-30 DDD 113.41 7028914
(63, 4) (57, 4)
Date Ticker Close Volume
61 2026-01-30 CCC 442.68 3378706
62 2026-01-30 DDD 113.41 7028914
Stack them and the row counts add up.
stacked = pd.concat([jan, feb], ignore_index=True) # 63 + 57 rows back in one frameprint(stacked.shape) # -> (120, 4)print(stacked["Date"].min()) # -> 2026-01-02 00:00:00print(stacked["Date"].max()) # -> 2026-02-26 00:00:00
(120, 4)
2026-01-02 00:00:00
2026-02-26 00:00:00
concat puts the frames down in the order I listed them and does no sorting. To get one ticker’s whole history together and in date order, sort afterwards.
srt = stacked.sort_values(["Ticker", "Date"], ignore_index=True) # ticker first, date secondprint(srt.head(2))# -> Date Ticker Close Volume# -> 0 2026-01-02 AAA 186.66 2183494# -> 1 2026-01-05 AAA 186.38 8463199print(srt.tail(2))# -> Date Ticker Close Volume# -> 118 2026-02-25 DDD 108.88 8875303# -> 119 2026-02-26 DDD 107.57 7025675
Date Ticker Close Volume
0 2026-01-02 AAA 186.66 2183494
1 2026-01-05 AAA 186.38 8463199
Date Ticker Close Volume
118 2026-02-25 DDD 108.88 8875303
119 2026-02-26 DDD 107.57 7025675
Step 3. Side by side with axis=1
axis=1 glues frames left to right instead of top to bottom. The columns sit next to each other and the rows are matched on the index, not on position. Here AAA has four dates and CCC has three.
aapl = prices[prices["Ticker"] =="AAA"].set_index("Date")[["Close"]].head(4) # four datesmsft = prices[prices["Ticker"] =="CCC"].set_index("Date")[["Close"]].head(3) # three datesaapl.columns = ["AAA"] # rename so the two Close columns do not collidemsft.columns = ["CCC"]side = pd.concat([aapl, msft], axis=1) # left to right, rows matched on the Date indexprint(side)# -> AAA CCC# -> Date# -> 2026-01-02 186.66 416.98# -> 2026-01-05 186.38 423.19# -> 2026-01-06 188.51 423.95# -> 2026-01-07 187.24 NaN
AAA CCC
Date
2026-01-02 186.66 416.98
2026-01-05 186.38 423.19
2026-01-06 188.51 423.95
2026-01-07 187.24 NaN
The result carries every date from either frame, and 2026-01-07 gets NaN for CCC because that frame has no such row.
Your turn
Three frames hold one day of closes each, for the same two tickers. Stack them into one frame of six rows numbered 0 to 5.
d1 = pd.DataFrame({"Ticker": ["AAA", "CCC"], "Close": [186.66, 416.98]}) # day oned2 = pd.DataFrame({"Ticker": ["AAA", "CCC"], "Close": [190.10, 421.30]}) # day twod3 = pd.DataFrame({"Ticker": ["AAA", "CCC"], "Close": [188.00, 430.00]}) # day three
TipShow answer
days = pd.concat([d1, d2, d3], ignore_index=True) # the list can hold any number of framesprint(days)# -> Ticker Close# -> 0 AAA 186.66# -> 1 CCC 416.98# -> 2 AAA 190.10# -> 3 CCC 421.30# -> 4 AAA 188.00# -> 5 CCC 430.00print(days.shape) # -> (6, 2)
NoteWhat if the columns do not match?
concat takes the union of the columns and fills the gaps with NaN.
one = pd.DataFrame({"Ticker": ["AAA"], "Close": [186.66]}) # two columnstwo = pd.DataFrame({"Ticker": ["DDD"], "Close": [125.18], "Volume": [7447373]}) # three columnsprint(pd.concat([one, two], ignore_index=True))# -> Ticker Close Volume# -> 0 AAA 186.66 NaN# -> 1 DDD 125.18 7447373.0
Ticker Close Volume
0 AAA 186.66 NaN
1 DDD 125.18 7447373.0
Volume became a float, because NaN is a float.
NoteDataFrame.append is gone
Older code adds a row with df.append(other). That method was removed in pandas 2.0.
print(hasattr(pd.DataFrame, "append")) # -> False
False
pd.concat([df, other]) does the job. It builds a whole new frame every time, so calling it once per row inside a loop copies everything on each pass. Collect the frames in a list and pass the list to concat once.