Data for Everyone
1 · Python fundamentals

Not a tour of the language — only what an analyst actually uses, and each idea explained by why it exists, not just how it is typed.

1.1 · Types: a promise about a value

A type tells you what you may do with a value. Numbers add up, strings glue together. Mix them up and "120" + "80" silently becomes "12080" instead of 200 — a bug that raises no error and simply puts a wrong number in your report.

None deserves special care: it means "there is no value", which is not zero. A user who never bought has revenue None; a user who bought and refunded everything has 0. Treating them the same shifts your average.

1.2 · Structures: list, dict, set

The structure you choose decides both readability and speed. The rule: list when order matters, dict when you look values up by key, set when you need uniqueness or a fast "is it there?" check.

The speed gap is not academic. A list checks elements one by one; a set jumps straight to the answer through a hash. Inside a loop over a million rows that is the difference between seconds and hours.

Practical rule: if you test membership inside a loop, convert the list to a set first. One line, and a slow script stops being slow.
1.3 · Conditions: teaching code to decide

An if asks a yes/no question and runs a block only when the answer is yes. With elif and else you build a chain: Python checks the conditions top to bottom and runs the first branch that is true — then skips the rest. That is exactly how you segment users by revenue, or turn a p-value into a verdict.

Python also treats values as true or false on their own — "truthiness". Zero, an empty string, an empty list and None count as false; everything else counts as true. That is why if orders: reads naturally as "if there are any orders".

The rule that prevents bugs: in an elif chain put the narrowest condition first. If revenue >= 100 came before revenue >= 1000, a VIP with 2500 would match the first line and be labelled "regular" — the VIP branch would never run.
1.4 · Loops: doing something for every element

A for loop takes a collection and runs the same block once for each element. Three helpers make it far more useful: range(n) produces the numbers 0 to n−1, enumerate hands you the position and the value, and zip walks two lists side by side.

A while loop repeats as long as a condition holds — useful when you do not know in advance how many steps you need, like "how many months until the balance reaches 1500?". Its danger is the infinite loop: if nothing inside changes the condition, it never stops. break leaves a loop early, continue skips straight to the next element.

Two habits worth building early: use for when you know what you are iterating over and keep while for "until something happens". And never write for i in range(len(items)) just to get an index — enumerate gives you both, and reads as plain English.
Watch a loop run, step by step

Reading loop code is easier once you have seen one run. The code below counts paid orders and their total. The table under it was produced by running that exact code and recording every variable after each pass — this is what the computer actually does, line by line.

amounts = [120, 0, 340, 55]   # order amounts; 0 = not paid

total = 0                      # accumulators start BEFORE the loop
paid = 0
for amount in amounts:         # amount takes each value in turn
    if amount > 0:
        total += amount        # same as: total = total + amount
        paid += 1

print(paid, total)             # 3 515
#amountamount > 0totalpaid
1120✓ True1201
20False1201
3340✓ True4602
455✓ True5153
Three things to notice. The loop variable amount takes each value in turn. total and paid are created before the loop and only grow inside it — that is the "accumulator" pattern behind every sum and count. And on row 2 the if is false, so nothing changes: the loop simply moves on.
1.5 · Comprehensions: a loop in one line

A comprehension builds a collection in one line. The gain is not brevity but intent: it says "this is a transformation of that", instead of "make an empty list, then append over and over".

1.6 · Functions: why they exist

A function is how you stop copy-pasting. The real benefit is not elegance: when a formula changes you fix it once instead of hunting every copy — and a copy you miss is exactly how two slides end up disagreeing.

Anatomy of a function

Every function has the same parts. Once you can name them, any function you read stops looking like a wall of symbols.

def conversion(visitors, buyers, digits=2):
    """Share of visitors who bought, in %."""
    if visitors == 0:
        return None
    return round(buyers / visitors * 100, digits)

print(conversion(900, 45))   # 5.0
  1. 1deftells Python "a function starts here"
  2. 2conversionthe name — a verb or a noun that says what it returns
  3. 3visitors, buyersparameters — the inputs, named placeholders
  4. 4digits=2a default value — this argument becomes optional
  5. 5"""…"""the docstring — shown as a hint in your editor
  6. 6if visitors == 0a guard — handles the impossible case first
  7. 7returnsends the result back and ends the function
  8. 8conversion(900, 45)the call — real values (arguments) fill the parameters
Parameter or argument? They are often mixed up at interviews. A parameter is the name in the definition (visitors); an argument is the actual value passed in the call (900).
Rule of thumb: a calculation that appears twice is a function waiting to happen. Add a docstring — that is the text your editor shows as a hint later.
1.7 · Functions in depth: arguments, scope and traps

