How do I load a CSV file?

Read a CSV into a DataFrame with pd.read_csv, parse the dates, and index the rows by date.

A CSV file is a text file with one row per line and commas between the fields. pd.read_csv turns one into a DataFrame.

In this lesson I read prices.csv, a simulated file of daily closes sitting next to this page, check what came in, fix the date column, and pull out one ticker with its rows labelled by date.

Step 1. Read the file

The file has four columns, Date,Ticker,Close,Volume, and three tickers stacked on top of each other. Pass the filename and pandas hands back a DataFrame.

import pandas as pd

prices = pd.read_csv("prices.csv")     # the file sits in the same folder as this lesson

print(prices.head())                   # first five rows, to see what came in
# ->          Date Ticker   Close   Volume
# -> 0  2026-01-02    AAA  186.66  2183494
# -> 1  2026-01-02    CCC  416.98  3117814
# -> 2  2026-01-02    DDD  125.18  7447373
# -> 3  2026-01-05    AAA  186.38  8463199
# -> 4  2026-01-05    CCC  423.19  3345056

print(prices.shape)                    # -> (120, 4)
         Date Ticker   Close   Volume
0  2026-01-02    AAA  186.66  2183494
1  2026-01-02    CCC  416.98  3117814
2  2026-01-02    DDD  125.18  7447373
3  2026-01-05    AAA  186.38  8463199
4  2026-01-05    CCC  423.19  3345056
(120, 4)

head() shows the first five rows. shape is rows then columns: 120 rows, 4 columns. Three tickers times 40 business days gives 120.

The first line of the file became the column names, and the numbers on the left, 0 to 4, are the index pandas made up because I gave it nothing to use.

Step 2. Date arrives as text

read_csv guesses a type per column. Ask it what it decided.

print(prices.dtypes)
# -> Date       object
# -> Ticker     object
# -> Close     float64
# -> Volume      int64
# -> dtype: object
Date       object
Ticker     object
Close     float64
Volume      int64
dtype: object

Close came in as a float and Volume as an integer. Date came in as object, which is how pandas reports a column of text. "2026-01-02" is a string of ten characters, not a date, so the date tools are not available on it.

print(type(prices["Date"][0]))     # -> <class 'str'>
<class 'str'>

Pass parse_dates with a list of column names and pandas converts them while reading.

prices = pd.read_csv("prices.csv", parse_dates=["Date"])    # Date becomes a real timestamp

print(prices.dtypes)
# -> Date      datetime64[ns]
# -> Ticker            object
# -> Close            float64
# -> Volume             int64
# -> dtype: object

print(prices["Date"].min(), "to", prices["Date"].max())
# -> 2026-01-02 00:00:00 to 2026-02-26 00:00:00
Date      datetime64[ns]
Ticker            object
Close            float64
Volume             int64
dtype: object
2026-01-02 00:00:00 to 2026-02-26 00:00:00

datetime64 is a real timestamp. Now .dt reaches into it and pulls out the parts of the date.

print(prices["Date"].dt.day_name().head(3).tolist())    # .dt only works on a timestamp column
# -> ['Friday', 'Friday', 'Friday']
['Friday', 'Friday', 'Friday']

The first three rows are the same day, one per ticker.

Step 3. Look at the whole frame

info() gives the row count, the column names, how many non-missing values each column holds, and its type, in one go.

prices.info()    # writes its own report, no print needed
# -> <class 'pandas.core.frame.DataFrame'>
# -> RangeIndex: 120 entries, 0 to 119
# -> Data columns (total 4 columns):
# ->  #   Column  Non-Null Count  Dtype
# -> ---  ------  --------------  -----
# ->  0   Date    120 non-null    datetime64[ns]
# ->  1   Ticker  120 non-null    object
# ->  2   Close   120 non-null    float64
# ->  3   Volume  120 non-null    int64
# -> dtypes: datetime64[ns](1), float64(1), int64(1), object(1)
# -> memory usage: 3.9+ KB
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 120 entries, 0 to 119
Data columns (total 4 columns):
 #   Column  Non-Null Count  Dtype         
---  ------  --------------  -----         
 0   Date    120 non-null    datetime64[ns]
 1   Ticker  120 non-null    object        
 2   Close   120 non-null    float64       
 3   Volume  120 non-null    int64         
dtypes: datetime64[ns](1), float64(1), int64(1), object(1)
memory usage: 3.9+ KB

Every column says 120 non-null, so nothing is missing.

value_counts() on the ticker column counts how many rows each ticker owns.

print(prices["Ticker"].value_counts())    # checks every ticker brought the same rows
# -> Ticker
# -> AAA    40
# -> CCC    40
# -> DDD    40
# -> Name: count, dtype: int64
Ticker
AAA    40
CCC    40
DDD    40
Name: count, dtype: int64

