Pandas DataFrames
Load, clean, transform and explore real datasets. Every Pandas operation ML projects actually use — with DoorDash and Stripe examples throughout.
Real data never arrives as a clean NumPy array. It arrives as a mess.
NumPy is perfect for numerical computation — arrays of floats, matrix multiplications, vectorised operations. But real ML datasets are not pure numbers. They're a mix of dates, categories, free text, IDs, and numbers all in the same table. Some columns are missing values. Some have wrong types. Some need to be joined to other tables. Some need to be grouped, aggregated, and reshaped before a model can touch them.
This is Pandas' job. It provides the DataFrame — a table with named columns, mixed types, and an enormous API for loading, cleaning, exploring, transforming, and exporting tabular data. Every single ML project starts in Pandas before the data ever reaches sklearn or PyTorch.
The running dataset in this module is a simulated DoorDash order table — 10,000 rows with order IDs, restaurant names, distances, delivery times, ratings, and some intentional data quality issues. By the end of this module you'll have cleaned it, explored it, engineered features from it, and prepared it for a model.
What this module covers:
Series and DataFrame — the building blocks
Pandas has two primary objects. A Series is a one-dimensional labelled array — like a single column of a spreadsheet. A DataFrame is a two-dimensional labelled table — like a full spreadsheet. Every DataFrame is a collection of Series sharing the same index.
Reading from CSV, JSON, SQL, Excel and Parquet
Most ML datasets come from files or databases. Pandas can read almost any format. The options you pass to these read functions directly determine data quality — getting them right saves hours of cleaning later.
The first five commands on every new dataset
Before touching a dataset you should always run the same five exploration commands. They take 30 seconds and reveal 90% of the data quality problems you'll spend hours debugging later if you skip them.
Selecting rows and columns — loc vs iloc vs direct access
Pandas has three ways to select data. Direct column access with df['col'] for columns. .loc for label-based selection (use column names and index labels). .iloc for integer position-based selection (use numbers like NumPy). Mixing these up is the most common Pandas mistake beginners make.
Handling missing values — detect, understand, decide, fix
Missing data is in every real dataset. The question is never "is there missing data?" but "why is it missing and what should I do about it?" There are three reasons data goes missing, and each calls for a different treatment.
The missingness has nothing to do with the data. A sensor randomly dropped readings. A survey respondent accidentally skipped a question. Safe to drop or impute with column mean/median.
Missingness depends on other observed variables but not on the missing value itself. More careful imputation needed — use information from correlated columns.
Missingness depends on the missing value itself. Dangerous — simple imputation introduces bias. Requires domain knowledge and careful handling.
apply, map and vectorised operations — transform any column
Transforming columns is the core of feature engineering. Pandas gives you three mechanisms: vectorised operations (fastest — use whenever possible), .map() for element-wise transformation of a Series, and .apply() for row-wise or column-wise operations on a DataFrame. Use them in that order of preference — vectorised operations are 100× faster than apply loops.
GroupBy — the most powerful Pandas operation
GroupBy splits the DataFrame into groups based on one or more columns, applies a function to each group, and combines the results. This is the core of almost all exploratory data analysis and feature engineering. It answers questions like "what is the average delivery time per restaurant?" or "which city has the highest fraud rate?" — the questions you answer before deciding what features to build.
Merge and join — combine data from multiple sources
Real ML projects always involve multiple tables. Orders table. Customers table. Restaurants table. Weather data. All need to be joined together before you can train a model. Pandas merge is SQL JOIN — if you know SQL joins, this is identical. If you don't, the examples below will make it clear immediately.
String operations — the .str accessor
Real datasets are full of string columns — restaurant names, addresses, product descriptions, customer comments. Before feeding them to a model you need to clean and extract information from them. The .str accessor applies string methods to every element of a Series in one vectorised call.
DateTime features — extract time-based signals for ML
Time columns are one of the richest sources of features in ML. Hour of day, day of week, month, whether it's a holiday, days since last event — these consistently improve models for delivery time, demand forecasting, fraud detection, and anything with temporal patterns. Pandas makes extracting them trivial.
From DataFrame to NumPy — prepare data for model training
After all the loading, cleaning, and feature engineering, the final step is converting the DataFrame to NumPy arrays that sklearn, PyTorch, or XGBoost can consume. This bridge is where most beginners make the mistakes that silently corrupt model training — leaking the test set into the training pipeline, not handling categoricals correctly, or fitting scalers on the wrong data.
Every common Pandas error — explained and fixed
Pandas is the bridge between raw data and a model — not the destination
On a real ML team, nobody trains a model directly against a SQL table or a folder of Parquet files. There is always a pandas layer in between. A data scientist pulls a slice of data into a notebook, runs the same five exploration commands from Module 2, then spends the bulk of the project time in groupby, merge, and transform calls — computing exactly the kind of group aggregates and rolling features you saw in Sections 8 and 9. Only once those features exist as clean numeric columns does the data leave pandas and become a NumPy array for sklearn or a tensor for PyTorch.
This is why pandas fluency is graded so heavily in ML interviews and take-home exercises: it is the tool every practitioner touches on every single project, regardless of whether the eventual model is a logistic regression or a transformer. The modelling library changes from project to project. The pandas step in front of it does not.
Pandas also has a real, well-known ceiling — and knowing where it is matters as much as knowing the API. Pandas is single-threaded for most operations and holds the entire DataFrame in memory. On a typical laptop or a mid-size cloud instance, that comfortably covers datasets from a few thousand rows up to tens of millions of rows — which is the overwhelming majority of feature-engineering workloads at most companies. Past that, two things start to hurt: a groupby or merge that used to take a second starts taking minutes, and the DataFrame itself no longer fits in RAM.
Five things people get wrong about Pandas
Pandas comfortably handles datasets from a few thousand rows up to tens of millions of rows, and from a few megabytes up to low tens of gigabytes, on a single reasonably resourced machine. That range covers the overwhelming majority of real feature-engineering work at most companies. It is not a toy library you graduate out of — it is the default tool at nearly every ML team for anything that fits in memory, which is most things.
The warning exists because pandas genuinely cannot always tell whether the intermediate object your chain produced is a view into the original data or an independent copy. That means an assignment like df[mask]['col'] equals value may silently modify the original DataFrame, may silently do nothing at all, or may work today and break after an unrelated pandas version upgrade. It is not a style nit — it is a real correctness bug waiting to happen, and the fix (df.loc[mask, 'col'] = value) costs nothing.
It is one of the single most common real-world pandas bugs, not an edge case. It happens constantly in ordinary code: filter a DataFrame with a boolean mask, then try to add or modify a column on the result. Whether that result is a view or a copy depends on internal details of how the filter was executed, not on anything visible in your code. The reliable fix is a habit, not a rare-case check: call .copy() explicitly the moment you intend to modify a filtered subset.
.apply() with axis=1 is a Python-level loop over rows under the hood, no different in spirit from a manual for loop — pandas is just managing the bookkeeping for you. A true vectorised operation instead calls into compiled C code that processes the whole column at once. On a dataset of a few hundred rows the difference is invisible; on a few million rows, apply(axis=1) can be fifty to a hundred times slower. Reach for it only when a vectorised or .map() alternative genuinely does not exist.
A bigger machine helps with memory pressure, but pandas is single-threaded for most operations — a groupby or merge that is compute-bound will not run meaningfully faster just because the machine has more cores sitting idle. Past a certain data size, teams do not keep scaling pandas vertically; they switch to Polars (multi-threaded, a very similar API, often a near drop-in replacement) or to Spark (distributed across many machines) specifically because pandas itself was never built to use more than one core for its core operations.
Pandas — 5 questions interviewers actually ask
It appears whenever pandas cannot guarantee that the object you are about to modify is the original DataFrame rather than a temporary view or copy produced by a previous selection. The classic trigger is chained indexing: df[df['city'] == 'Seattle']['rating'] = value, where the first bracket produces an intermediate object of ambiguous origin. The permanent fix is to never chain: use df.loc[mask, 'rating'] = value to modify the original directly, or call .copy() explicitly right after filtering if you intend to work with an independent subset instead.
Vectorised operations push the loop down into compiled C code that operates on an entire NumPy-backed column at once; .apply(), especially with axis=1, is still a Python-level loop calling your function once per row, with all the interpreter overhead that implies. I would reach for .apply() only when the row-wise logic genuinely cannot be expressed as column-level arithmetic or a small set of vectorised conditions — for example, calling an external function per row, or logic with many interacting business rules. Whenever a vectorised or pd.cut / np.where alternative exists, that is almost always the better choice, especially at scale.
First I would check dtypes with df.info(memory_usage='deep') — object columns and float64 are usually the biggest offenders. Concretely: downcast numeric columns with pd.to_numeric(..., downcast equals 'float' or 'integer'), convert low-cardinality string columns to the category dtype (which stores each unique value once instead of repeating it per row), and read only the columns actually needed with usecols at load time rather than dropping them afterward. If the dataset still does not fit, I would switch to chunked reading with chunksize, or move to Polars or a columnar format like Parquet that only decompresses the columns you touch.
I would start with df.isnull().sum() and the percentage version to see which columns are affected and how badly. Then I would check whether the missingness looks random or patterned — for example, grouping by another column and comparing missing rates, the same check shown in Section 6 of this module. Only after understanding the pattern would I choose a fix: drop for MCAR columns with very low missing rates, group-based imputation for MAR columns, and a much more careful, domain-informed approach for MNAR columns, since naive imputation there actively introduces bias rather than removing it.
merge() is pandas' general-purpose SQL-style join — it combines two DataFrames on one or more key columns and supports inner, left, right, and outer joins explicitly. .join() is a convenience wrapper around merge that combines on the index by default, which is useful when two DataFrames already share a meaningful index but is otherwise just merge with different defaults. concat() does not match on keys at all — it stacks DataFrames either vertically (combining rows from multiple files with identical columns) or, less commonly, horizontally. I use merge for combining related tables on a key, and concat for stacking multiple extracts of the same shape.
You can now take any real dataset from raw file to model-ready array.
The programming ecosystem section is complete. Python, NumPy, and Pandas — the three tools every ML engineer uses every day, and that every ML library is built on top of. Every algorithm in the Classical ML section assumes you can load data, explore it, clean it, engineer features, and convert it to a NumPy array. You can do all of that now.
Module 11 begins the Data Engineering section with data collection — pulling data from REST APIs, SQL databases, file systems, and web scraping. In production ML, the data you get from your company's systems is never as clean as the datasets in tutorials. The next section closes that gap.
Where ML data actually comes from and how to pull it reliably — REST APIs, SQL queries, Parquet files, and web scraping.
🎯 Key Takeaways
- ✓A Series is a 1D labelled array (one column). A DataFrame is a 2D labelled table (multiple columns sharing one index). Every DataFrame is a dict of Series.
- ✓Always run df.info(), df.head(), df.describe(), df.isnull().sum(), and df.value_counts() on every new dataset before touching it. These five commands reveal 90% of data quality issues.
- ✓.loc selects by label (column names, index labels) — end label IS included. .iloc selects by integer position (like NumPy) — end position NOT included. Never chain them: df.loc[mask]['col'] = val is always wrong — use df.loc[mask, col] = val.
- ✓Missing data has three types: MCAR (safe to impute with statistics), MAR (use correlated columns), MNAR (dangerous — requires domain knowledge). Always check whether missingness is random before choosing an imputation strategy.
- ✓GroupBy is split-apply-combine. Use .agg() for multiple aggregations, named aggregations for clean output, .transform() to add group statistics back to every row (essential for feature engineering without changing DataFrame shape).
- ✓Always fit scalers and encoders on training data only, then transform both train and test. Fitting on the full dataset leaks test information into training — a silent bug that inflates evaluation metrics. Use sklearn Pipeline to prevent leakage automatically.
- ✓For time columns use the .dt accessor to extract hour, day_of_week, month, is_weekend etc. Use sine/cosine encoding for cyclical features (hour, day of week) so that hour 23 and hour 0 are recognised as numerically adjacent.
Discussion
0Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.