Arguments. You can pass arguments by position or by name. Named (keyword) arguments make a call self-explanatory — conversion(visitors=900, conversions=45) cannot be misread — and a default value makes an argument optional.

The classic trap. A default value is created once, when the function is defined — not on every call. If that default is a list, every call shares the same list, and data from one call leaks into the next. The fix is a convention everyone uses: default to None and create the list inside.

Scope. A variable created inside a function lives only inside it and disappears when the function returns. Assigning to a name inside a function creates a new local variable — it does not touch a global one with the same name. That isolation is a feature: functions cannot quietly break each other.

Functions as values. In Python a function is an object you can pass around. sorted, max and min accept a key — a function that says what to compare. A lambda is a short unnamed function written right there, ideal for exactly this.

Look at the output: the second call to add_event_bad returned ['open', 'click'] although we only passed "click". That is the mutable-default bug in action — and one of the most common Python interview questions.
1.8 · Strings and f-strings

Strings are everywhere in analytics: messy CSV columns, event names, log lines — and a whole family of interview problems. Four methods cover most needs: split cuts a string into a list, join glues a list back, strip removes surrounding spaces, replace swaps one fragment for another.

An f-string puts values straight into text, and the part after the colon controls formatting: :.1% shows a share as a percentage, :,.0f adds thousands separators, :>6 right-aligns in six characters. One more thing to remember: strings are immutable — every method returns a new string and leaves the original untouched.

The immutability trap: writing s.upper() on its own line does nothing visible — the result is thrown away. You must assign it: s = s.upper(). The output above shows both.
1.9 · Errors and exceptions

Real data is broken. When Python hits something it cannot do — turn "n/a" into a number, divide by zero — it raises an exception and stops. try / except lets you catch it and decide what to do instead: skip the value, log it, use a default.

Two more pieces: else runs only if no error happened, and finally runs no matter what. And sometimes the right move is the opposite — raise your own error with a clear message, so a nonsensical input fails loudly instead of producing a quietly wrong number.

Never write a bare except:. It also swallows typos in variable names and even Ctrl+C, hiding real bugs. Catch the specific error you expect — here ValueError — and let everything else surface.
2 · Core algorithms

These are the classic interview problems — but what matters here is the pattern behind each one. The same four patterns underpin how pandas performs a join, a group-by or a rolling window.

2.1 · Complexity in one paragraph

Complexity answers one question: as data grows, how fast does the work grow? O(n) means one pass — double the rows, double the time. O(n²) means comparing every element with every other — double the rows and the time goes up fourfold. Most "why is my script hanging" stories are an accidental O(n²).

The practical takeaway: a loop inside a loop over the same data is a red flag. Almost always it can be replaced by a dictionary lookup, turning O(n²) into O(n).

2.2 · Pattern: a dict as memory

The single most useful pattern in analytics. Instead of comparing every pair, you walk once and remember what you have already seen. Whenever a task sounds like "find a pair / find a duplicate / match one table to another", this is the shape of the answer.

Why this matters beyond interviews: a merge in pandas is exactly this — it builds a hash of one table and walks the other once. Understanding the pattern is understanding why a join on an indexed key is fast.
2.3 · Pattern: counting and top-K

Counting occurrences is the most common analytical operation there is: popular pages, frequent errors, best-selling items. Counter does it in one pass and hands you the ranking ready-made.

2.4 · Pattern: sliding window

When a question involves "k consecutive days", recomputing the sum for every position is wasteful. A sliding window adds the entering value and subtracts the leaving one — one pass instead of many. This is precisely what rolling() does in pandas.

2.5 · Pattern: grouping by a computed key

Sometimes the key you need is not in the data — you compute it. Sorting the letters of a word groups anagrams; normalising a name groups duplicate customers; rounding a timestamp groups events into hours. The mechanism is identical, and it is what groupby does under the hood.

What ties these together: four of the five problems above are solved by "remember what you have seen in a dict or set". That single idea converts brute-force comparison into a single pass — and it is the foundation of every fast operation in pandas.
2.6 · One task, several ways

The same answer can usually be written three or four ways: a plain loop with a dict, defaultdict, Counter, pandas. All of them are correct, so the choice is about readability and about one hidden trap. Below, each way solves the same task on the same data, checks that its answer matches, and is timed.

Counting page views five ways. On a few distinct values they all take about the same time. The difference appears when there are many distinct values: list.count() rescans the whole list for each of them.

Removing duplicates while keeping the first occurrence — and the top-3 question asked three ways. The trap here is x not in result when result is a list: every check walks the list again.