Forty rows each, the same count for every ticker.

Step 4. One ticker, labelled by date

prices["Ticker"] == "AAA" is the boolean mask from Lesson 13, one True or False per row. Putting it in square brackets keeps the True rows.

aapl = prices[prices["Ticker"] == "AAA"]    # keep the rows the mask marked True

print(aapl.shape)                            # -> (40, 4)
print(aapl.head())
# ->          Date Ticker   Close   Volume
# -> 0  2026-01-02    AAA  186.66  2183494
# -> 3  2026-01-05    AAA  186.38  8463199
# -> 6  2026-01-06    AAA  188.51  5299573
# -> 9  2026-01-07    AAA  187.24  4931962
# -> 12 2026-01-08    AAA  189.27  7949369
(40, 4)
         Date Ticker   Close   Volume
0  2026-01-02    AAA  186.66  2183494
3  2026-01-05    AAA  186.38  8463199
6  2026-01-06    AAA  188.51  5299573
9  2026-01-07    AAA  187.24  4931962
12 2026-01-08    AAA  189.27  7949369

The index reads 0, 3, 6, 9, 12. Those are the positions the AAA rows had in the full file, carried over.

set_index("Date") moves the date column into the index, so each row is named by its day.

aapl = aapl.set_index("Date")    # Date leaves the columns and names the rows

print(aapl.head())
# ->            Ticker   Close   Volume
# -> Date
# -> 2026-01-02    AAA  186.66  2183494
# -> 2026-01-05    AAA  186.38  8463199
# -> 2026-01-06    AAA  188.51  5299573
# -> 2026-01-07    AAA  187.24  4931962
# -> 2026-01-08    AAA  189.27  7949369

print(aapl.columns)
# -> Index(['Ticker', 'Close', 'Volume'], dtype='object')
           Ticker   Close   Volume
Date                              
2026-01-02    AAA  186.66  2183494
2026-01-05    AAA  186.38  8463199
2026-01-06    AAA  188.51  5299573
2026-01-07    AAA  187.24  4931962
2026-01-08    AAA  189.27  7949369
Index(['Ticker', 'Close', 'Volume'], dtype='object')

Date left the columns and became the row labels. Ask for a day by name:

print(aapl.loc["2026-01-06", "Close"])     # -> 188.51   <- one cell, row label then column
print(round(aapl["Close"].mean(), 2))      # -> 183.69   <- average close over the 40 days
188.51
183.69

read_csv can set the index while it reads, with index_col:

alt = pd.read_csv("prices.csv", parse_dates=["Date"], index_col="Date")    # parsed and indexed in one read

print(alt.head(3))
# ->            Ticker   Close   Volume
# -> Date
# -> 2026-01-02    AAA  186.66  2183494
# -> 2026-01-02    CCC  416.98  3117814
# -> 2026-01-02    DDD  125.18  7447373
           Ticker   Close   Volume
Date                              
2026-01-02    AAA  186.66  2183494
2026-01-02    CCC  416.98  3117814
2026-01-02    DDD  125.18  7447373

All three tickers are still in there, so every date appears three times.

So the mask still has to run. Same two lines again, on CCC:

msft = alt[alt["Ticker"] == "CCC"]    # a date index does not filter, the mask still does

print(msft.shape)                      # -> (40, 3)
print(msft["Close"].head(3))
# -> Date
# -> 2026-01-02    416.98
# -> 2026-01-05    423.19
# -> 2026-01-06    423.95
# -> Name: Close, dtype: float64
(40, 3)
Date
2026-01-02    416.98
2026-01-05    423.19
2026-01-06    423.95
Name: Close, dtype: float64

Four columns became three, because Date is the index now and no longer counts as a column.

Your turn

Read prices.csv with the dates parsed, keep the DDD rows, put the date in the index, and print how many rows you kept and the highest close.

import pandas as pd

prices = pd.read_csv("prices.csv", parse_dates=["Date"])
nvda = prices[prices["Ticker"] == "DDD"].set_index("Date")

print(nvda.shape)                # -> (40, 3)
print(nvda["Close"].max())       # -> 126.54
print(nvda["Close"].idxmax())    # -> 2026-01-08 00:00:00

idxmax() gives the index label of the largest value, which is a date now that the date is the index.

pd.read_csv("prices.csv") looks in the working directory, which for a lesson page is the folder the page lives in. If the file sits somewhere else, give the path: pd.read_csv("data/prices.csv"), or the full path. A missing file raises FileNotFoundError naming what it tried to open.

Further reading: I put a live price feed behind the same reading steps in Connect to IBKR with Python.