📋 Pandas Analyst Path · cheat sheet, built from the lessons

Part 1 · Meet the data · Week 1, Monday

1.1Anatomy of a DataFrame

📘 Concept

CommandAnswers
df.shape(rows, columns): an attribute, no brackets
df.head(n) / df.tail(n) / df.sample(n)first / last / random n rows
df.columns, df.dtypescolumn names, their types
df.info()types + non-null counts + memory, all at once
df.describe()count, mean, std, min, quartiles, max of numeric columns
df.describe(include="all")the same + text columns (count, unique, top, freq)
df["col"].mean() .median() .min() .max() .sum() .std()one number from a column

1.2Selecting columns

📘 Concept

1.3Counting categories: value_counts, unique, nunique

📘 Concept

CodeReturns
s.value_counts()count per value, sorted from most to least frequent
s.value_counts(normalize=True)share (0–1) instead of count
s.value_counts(dropna=False)also counts missing values
s.unique() / s.nunique()the distinct values / how many distinct values
s.value_counts().idxmax()the most frequent value (also .index[0])

💡 A boolean trick you'll use daily: (s == "X").mean() = share of rows where s is X (True=1, False=0).

🧩 A Series has two parts. The result of value_counts() looks like a two-column table, but it is a Series:

So "which ones?" questions (names, top 3, the biggest) are answered by the index, and "how many?" questions by the values. .head(3) keeps the first 3 rows, .index takes their labels, list(...) makes them a plain list (same trick as list(products.columns) in 1.2).

1.4Sorting and top‑N

📘 Concept

Part 2 · Find the right rows · Tuesday

2.1Boolean masks: the filter of pandas

📘 Concept 1. A condition on a column gives a Series of True/False: products["retail_price"] > 100. 2. Put it inside df[...] and only the True rows stay: products[products["retail_price"] > 100]. 3. Combine with & (and), | (or), ~ (not), and each condition goes in parentheses.

NeedCode
equals one of several valuesdf["col"].isin(["A", "B"])
inside a range (both ends included)df["col"].between(10, 20)
text containsdf["col"].str.contains("jean", case=False, na=False)
missing / not missingdf["col"].isna() / df["col"].notna()
how many rows matchmask.sum()
what share of rows matchmask.mean()

⚠️ Traps: and / or → ValueError: The truth value of a Series is ambiguous. Use & / |. Missing parentheses: df["a"] > 1 & df["b"] < 5 is evaluated in the wrong order → wrong rows or an error.

2.2loc and iloc: rows and columns together

📘 Concept

⚠️ Biggest trap: loc[0:5] includes 5 (labels), iloc[0:5] excludes 5 (positions). ⚠️ Second trap: df[df["a"] > 1]["b"] = 0 does nothing useful (it changes a temporary copy, and pandas warns with SettingWithCopyWarning). Write df.loc[df["a"] > 1, "b"] = 0.

Part 3 · New columns · Wednesday

3.1Calculated columns

📘 Concept

3.2Categories from conditions: np.where, np.select, map, apply

📘 Concept

SituationTool
two outcomesnp.where(condition, value_if_true, value_if_false)
several ordered rulesnp.select([cond1, cond2, ...], [val1, val2, ...], default=...): the first true condition wins
translate values with a dictionarys.map({"old1": "new1", "old2": "new2"}): unmapped values become NaN
any custom Python functions.apply(func): flexible but slow (row-by-row), so use it as a last resort

3.3Binning numbers: pd.cut and pd.qcut

📘 Concept

3.4Text columns: the .str accessor

📘 Concept: .str applies string methods to every value of a column: .str.lower() .str.upper() .str.title() .str.strip() .str.len() .str.contains("x", case=False, na=False) .str.startswith("x") .str.replace("a", "b") .str.split(" ") → lists, .str[0] → first element / first character.

⚠️ Trap: brand has missing values. str.contains returns NaN for them and the mask fails with "Cannot mask with non-boolean array containing NA". Fix: na=False.

Part 4 · Dates and durations · Thursday

4.1Datetime basics

📘 Concept

4.2Filtering by date ranges

📘 Use half-open intervals: start <= date < first day after the period.

⚠️ Trap: o["created_at"].between("2026-07-01", "2026-09-30") stops at 2026-09-30 00:00:00, so almost the whole last day is lost. Timestamps have hours.

Part 5 · Clean a messy file · Friday

5.1Reading files

📘 pd.read_csv(path, sep=",", decimal=".", encoding="utf-8", parse_dates=[...], dtype={...})

5.2Missing values

📘 Concept

CodeDoes
df.isna().sum()missing values per column
df.isna().mean()share missing per column
df.dropna(subset=["col"])drop rows where col is missing
df["col"].fillna(value)fill missing, e.g. with 0, "Unknown", df["col"].median()