How to choose: for counting, Counter — shortest and fastest; for “unique, in order”, dict.fromkeys; when the data is already a DataFrame column, stay in pandas. Whatever you pick, never check membership in a list inside a loop: with thousands of distinct values that single habit makes code hundreds of times slower. Times in the output are from one real run and will differ on your machine; the ratios are what matters.
3 · Working with data

Where the language stops and the craft begins. Half of all analytical errors are born not in the calculation but in how the table was assembled.

3.1 · numpy: why vectorisation

numpy stores numbers in one continuous block of memory and applies an operation to the whole block at once, in compiled code. A Python loop, by contrast, unpacks and repacks every value individually. That is where the order-of-magnitude difference comes from — it is not a style preference.

Rule: if you are writing for over the rows of a DataFrame, there is almost certainly a vector equivalent. The exact multiple depends on the machine, but the order of magnitude is stable.
3.2 · Building a dataset properly

The most common mistake is leaving everything as strings. Then dates will not sort, categories waste memory, and numbers will not add up. Declare types as you build the table, not after something breaks.

Note the fixed seed. Anything random — sampling, simulation, synthetic data — must be reproducible, otherwise tomorrow you get different numbers and cannot tell whether the data changed or you simply got lucky.

Why category matters: on 500 rows the saving is invisible, on ten million it is the difference between "the table fits in memory" and "the kernel died". It also gives correct sorting: ordered=True knows that free comes before pro.
3.3 · Long and wide format

This is the central idea of data organisation. Wide is comfortable for a human reading a report. Long (or "tidy") is the only shape that group-bys and plotting libraries work with naturally.

The tidy rule: one row is one observation, one column is one variable. If column names contain values (jan, feb, mar are values of the variable "month"), the table is wide and should be reshaped with melt before analysis.
Spot the detail in the output: after pivot the columns came back alphabetically — feb, jan, mar. For months that is wrong. This is exactly why a month should be stored as a date or an ordered category, never as a plain string.
3.4 · pandas: select, group, join

Four operations cover most day-to-day work: look at the data, select what you need, group it, and join it to another table. Everything else is a variation.

The distinction people get wrong: .loc addresses by label, .iloc by position. And count() counts rows while nunique() counts distinct values — confusing those two is the most common cause of inflated metrics.
3.5 · Checking and cleaning

This is the step that separates an analyst from someone who can write code. A beautiful chart built on broken data is worse than no chart at all, because people will believe it.

The ritual after every load: shape, missing values, duplicates, types, and value ranges. Five lines that catch most problems before they reach a presentation.
A subtlety visible in the output: duplicated() found zero full duplicates even though user 2 appears twice — the rows differ in the revenue column. That is why duplicate users are caught with drop_duplicates(subset=[...]), not by comparing whole rows.
And note "XX" — a country that does not exist, which cleaning did not remove: the code fixed the case but never checked against a reference list. Such things are only caught by looking at value_counts(). Automation helps; it does not replace looking at your data.
3.6 · Writing code you can trust

Analytical code is read more often than it is written, and decisions are made from its output. Badly written code is not merely ugly — it quietly produces wrong numbers.

Names instead of riddles. d, x, tmp2 mean nothing a week later. orders, paid_orders, arpu_by_country read like a sentence. A variable name is free documentation.

An explicit copy instead of a warning. The most common pandas trap: you filter a table, then write into the result, and pandas cannot tell whether you meant to change the copy or the original. The result is a warning and values that silently do not get written.

One readable chain instead of five variables. With t1…t5 it is easy to lose track of which one is current and accidentally carry a stale table into the final calculation.

The underlying point: code is written once and read dozens of times. If you have to decode your own script, that is not saved time — it is a deferred error.
4 · Business cases

Six tasks in the form they actually arrive: a question from a product manager, a trap in the naive answer, and the code that gets it right.

Case 1 · Signup funnel
Task: "Signups are low. Which step do people drop at?"

The thing to get right: count distinct users, not events. Otherwise one person who opened the form twice is counted twice and the funnel inflates.

0 2 4 6 8 10 Unique users visit signup_form submitted activated 8 (100%) 6 (75%) 4 (50%) 2 (25%) Each step keeps fewer users; activation halves
Produced by the code above
Answer for the product manager: the absolute losses are equal (two people per step), but in relative terms the last transition hurts most — only half of those who submit ever activate. Fix the onboarding, not the form.
Case 2 · "ARPU is up" — is it?
Task: "Average revenue per user went from 22 to 24.7. Time to celebrate?"

Revenue is almost always skewed: a handful of large customers drag the mean upwards. One number hides that; the distribution reveals it.

