Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Open In Colab Open In Kaggle

pandas is the workhorse for labelled, tabular data: the Series (a labelled 1D array) and the DataFrame (named columns sharing an index). This notebook loads a small table of daily station observations — air temperature and river discharge at two stations, with realistic sensor gaps — and works through reading, selecting, time-indexing, rolling windows, grouping, and joining. The running theme is treating missing data as physical information the sensor recorded, rather than smoothing it away — exactly the choice the generated-code bug at the end gets wrong.

1.5.1 Series and DataFrame

A Series pairs values with an index; a DataFrame is a collection of columns sharing one index. Each column has its own dtype.

2024-06-01    18.2
2024-06-02    17.5
2024-06-03    19.1
Name: temp_celsius, dtype: float64
   temp_celsius  discharge_m3s
0          18.2           48.0
1          17.5           51.2
2          19.1           47.5
{'temp_celsius': dtype('float64'), 'discharge_m3s': dtype('float64')}

1.5.2 Reading a CSV Robustly

read_csv infers types, but for analysis you should be explicit: parse date columns to datetime64, and pin the dtype of key columns. First we generate an example file; csv writing is covered near the end.

wrote station_observations.csv with 90 rows
{'date': dtype('<M8[us]'), 'station': <StringDtype(na_value=<NA>)>, 'temp_celsius': dtype('float64'), 'discharge_m3s': dtype('float64')}
        date station  temp_celsius  discharge_m3s
0 2024-06-01     BAS          18.2           48.4
1 2024-06-02     BAS          17.8           57.3
2 2024-06-03     BAS          19.0           59.8
3 2024-06-04     BAS           NaN           59.0
4 2024-06-05     BAS           NaN           56.6

1.5.3 Selecting: .loc, .iloc, and Boolean Masks

.loc selects by label, .iloc by integer position, and a boolean mask filters rows by condition. Combine several conditions with & (and) and | (or) — not Python’s and/or from subchapter 1.2, which do not work element-wise on a Series — and wrap each condition in its own parentheses, since &/| bind more tightly than comparisons like == and >, so omitting them changes what gets evaluated.

         date  temp_celsius
46 2024-06-02          23.4
56 2024-06-12          22.5
59 2024-06-15          22.3
71 2024-06-27          23.1
72 2024-06-28          22.1
{'date': Timestamp('2024-06-01 00:00:00'), 'station': 'BAS', 'temp_celsius': 18.2, 'discharge_m3s': 48.4}
BAS

1.5.4 Updating and Transforming Values

A column is created or overwritten by assignment, applied to every row at once. A boolean mask combined with .loc updates only the matching rows, leaving the rest untouched.

  station  temp_celsius  temp_fahrenheit  flow_flag
0     BAS          18.2            64.76        NaN
1     BAS          17.8            64.04  high_flow
2     BAS          19.0            66.20  high_flow

1.5.5 A datetime Index: resample and Rolling Windows

Setting a datetime index unlocks time-aware operations. resample re-bins to a coarser period; rolling computes a moving window.

monthly mean °C: [17.82, 18.39]
7-day rolling mean discharge (m3 s-1):
[nan, nan, nan, nan, nan, nan, 53.84, 54.07, 53.5, 51.19]

1.5.6 groupby: Split, Apply, Combine

groupby splits rows by a key, applies an aggregation to each group, and combines the results.

         mean_temp  max_discharge  n_obs
station                                 
BAS          18.02           60.0     45
LUG          20.88           39.1     45

nunique counts how many distinct values a column holds; idxmax (or idxmin) returns the label of the row holding the maximum (minimum) value — useful directly on a groupby result like summary above.

distinct stations: 2
station with the warmest mean temperature: LUG

1.5.7 Quick Plots with .plot()

Series and DataFrames carry a .plot() method that wraps matplotlib: it reads the index for the x-axis and the column names for the legend, so a first look needs no fig, ax boilerplate. Pass kind= to choose the plot type, and ax= to draw into axes you already created — return to the explicit figure/axes model from subchapter 1.4 whenever a plot needs more control than this gives you. For right-skewed data — many small values and a few very large ones — pass logy=True to put the count axis on a log scale, so a long tail does not crush everything else into one corner.

<Figure size 600x300 with 1 Axes>

kind="bar" turns a grouped summary into a labelled bar chart in one line, reusing summary from the groupby above.

<Figure size 500x300 with 1 Axes>

Many environmental quantities are right-skewed: most values sit near a typical range, with a long tail of rare, much larger ones. A linear count axis buries that tail near zero; passing logy=True spreads it out so both the common and the rare values are visible in the same plot.

<Figure size 900x300 with 2 Axes>

1.5.8 Missing Data as Physical Information

When a sensor stops reporting, the record should show that gap, not invent a reading. pandas marks missing values as NaN, detects them with isna, and its reductions skip them by default. How you fill a gap is a modelling choice: ffill carries the last value forward (sensible for slowly varying state), interpolate draws a straight line between neighbours.

{'date': 0, 'station': 0, 'temp_celsius': 3, 'discharge_m3s': 0, 'temp_fahrenheit': 3, 'flow_flag': 79}
with gaps:   [18.2, 17.8, 19.0, nan, nan, 18.5]
ffill:       [18.2, 17.8, 19.0, 19.0, 19.0, 18.5]
interpolate: [18.2, 17.8, 19.0, 18.83, 18.67, 18.5]

1.5.9 Combining Tables: merge and concat

merge joins tables on a shared key (a database-style join); concat stacks tables along an axis.

        date station             name  elevation_m  temp_celsius
0 2024-06-01     BAS  Basel-Binningen          316          18.2
1 2024-06-02     BAS  Basel-Binningen          316          17.8
2 2024-06-03     BAS  Basel-Binningen          316          19.0

1.5.10 Writing Outputs

Save results to disk with a pathlib path, which keeps the code OS-independent.

wrote station_summary.csv - 99 bytes

When generated code lies: filling gaps with zero

Asked to “clean and average” a column with gaps, an assistant fills the missing values with zero and then takes the mean. The code runs, but zero is a valid temperature, so the gaps become spurious cold readings that drag the mean down.

buggy mean:   17.22
correct mean: 18.02
gaps filled with 0 °C: 2
fixed mean: 18.02

Summary

ConceptRule to remember
Series and DataFrameA Series is a labelled 1D array; a DataFrame is named columns sharing one index.
ReadingBe explicit: parse_dates for time columns, dtype for key columns.
Selecting.loc by label, .iloc by position, boolean masks to filter.
TimeA datetime index is what enables resample and rolling.
DerivingAssign to a column, or to a masked .loc selection — no loop needed.
Grouping and joininggroupby is split-apply-combine; merge joins on a key, concat stacks.
Missing dataMissing is not zero: detect with isna, and fill only as a deliberate physical choice.
The zero trapfillna(0) silently biases every statistic whenever zero is itself a valid value.

Resources