Drop or fill? Drop when few rows are affected and they're random. Fill when the value has a clear meaning (missing spend = no purchase → 0) or you'd lose too much data (age → median). Always say which you did.

⚠️ Trap: text like "unknown" or "" in a number column is not NaN to pandas. It makes the whole column text (object). isna() won't count it, and you only see it after converting (5.4).

5.3Duplicates

📘 df.duplicated() marks rows that repeat an earlier row (all columns equal). df.duplicated(subset=["id"]) compares only some columns. df.drop_duplicates(subset=..., keep="first" | "last") removes them.

The export has two kinds of duplicates: exact copy-paste repeats, and customers exported twice whose later row has an updated spend. For those, the last row is the truth.

5.4Fixing text and types

📘 Concept

5.5Outliers

📘 Concept: the IQR rule (box-plot rule, a Data Mining exam favourite): IQR = Q3 − Q1, outliers are below Q1 − 1.5·IQR or above Q3 + 1.5·IQR. Quantiles: s.quantile(0.25), s.quantile(0.75).

What to do with them? Investigate first. A $999 coat is real. An age of 230 is an error. Options: keep (and say so) · remove · cap (s.clip(lower, upper), called winsorizing) · analyse separately. For modelling (Part 12) capping is common. For revenue reporting, you never delete real sales.

Part 6 · Join tables · Week 2, Monday

6.1merge: the SQL JOIN of pandas

📘 Concept: left.merge(right, on="key", how="left")

how=keeps
"inner" (default)only keys present in both tables
"left"all rows of the left table; no match → NaN
"outer"everything from both

⚠️ The #1 merge bug: row explosion. If the key isn't unique on the side you think it is, rows multiply and your revenue doubles silently. Always compare row counts before and after, or let pandas check it: validate="many_to_one" raises an error if the right side has duplicate keys.

6.2concat: stacking tables

📘 pd.concat([df1, df2, df3], ignore_index=True) puts tables under each other (same columns), for example monthly exports. axis=1 puts them side by side (rare; prefer merge). ignore_index=True renumbers the rows 0…n−1.

Part 7 · groupby: the heart of analytics · Tuesday–Wednesday

7.1split → apply → combine

📘 df.groupby("key")["value"].agg_function() = split rows into groups by key, apply a function to each group's values, combine into a result indexed by the key.

`` category sale_price groupby("category")["sale_price"].sum() Jeans 100 ─┐ Jeans 50 ─┴─► Jeans 150 Socks 10 ───► Socks 10 ``

FunctionCounts / computes
.sum() .mean() .median() .min() .max()as usual, per group
.count()non-missing values per group
.size()rows per group (including missing)
.nunique()distinct values per group (e.g. orders, customers)

⚠️ Trap: one order has several items, so count() on order_id counts items. Number of orders = nunique().

7.2Several metrics at once: agg and named aggregation