0 25 50 75 100 125 150 175 Revenue per user 0 100 200 300 400 500 600 Users The mean sits far right of the typical user mean 25 median 13
Produced by the code above
Conclusion: 72% of users earn less than the "average", and the top 10% bring 45% of all revenue. The ARPU rise could come from a couple of large deals while the typical user did not change at all. Track the median and segment.
Case 3 · Cohorts
Task: "We spent a quarter improving onboarding. Did it work?" Overall retention will not answer — it mixes old and new users.

The technique: pivot turns a long table into a cohort matrix, then each row is divided by its own month zero.

0 1 2 3 Months since signup 2026-01 2026-02 2026-03 2026-04 Signup cohort 100 30 22 16 100 32 23 100 35 100 Read down a column: later cohorts retain better
Produced by the code above
How to read it: look down column 1 — 30.2 → 32.4 → 34.6. Each later cohort retains better at the same age. That is the proof the onboarding worked, and overall retention would never have shown it.
Case 4 · A/B test
Task: "Variant A shows 11.36% against the control's 9.80%. Ship it?"

A point estimate alone means nothing — you need the confidence interval. If the intervals overlap, the difference may well be noise.

8.5 9.0 9.5 10.0 10.5 11.0 11.5 12.0 12.5 Conversion, % (bars = 95% CI) Control Variant A Variant B 9.80% 11.36% 10.48% Intervals overlap the control: no winner yet
Produced by the code above
Conclusion: p = 0.073 > 0.05 and variant A's interval overlaps the control. The difference is not significant — shipping now would be premature. This is exactly why error bars are more honest than bare bars.
Case 5 · RFM segmentation
Task: "We have 800 customers and one campaign budget. Who do we write to?"

RFM splits the base along three axes: Recency, Frequency, Monetary. Each axis is cut into quintiles with qcut, and the scores combine into segments.

0 20 40 60 80 100 120 140 Recency — days since last purchase 2 4 6 8 10 12 Frequency — purchases Dot size = money spent. Red = worth winning back At risk Champions Dormant Middle New
Produced by the code above
Answer for marketing: "At risk" is 111 people who used to buy often and then disappeared. They are the most profitable target for reactivation — they have already proved they will pay. The 138 "dormant" are cheaper to let go.
Case 6 · Finding anomalies
Task: "Something broke in early July but the chart looks normal. Check it."

The technique: smooth the series with a rolling mean, subtract it from the actual values, and flag points where the deviation exceeds a few standard deviations.

2026-05-01 2026-05-15 2026-06-01 2026-06-15 2026-07-01 2026-07-15 2026-08-01 800 900 1000 1100 1200 1300 1400 The rolling mean exposes a dip lost in the noise daily signups 7-day rolling mean anomaly
Produced by the code above
Read the output honestly: the detector flagged 6 points, but not all are real problems. 1 July (before the dip) and 6–7 July (after it) were marked as spikes — an artefact: the centred 7-day window is dragged down by the dip, so its neighbours look unusually high. And 3 July, inside the dip, was missed. A detector finds the unusual; a human decides what it means. Never wire this straight into alerts.
5 · Advanced visualisation

Three techniques that separate an analyst's chart from a spreadsheet default. Each solves a specific perception problem.

Every chart on this page starts with the same short plt.rcParams block: it removes the top and right frame, softens the axes and puts the grid behind the data. Copy it once into your notebook and all your charts lose the "default matplotlib" look.
Small multiples instead of spaghetti

Beyond three series, lines on a single chart become unreadable. The fix is to split into small panels and highlight one segment per panel, keeping the others as grey context.

20 40 60 Mobile iOS Mobile Android 0 2 4 6 8 10 20 40 60 Desktop 0 2 4 6 8 10 Tablet Week Retention, %
Produced by the code above
Why it works: the grey background provides comparison — you see immediately that Tablet falls fastest while Desktop holds. On one combined chart that would drown in crossings.
Show the distribution, not just the mean

Two bars of averages hide everything interesting. A box plus the raw points plus a mean marker shows shape, spread and outliers at once.

0 10 20 30 40 50 60 70 80 Revenue per user Control Variant Box = median & quartiles, diamond = mean, dots = raw data
Produced by the code above
Look closely: the red diamond (mean) sits clearly to the right of the box centre (median) — that is skewness made visible. A bar chart would never show it.
Waterfall: decomposing a change

The question "revenue grew by 73k — where from?" is best answered by a waterfall: every contribution is separate, and gains are distinguished from losses by colour.

