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())
# ->          Date Ticker   Close   Volume
# -> 0  2026-01-02   AAPL  186.66  2183494
# -> 1  2026-01-02   MSFT  416.98  3117814
# -> 2  2026-01-02   NVDA  125.18  7447373
# -> 3  2026-01-05   AAPL  186.38  8463199
# -> 4  2026-01-05   MSFT  423.19  3345056

print(prices.shape)                    # -> (120, 4)
         Date Ticker   Close   Volume
0  2026-01-02   AAPL  186.66  2183494
1  2026-01-02   MSFT  416.98  3117814
2  2026-01-02   NVDA  125.18  7447373
3  2026-01-05   AAPL  186.38  8463199
4  2026-01-05   MSFT  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          str
# -> Ticker        str
# -> Close     float64
# -> Volume      int64
# -> dtype: object
Date          str
Ticker        str
Close     float64
Volume      int64
dtype: object

Close came in as a float and Volume as an integer. Date came in as str, which is 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"])

print(prices.dtypes)
# -> Date      datetime64[us]
# -> Ticker               str
# -> 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[us]
Ticker               str
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())
# -> ['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()
# -> <class 'pandas.DataFrame'>
# -> RangeIndex: 120 entries, 0 to 119
# -> Data columns (total 4 columns):
# ->  #   Column  Non-Null Count  Dtype
# -> ---  ------  --------------  -----
# ->  0   Date    120 non-null    datetime64[us]
# ->  1   Ticker  120 non-null    str
# ->  2   Close   120 non-null    float64
# ->  3   Volume  120 non-null    int64
# -> dtypes: datetime64[us](1), float64(1), int64(1), str(1)
# -> memory usage: 3.9 KB
<class 'pandas.DataFrame'>
RangeIndex: 120 entries, 0 to 119
Data columns (total 4 columns):
 #   Column  Non-Null Count  Dtype         
---  ------  --------------  -----         
 0   Date    120 non-null    datetime64[us]
 1   Ticker  120 non-null    str           
 2   Close   120 non-null    float64       
 3   Volume  120 non-null    int64         
dtypes: datetime64[us](1), float64(1), int64(1), str(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())
# -> Ticker
# -> AAPL    40
# -> MSFT    40
# -> NVDA    40
# -> Name: count, dtype: int64
Ticker
AAPL    40
MSFT    40
NVDA    40
Name: count, dtype: int64

Forty rows each, the same count for every ticker.

Step 4. One ticker, labelled by date

prices["Ticker"] == "AAPL" 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"] == "AAPL"]

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

The index reads 0, 3, 6, 9, 12. Those are the positions the AAPL 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")

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

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

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

print(aapl.loc["2026-01-06", "Close"])     # -> 188.51
print(round(aapl["Close"].mean(), 2))      # -> 183.69
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")

print(alt.head(3))
# ->            Ticker   Close   Volume
# -> Date
# -> 2026-01-02   AAPL  186.66  2183494
# -> 2026-01-02   MSFT  416.98  3117814
# -> 2026-01-02   NVDA  125.18  7447373
           Ticker   Close   Volume
Date                              
2026-01-02   AAPL  186.66  2183494
2026-01-02   MSFT  416.98  3117814
2026-01-02   NVDA  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 MSFT:

msft = alt[alt["Ticker"] == "MSFT"]

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 NVDA 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"] == "NVDA"].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.