📘 Concept ``python sales.groupby("category").agg( revenue=("sale_price", "sum"), # new_column=(source_column, function) orders=("order_id", "nunique"), items=("order_item_id", "count"), ) ` Named aggregation gives clean column names. Ratios (AOV, margin) are computed **after** aggregating: kpis["aov"] = kpis["revenue"] / kpis["orders"]`.

⚠️ Trap: the average of ratios ≠ the ratio of totals. AOV is total revenue / total orders, not the mean of item prices.

7.3Several keys and unstack

📘 groupby(["a", "b"]) gives a result with a two-level (Multi)Index. .unstack() moves the inner level into columns, turning a long list into a readable matrix. .stack() does the reverse.

7.4transform: group values back on every row

📘 agg gives one row per group. transform gives one value per original row (the group's result repeated), so you can compare each row to its group: ``python sales["cat_avg"] = sales.groupby("category")["sale_price"].transform("mean") sales["share_of_cat"] = sales["sale_price"] / sales.groupby("category")["sale_price"].transform("sum") `` Use it for: share within a group, difference from the group average, ranking inside groups.

7.5pivot_table and crosstab

📘 Concept

💡 Rate trick: the mean of a boolean column is a rate: items.assign(is_returned=items["status"].eq("Returned")).groupby("category")["is_returned"].mean()

Part 8 · Charts that make a point · Thursday

8.1Anatomy: figure, axes, and who draws what

📘 Concept

In the tasks below, keep your chart in a variable called ax. The check reads it (number of bars, title, labels…). show_me("8.x") draws a reference chart.

8.2Distributions: histogram and box plot

📘 "What do typical values look like? Is it skewed? Outliers?"

8.3Comparing categories: bar charts

📘 Aggregate first, then plot (pandas does the math, the chart only shows it).

8.4Relationships: scatter and heatmap

📘 sns.scatterplot(data=df, x="a", y="b", alpha=0.3, s=10): with many points, use transparency (alpha) or a sample (df.sample(5000, random_state=0)). sns.heatmap(matrix, annot=True, fmt=".2f", cmap="Blues") colours a table, for example a correlation matrix (df[cols].corr()) or a pivot table.

8.5Trends: line charts

📘 Time on the x-axis → a line. Aggregate per month first (Part 9 goes deeper): monthly = sales.groupby(sales["created_at"].dt.to_period("M"))["sale_price"].sum(). Drop the incomplete current month. A half month always looks like a crash.

8.6Choosing the chart and making it honest

QuestionChart
How are values distributed?histogram, box plot
Which category is biggest?sorted (horizontal) bar
How does it change over time?line
Are two numbers related?scatter (+ correlation)
Two categorical dimensions at onceheatmap of a pivot table
Parts of a wholestacked bar, or a pie with ≤ 4 slices

Checklist before sending: the title states the insight ("Returns doubled in Q3"), not just the topic ("Returns") · units on axes · sorted bars · bars start at zero · no incomplete periods · readable labels (rotation=45 or horizontal bars) · plt.tight_layout().

Part 9 · KPIs over time · Friday

9.1Grouping by time

📘 Concept

9.2Change over time

📘 Concept

CodeMeaning
s.diff()change vs previous row (absolute)
s.pct_change()change vs previous row (relative): MoM growth on monthly data
s.pct_change(12)vs 12 rows earlier: YoY on monthly data (removes seasonality)
s.shift(1)the previous row's value next to the current one (lag)
s.rolling(3).mean()3-month moving average: smooths noise
s.cumsum()running total (year-to-date)

⚠️ Rows must be sorted by time and have no missing months, otherwise shift(12) is not "a year ago".

Part 10 · Customer analytics: Pareto, cohorts, RFM, churn, funnel · Week 3

10.2Pareto: do 20% of customers bring 80% of revenue?

📘 Sort customers by revenue (desc), compute the cumulative share s.cumsum() / s.sum(), and read where the top 20% land. The same logic on products is ABC analysis (A = items making the first 80% of revenue, B = next 15%, C = the rest).

10.3Cohorts and retention

📘 Steps: (1) each order's month; (2) each customer's cohort = month of their first order (transform("min")!); (3) months since first order = (year diff) × 12 + (month diff); (4) pivot: cohorts × months-since, counting distinct customers; (5) divide each row by its month-0 size.

10.4RFM segmentation

📘 Three numbers per customer: Recency (days since last order: lower is better), Frequency (number of orders), Monetary (revenue). Score each 1–5 with pd.qcut, then name segments with rules (np.select).

⚠️ Trap: most customers have exactly 1 order, so pd.qcut(frequency, 5) fails with "Bin edges must be unique". The standard fix: rank first, pd.qcut(s.rank(method="first"), 5, ...). Be aware it splits tied customers arbitrarily. Another option is fixed bins with pd.cut (e.g. 1 / 2 / 3+ orders). For recency the labels go reversed ([5, 4, 3, 2, 1]): fewer days = better score.

10.5Churn: who have we lost?

> A customer is churned if they have made no paid order in the last N days before TODAY.

How to pick N? Look at how long returning customers usually wait between orders. If N is shorter than a normal gap, you call loyal customers "churned".

Who can churn? Only customers who have existed for at least N days. Someone whose first order was last week cannot have "stopped buying" yet. Including them makes churn look lower than it is. So: eligible = first_order <= TODAY − N days.

Churn rate = churned eligible customers / all eligible customers. Then compare churn between groups to find levers.

10.6Conversion funnel (website sessions, Jul–Sep 2026)

📘 A funnel counts how many sessions reach each step: visit → product page → cart → purchase. Step conversion = this step / previous step. Where the drop is biggest, there's the biggest opportunity.

Part 12 · Bonus for the Data Mining exam: correlation, encoding, scaling, k-means

12.1Correlation

📘 df.corr() gives a Pearson matrix (linear relationships, sensitive to outliers). df.corr(method="spearman") uses ranks, so it handles monotonic relationships and is robust to outliers. Values run from −1 to 1. Correlation ≠ causation.

12.2Encoding categorical variables

📘 Models need numbers.

12.3Scaling

📘 Distance-based methods (k-means, kNN) get dominated by the column with the biggest numbers (monetary in $ vs frequency 1–4).

12.4k-means segmentation

📘 KMeans(n_clusters=k, n_init=10, random_state=42).fit_predict(X) assigns each customer to the nearest of k centres. Choose k with the elbow of the inertia curve (plus business sense: can marketing run 4 campaigns? 10?). Then profile the clusters by averaging the unscaled features per cluster, and name them.

Appendix · Exam drill, cheat sheet, error decoder, glossary

A.2Cheat sheet

TaskCode
lookdf.shape df.head() df.info() df.describe() df["c"].value_counts(normalize=True)
selectdf["c"] df[["a","b"]] df.loc[mask, "c"] df.iloc[:5, :3]
filterdf[(df.a > 1) & (df.b.isin(["x","y"]))] · ~ not · .between(a, b) · .str.contains("x", case=False, na=False)
sort / topdf.sort_values("c", ascending=False).head(10) · df.nlargest(10, "c")
new columndf["n"] = df.a / df.b · np.where(cond, x, y) · np.select(conds, vals, default) · s.map(dict)
binspd.cut(s, bins, labels) · pd.qcut(s, 4, labels)
datespd.to_datetime(s, dayfirst=True) · .dt.year/.month/.day_name()/.to_period("M") · (d2 - d1).dt.days
missingdf.isna().sum() · dropna(subset=[...]) · fillna(value)
duplicatesdf.duplicated().sum() · drop_duplicates(subset=[...], keep="last")
typespd.to_numeric(s, errors="coerce") · s.astype(int) · s.str.strip().str.title()
outliersq1, q3 = s.quantile([.25, .75]) · fence q3 + 1.5*(q3-q1) · s.clip(upper=...)
joina.merge(b, on="key", how="left", validate="many_to_one") · pd.concat([a, b], ignore_index=True)
groupdf.groupby("k")["v"].sum() · .agg(name=("col","func")) · .transform("mean") · .nunique()
pivotdf.pivot_table(index, columns, values, aggfunc, fill_value=0) · pd.crosstab(a, b, normalize="index") · .unstack()
timegroupby(dt.to_period("M")) · pct_change() · pct_change(12) · shift() · rolling(3).mean() · cumsum()
chartsfig, ax = plt.subplots() · sns.histplot / boxplot / barplot / countplot / lineplot / scatterplot / heatmap(..., ax=ax) · ax.set_title()
ML prepdf.corr() · pd.get_dummies(df, columns=[...], drop_first=True) · (X - X.mean()) / X.std(ddof=0) · KMeans(...).fit_predict(X)

A.3Error decoder: "why didn't it work?"

You seeIt meansFix
KeyError: 'Revenue'no such column (typo, case, or it was never created)df.columns.tolist(); column names are case-sensitive
NameError: name 'x' is not definedthe cell defining x wasn't run (Colab restarted?)run ⚙️ Setup + ▶ Part setup + your earlier cells
ValueError: The truth value of a Series is ambiguousand/or/if on a whole column&, ``, and parentheses around each condition
TypeError: '>' not supported between 'str' and 'int'a "number" column is actually textpd.to_numeric(s, errors="coerce")
TypeError: 'tuple' object is not callabledf.shape()df.shape (no brackets)
SettingWithCopyWarningyou changed a filtered view.copy() after filtering, or df.loc[mask, "c"] = v
ValueError: Bin edges must be uniqueqcut on a column with many equal valuess.rank(method="first") first, or pd.cut
ValueError: cannot mask with non-boolean array containing NAstr.contains on a column with NaNna=False
MergeError ... not a many-to-one mergethe right table has duplicate keysdedupe or aggregate the right table before merging
numbers suddenly 2–10× too big after a mergerow explosion (non-unique key)check len() before/after, use validate=
column full of NaN after mapthe dict didn't cover those values (case? spaces?)normalise text first, or use replace
chart drops at the endincomplete last periodfilter it out or label it

A.4Glossary EN → RU

ENRU
DataFrame / Series / indexтаблица / столбец (одномерный массив с метками) / индекс (метки строк)
boolean maskбулева маска (True/False для каждой строки)
missing value (NaN, NaT)пропуск (числовой / дата)
outlier / IQR / to cap (clip, winsorize)выброс / межквартильный размах / ограничить «потолком»
to merge / join / anti-joinобъединить таблицы / соединение / «строки без пары»
row explosion«размножение» строк при неуникальном ключе
aggregate / named aggregationагрегировать / именованная агрегация
revenue / profit / marginвыручка / прибыль / маржа (доля прибыли в выручке)
AOV (average order value)средний чек
return rate / cancellation rateдоля возвратов / доля отмен
MoM / YoY / YTDк прошлому месяцу / к прошлому году / с начала года
moving (rolling) averageскользящее среднее
cohort / retention / churnкогорта / удержание / отток
repeat rateдоля повторных покупателей
RFM (recency, frequency, monetary)давность, частота, деньги
funnel / conversionворонка / конверсия
Pareto / ABC analysisпринцип Парето / ABC-анализ
one-hot encodingone-hot кодирование (дамми-переменные)
standardisation / normalisation (min-max)стандартизация (z-score) / нормализация к 0…1
clustering / centroid / inertiaкластеризация / центр кластера / сумма квадратов расстояний
insight / finding / recommendationинсайт / вывод / рекомендация

Nothing found. Try a shorter word.