pandas Fundamentals: Series, DataFrames, the DatetimeIndex, Selecting and Filtering, and Resampling Daily Bars to Weekly
Understand Series and DataFrames, index price data by date, select rows with loc and iloc, filter with boolean masks and aggregate daily bars into weekly bars.
What you will learn
- What a
Seriesand aDataFrameare and why theDatetimeIndexis the backbone of price analysis. - The difference between label-based
locand position-basediloc, and how boolean masks filter rows. - How to build weekly or monthly bars from daily bars with
resampleand the correct aggregation per column. - Which pandas defaults changed in version 3.0 and how to write code that behaves the same on 2.2 and 3.x.
Explanation
A Series is a one-dimensional labelled array: a column of closes with a date attached to each value. A DataFrame is a table of Series that share one index: open, high, low, close and volume side by side. The index holds the row labels; for price data it should be a DatetimeIndex, a specialised index of timestamps that enables date slicing, resampling and time-zone handling. Convention: import pandas as pd.
Create a frame with a dictionary of columns and a date index:
idx = pd.bdate_range("2025-03-03", periods=10)
df = pd.DataFrame({"open": [...], "high": [...], "low": [...], "close": [...], "volume": [...]}, index=idx)
bdate_range produces weekdays only and knows nothing about exchange holidays; real data comes from files (lesson 4). Inspect with df.head(), df.info() (types and missing values) and df.describe(). df.index.dtype must show datetime64; if it shows object, the dates are still text.
Selecting. df["close"] returns a Series; df[["open", "close"]] (a list) returns a DataFrame. df.loc[row_labels, columns] selects by label: df.loc["2025-03-05":"2025-03-07", "close"] gives the closes for those three dates, and with loc both ends of a slice are included, unlike Python lists. df.iloc[0:3] selects by integer position (first three rows, end excluded) and df.iloc[-1] is the latest bar.
Filtering. A comparison on a Series produces a Series of booleans, a mask: up = df["close"] > df["open"]. Passing the mask selects the matching rows: df[up]. Combine masks with & (and), | (or) and ~ (not), each condition in its own brackets: df[(df["close"] > df["open"]) & (df["volume"] > 1200)]. up.sum() counts True values, and up.mean() is the fraction of up days.
Adding columns is assignment: df["range"] = df["high"] - df["low"]. Operations are vectorised, applied to whole columns at once, which is why pandas is fast. shift(1) moves values down one row (yesterday's close on today's row) and pct_change() gives percentage changes; both leave NaN (not a number) in the first row.
Resampling. resample groups rows into fixed time buckets. Each column needs its own rule: the weekly open is the first daily open, the high is the maximum, the low the minimum, the close the last, and volume the sum:
rules = {"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"}
weekly = df.resample("W-FRI").agg(rules)
The code "W-FRI" labels each week by its Friday. Monthly bars use "ME" (month end). Older tutorials write "M"; pandas 2.2 deprecated that alias in favour of "ME" (likewise "QE" and "YE"), and code written with the new aliases runs on both 2.2 and 3.x.
pandas 3.0 changes worth knowing. Copy-on-Write is now the only mode: a filtered frame is an independent copy, so df[mask]["close"] = 0 never modifies df; write df.loc[mask, "close"] = 0. Text columns now have dtype str rather than object.
Worked example
Hypothetical example: ten daily bars from Monday 3 March to Friday 14 March 2025, prices in euros, no holidays.
| Date | Open | High | Low | Close | Volume |
|---|---|---|---|---|---|
| 03 Mar | 100 | 102 | 99 | 101 | 1,000 |
| 04 Mar | 101 | 104 | 100 | 103 | 1,200 |
| 05 Mar | 103 | 104 | 101 | 102 | 900 |
| 06 Mar | 102 | 105 | 101 | 104 | 1,100 |
| 07 Mar | 104 | 106 | 103 | 105 | 1,500 |
| 10 Mar | 105 | 106 | 103 | 104 | 1,300 |
| 11 Mar | 104 | 107 | 103 | 106 | 1,000 |
| 12 Mar | 106 | 108 | 105 | 107 | 1,400 |
| 13 Mar | 107 | 109 | 106 | 108 | 1,200 |
| 14 Mar | 108 | 110 | 106 | 107 | 1,600 |
Resampling gives two rows. Week ending 7 March: open = first open = 100; high = max(102, 104, 104, 105, 106) = 106; low = min(99, 100, 101, 101, 103) = 99; close = last close = 105; volume = 1,000 + 1,200 + 900 + 1,100 + 1,500 = 5,700. Week ending 14 March: open 105, high 110, low 103, close 107, volume = 1,300 + 1,000 + 1,400 + 1,200 + 1,600 = 6,500.
Filtering: (df["close"] > df["open"]).sum() returns 7, because only 5, 10 and 14 March closed below their open. df.loc["2025-03-05":"2025-03-07", "close"] returns 102, 104, 105.
Common mistake
df.iloc[0:5] returns five rows (positions 0 to 4), but df.loc["2025-03-03":"2025-03-07"] returns every row up to and including 7 March. Use iloc for "the first N" or "the last bar", and loc with explicit dates for periods.Practice activity
Using the ten-bar table above and working on paper: (a) write the pandas expression that selects the bars from 10 to 12 March inclusive; (b) state what df.iloc[-3:]["close"].tolist() returns; (c) compute the single monthly bar that df.resample("ME").agg(rules) would produce; (d) explain what goes wrong if someone writes df.resample("ME").last() for all columns.
Show solution
(a) df.loc["2025-03-10":"2025-03-12"], three rows, both ends included.
(b) The last three closes: [107, 108, 107].
(c) One row labelled 31 March 2025 (month end): open 100 (first), high 110 (max), low 99 (min), close 107 (last), volume 12,200 (5,700 + 6,500).
(d) .last() takes the final value of every column: the monthly low would be 106 instead of 99, the open 108 instead of 100 and the volume 1,600 instead of 12,200. Always pass the per-column rules.
Limitations: the example has no holidays or missing days; with real data, a week containing a holiday simply aggregates four bars, and a month label of "31 March" appears even if the last trading day was 28 March.
Key takeaways
- Price data lives in a DataFrame with a DatetimeIndex; check
df.index.dtypebefore anything else. locselects by label and includes the slice end;ilocselects by position and excludes it.- Boolean masks combined with
&,|and~replace loops for filtering, and.sum()on a mask counts matches. - Resample OHLCV with first/max/min/last/sum, use the
"W-FRI"and"ME"aliases, and remember pandas 3.0's Copy-on-Write when assigning.
Next step
Lesson 4 brings in real data: reading CSV files and free APIs, understanding adjusted versus unadjusted prices, and handling missing values and time zones.
Self-check
Answer the questions, then check your answers. You can retry as often as you like.
Automatic self-check for your own revision. Your answers are checked in your browser and are not seen or assessed by us. It is not an exam and not a qualification.
Your progress
“Complete” only means you marked the lesson as done. It is not a test of your understanding. Progress and notes are saved only in this browser on this device — not in an account.