Revenue May New customers Win-back Upgrades Downgrades Churn Revenue June 0 100 200 300 400 500 600 Revenue, k 420 +85 +22 +48 -19 -63 493 Where the growth came from, factor by factor
Produced by the code above
Implementation subtlety: for negative values the bar's base is cum + v and its height is abs(v). Passing a negative height together with a shifted base draws the bar at half its size — a classic bug when building a waterfall by hand.
Correlation matrix: show half, colour by sign

A full correlation matrix repeats every number twice and wastes the diagonal on 1.00. Mask the upper triangle, trim the empty edge, and use a diverging palette centred on zero — red for positive, blue for negative — so strength and direction read instantly.

sessions page_views time_on_site orders revenue page_views time_on_site orders revenue tickets 0.82 0.64 0.80 0.37 0.32 0.26 0.31 0.29 0.24 0.82 0.06 0.09 0.06 0.06 0.07 Correlation matrix: lower triangle only −1.00 −0.75 −0.50 −0.25 0.00 0.25 0.50 0.75 1.00
Produced by the code above
What the chart says: revenue moves with orders (0.82) but only weakly with sessions (0.31), and support tickets are unrelated (0.07). And the usual warning: correlation tells you what moves together, not what causes what — for that, see the causal inference section.
Dumbbell chart: level and change together

When you compare "before" and "after" across many categories, two bars per category become noise. A dumbbell draws one line per category from the old value to the new one: its position shows the level, its length the size of the change, its colour the direction. Sorting by change puts the story in order.

1 2 3 4 5 6 7 Conversion, % Referral Paid search Display Organic Email Social Dumbbell: level AND change at a glance before redesign after redesign
Produced by the code above
Reading it: the redesign helped Social most (+1.1 pp), while Referral got worse (−0.6 pp). A grouped bar chart would show the same numbers, but you would have to compare twelve bars to see it.
Slope chart: one outlier among many

Two time points and several categories? Draw a line for each and let the slopes speak. Grey out everything that behaves as expected and colour only the line that breaks the pattern — the reader's eye goes there without any explanation.

Q1 Q2 Free 18% 20% Starter 31% 33% Pro 46% 39% Team 52% 55% Enterprise 61% 63% Day-30 retention by plan: one line breaks the pattern
Produced by the code above
The finding: every plan improved by 2–3 pp except Pro, which fell from 46% to 39%. This is the "highlight one thing" principle: the chart answers a single question, and colour is spent only on the answer.
Calendar heatmap: seasonality you can see

A daily metric as a line hides its weekly rhythm in the wiggle. Fold it into a grid — weekdays as rows, weeks as columns — and the pattern becomes a texture you notice immediately. It is the same idea as the GitHub contribution graph.

wk 1 wk 5 wk 9 wk 13 wk 17 wk 21 wk 25 Mon Tue Wed Thu Fri Sat Sun Calendar heatmap: weekends dip, every single week 1000 1500 DAU
Produced by the code above
What it reveals: Saturday and Sunday run about 40% below weekdays (≈830 vs ≈1390 DAU), every single week, while the darker colour on the right shows steady growth. Before comparing two days, always ask whether they are the same weekday.
Rules for a good chart
The title is the conclusion, not the label"Variants overlap the control — no winner" beats "Conversion by group". The reader gets the thought, not the raw data.
Remove the frame and surplus gridlinesAnything that carries no information steals attention from what does.
Label values directly on the chartIt saves the reader from looking back and forth to the axis.
Do not truncate the Y axis on bar chartsBars are compared by length, so a cut axis exaggerates the difference. For lines it is acceptable.
Colour is meaning, not decorationOne accent for the point being made, grey for context. A rainbow without meaning slows reading down.
Show uncertaintyConfidence intervals are more honest than point estimates and prevent premature conclusions.
The test before you send it: cover the title and show the chart to a colleague. If they cannot tell you what point you were making, the chart is not finished.
6 · Common mistakes
MistakeWhat it costs youDo this instead
count() instead of nunique()The funnel inflates and conversion is overstatedCount distinct users
Reporting only the meanA few large customers mask a decline in everyone elseMedian, percentiles, histogram
Overall retention instead of cohortsMixes old and new users, so improvements stay invisibleA cohort matrix
A loop instead of vectorisationSlow, longer to write, more room for errorOperate on the whole column
Ignoring SettingWithCopyWarningValues silently fail to be written.copy() or .loc[...]
Never checking isna()mean() quietly skips gaps and the metric driftsCount missing values before calculating
Not fixing the random seedResults change between runs and cannot be reproduceddefault_rng(42)
Comparing groups of different size by absolute numbersThe bigger segment always appears to winCompare shares, normalise
← A/B Testing Calculator Causal Inference Algorithms for interviews → Home