learn-python-data-analysis-with-phoebe / Session 8 of 8
Learn Python Data Analysis with Phoebe · Session 8 of 8

From insight to a shareable report

Analysis only matters when someone else can act on it. Today you turn seven sessions of work into findings a human wants to read, add a simple model that predicts salary from role, location, and experience, and package the whole thing into a portfolio piece you'd be proud to post. This is the finale - it ties everything together.

🔴 Hardest Builders Capstone 45 min live + self-study
0-3 · Welcome 3-18 · Concepts 18-40 · Build the report 40-45 · Q&A
Part 0

The last mile is the whole point

Seven sessions in, you can load, clean, join, group, and chart 124,000 real job postings. That's real skill - but a notebook full of true tables is not an analysis anyone will read. The last mile is turning that work into findings a human wants to act on, a model that makes a testable prediction, and a package you can hand to a stranger. That last mile is where analysts become analysts people trust. Today you walk it end to end and finish with something you can put your name on.

Live - presented in session Self-study - read after class ★ Build-along demo The project: the data job market
★ What you walk out with today A finished mini-report - five headline findings, five clean charts, and a salary model that predicts pay from role, location, and experience - reproducible top to bottom, exported to HTML, and drafted into a LinkedIn post with your name on it. The portfolio piece the whole course was building toward.
Part 1 · covers turning output into insight (McKinney ch13)

From numbers to findings 4 min live

Output is what the computer prints. A finding is what a human should do about it. The gap between the two is the single most valuable thing you'll learn today - and it's not code.

124k raw rows everything, messy, unread Cleaned + typed salaried subset, tidy titles, deduped Grouped by role, level, location 5 findings subject + number 1 headline Narrow relentlessly - the report leads with the headline, not the 124k rows.
🔍 Click to zoom - the funnel: raw rows narrow to five findings and one headline that leads the report
LiveWhat makes a finding a finding2 min

A finding has three parts, and a wall of tables has none of them. Every headline you write today passes this test:

  • A subject - who or what is this about? "Senior data engineers", not "the data".
  • A number - quantified, with the sample size. "$142k median (n=1,830)", not "high pay".
  • A so-what - why should the reader care, and what's the caveat? "...but that's the 30% who disclose salary."

Then structure the report the way people actually read: lead with the headline, one chart per point, quantify everything, and name the caveat out loud. The biggest caveat in this dataset is unavoidable - roughly 70% of postings have no salary, so every pay finding is about the salaried subset. Say so. Honesty is what makes the whole thing trustworthy.

Real world

"Median data-analyst salary is $X (n=1,204 salaried postings)" beats a wall of groupby tables every single time. The table is your evidence; the finding is what you actually put in front of a human. On LinkedIn, the finding is the post - the table is the reply when someone asks "source?".

Self-studyThe five findings, sketched2 min read

You already computed most of these across Sessions 5-7. The job now is to phrase them as findings, not describe them as outputs:

Raw output you havePhrased as a finding
groupby role -> median salary"Data engineers out-earn analysts by ~35% at the median (salaried subset)"
value_counts on location"Three metros hold a quarter of all senior postings"
groupby experience_level"Each step up the level ladder adds roughly $Xk to median pay"
skill counts (skills table)"SQL and Python appear in over half of analyst postings"
salary disclosure rate"Only ~30% of postings disclose pay - the honest caveat on all of the above"
Pick the finding, then the chart Never plot everything and hunt for a story. Decide the five sentences first, then choose the one chart that proves each. A chart with no finding attached is decoration.
Part 2 · covers Kaggle Intro to ML L1-L3 + scikit-learn basics

A first predictive model 6 min live

Describing is "what happened". Predicting is "given a new role, what would it pay?". You already have everything you need to build an honest first model - and the framing matters more than the algorithm.

Features X Train / test split Fit model Predict Validate role, location, experience hold out 20% it never sees learn on the 80% training estimate pay on unseen rows MAE + R2 vs a baseline A model is a hypothesis you can test - fit on what it knows, judge it on what it doesn't.
🔍 Click to zoom - the pipeline: features to split to fit to predict to validate, every time
LiveSupervised learning, in plain words3 min

Supervised learning is just this: you have example rows where you know the answer, and you want to predict the answer for new rows. Split your table into two:

  • Features (X) - the inputs you'd know about a new posting: role bucket, location, experience level.
  • Target (y) - the thing you want to predict: salary_year, the column you built back in Session 4.

Then you hold out some rows (the test set) that the model never sees while learning, so you can honestly ask "how would this do on a posting it's never met?". You encode the categorical columns - a model can't read the word "Senior", so get_dummies turns each category into 0/1 columns (one-hot encoding). Finally you fit a LinearRegression: it learns a weight per feature - roughly "how many dollars does being Senior add?".

Real world

A linear model on role + location + level won't nail any single salary - real pay depends on company, negotiation, luck. But it will tell you the shape: which factors move pay most, and by how much. That's a genuinely useful thing to say on LinkedIn - "location adds more to pay than one level of seniority, in this dataset" is a finding a model earned.

Self-studyThe honest caveats2 min read

State these out loud in the report. They're not weaknesses to hide - naming them is what separates an analyst from a hype account:

  • Trained on the salaried 30%. The model only knows postings that disclosed pay. Those skew larger and more formal - it may not generalize to the silent 70%.
  • Correlation, not cause. The model finds patterns, not mechanisms. "Senior adds $Xk" describes this data; it doesn't promise you a raise.
  • Linear is a simplification. Real pay isn't a straight line. A modest fit is expected and fine - Part 3 shows how to say that honestly.
A model is a hypothesis, not a crystal ball The value is a testable, quantified claim - not a promise. The moment you present a model as certainty, you've stopped being trustworthy. Show the error, show the caveat, invite the pushback.
Part 3 · covers Kaggle Intro to ML L4 (validation) + packaging

Validate honestly, then package 5 min live

A model that looks brilliant on its training data is telling you nothing - it may have just memorized. Judging it fairly, in units a human understands, then wrapping it so anyone can re-run it: that's the professional finish.

1 Reproducible notebook Restart & Run All, top to bottom, no errors 2 Labeled charts title, axes, units, source - every one 3 Stated caveats the 70% missing salary, said out loud 4 One clear headline the single sentence the report leads with All four, or it's a notebook - not a report. Ship the four.
🔍 Click to zoom - the packaging checklist: reproducible, labeled, honest, headlined
LiveNever judge a model on training data3 min

This is the single most important idea in Part 3. If you score the model on the same rows it learned from, it flatters itself - it can memorize its way to a great score and fall apart on anything new. That's overfitting, and the held-out test set is how you catch it. Score only on data the model has never seen. Two numbers say it plainly:

  • MAE (mean absolute error) - the average dollars your prediction is off, in the target's real units. "MAE = $18k" means predictions miss by about $18,000 on average. Human-readable, no interpretation needed.
  • R2 - the share of the variance in salary the model explains, from 0 to 1. R2 = 0.35 means it explains about a third of the spread; the rest is factors you didn't feed it.

And always compare to a baseline: what if you just predicted the mean salary for everyone? If your model can't beat that dumb guess, it isn't earning its keep. Beating the baseline - even modestly - is the honest bar.

A modest R2 is expected and fine Predicting salary from three columns will never give you R2 = 0.9, and anyone who reports that on this data made a mistake. R2 = 0.3-0.4 that beats the mean baseline is a real, honest result. Report it proudly and say what's missing.
LivePackage it so a stranger can run it2 min

Three moves turn a working notebook into a shareable artifact:

  • A clean notebook, top to bottom. Restart & Run All must produce every table and chart with no manual steps and no errors. If it only runs in the order you happened to click, it isn't reproducible.
  • An exported report. jupyter nbconvert --to html freezes the notebook into a file you can email or host - no Python required to read it. PDF works too.
  • A LinkedIn-ready summary. Hook, three findings, one chart, one honest caveat, soft CTA. No hype, no "🚨 SHOCKING". The restraint is the credibility.
Real world

This is exactly Phoebe's KOL "ship it" move: finish the thing, credit yourself ("Analysis by [you]"), and end with a question that invites discussion rather than a demand for likes. A post that says "here's what I found, here's my caveat, what am I missing?" starts conversations - and conversations are what build a following. The finished report is the proof; the caveat is the invitation.

Demo 1 of 3

Assemble the five findings ★ 8 min · everyone writes

Open your saved summary frames from Sessions 5-7 (the groupby results you exported). If you saved them as CSVs, reload them; if they're in the notebook, scroll to them.

For each of the five, write the finding as a one-line markdown headline using the subject + number + so-what test. Put a sample size on every pay number.

Pick the single best chart for each finding from Sessions 6-7. One chart per point - if two charts say the same thing, drop one.

Add the honest caveat as its own line: pay findings cover only the ~30% of postings that disclose salary.

★ Findings as markdown, above each chart# 5 findings - the state of the data job market (salaried subset, 2023-24) **1. Data engineers out-earn analysts by ~35% at the median** ($142k vs $105k, n=1,830). **2. Each step up the seniority ladder adds ~$22k** to median pay (Entry to Senior). **3. Three metros hold ~25% of all senior postings** - location is a pay lever, not just a dot on a map. **4. SQL and Python appear in over half of analyst postings** - the non-negotiable pair. **5. Only ~30% of postings disclose salary** - the caveat on findings 1-3, stated up front.
Write the caveat first, not last Put finding 5 (the disclosure rate) at the top of your report, not buried at the bottom. Leading with your biggest limitation is disarming - readers trust you more, not less, when you show the cracks before they find them.
Demo 2 of 3

Build the salary model ★ 9 min · everyone models

Start from your cleaned, salaried frame. Keep three feature columns plus the target: experience_level, a top-locations column, a role bucket, and salary_year.

One-hot encode the categoricals with get_dummies, then split into train and test with a pinned seed.

★ Encode + splitimport pandas as pd model_df = jobs_salaried[["experience_level", "location_top", "role_bucket", "salary_year"]].dropna() # one-hot: a model can't read words, so each category becomes a 0/1 column X = pd.get_dummies(model_df[["experience_level", "location_top", "role_bucket"]], drop_first=True) y = model_df["salary_year"]
★ Split, fit, predict, score - honestlyfrom sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_absolute_error, r2_score X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) model = LinearRegression().fit(X_train, y_train) preds = model.predict(X_test) # predict on rows the model never saw mae = mean_absolute_error(y_test, preds) r2 = r2_score(y_test, preds) print(f"MAE: ${mae:,.0f} R2: {r2:.2f}")
★ Beat the baseline - or the model isn't earning its keep# the dumb guess: predict the mean salary for everyone baseline = [y_train.mean()] * len(y_test) base_mae = mean_absolute_error(y_test, baseline) print(f"Baseline MAE (predict the mean): ${base_mae:,.0f}") print(f"Model MAE: ${mae:,.0f}") # if model MAE < baseline MAE, your features add real signal
Real world

Expect a modest R2 - somewhere around 0.3 to 0.4 - and an MAE in the tens of thousands. That is not failure; that is honest. Three columns can't capture company, negotiation, or timing. The finding is "role, level, and location together explain about a third of salary spread, and beat guessing the mean by $Xk" - a claim a model earned and a caveat a professional states.

Demo 3 of 3

Package and ship ★ 5 min · everyone exports

Reproducibility check: Kernel -> Restart & Run All. Watch it run top to bottom with no errors and every chart re-rendering. If a cell breaks, fix the order now - a report that only runs by hand isn't a report.

Export the notebook to a standalone file anyone can open:

★ Freeze the notebook into a shareable file# run in a terminal, from your project folder: jupyter nbconvert --to html 08-report.ipynb # result: 08-report.html - email it, host it, no Python needed to read it # for a PDF instead: jupyter nbconvert --to pdf 08-report.ipynb

Draft the LinkedIn post - hook, three findings, one chart, the caveat, a soft CTA. Restraint over hype.

★ The LinkedIn post skeletonI analyzed 124k LinkedIn job postings to see what the data job market pays. Three things stood out: - Data engineers out-earn analysts by ~35% at the median. - Each step up the seniority ladder adds ~$22k. - Location moves pay more than one level of seniority does. Honest caveat: only ~30% of postings disclose salary, so this is the salaried slice - not the whole market. [one chart attached] What surprised you here - and what would you have looked at next? Analysis by [you] · full notebook in comments.
Ship it, then credit yourself The post is not finished until your name is on it and it's actually posted. A perfect analysis in a private notebook helps no one and builds no reputation. Done and shared beats perfect and hidden - every time.
After the session · and where to go next

Finish it, post it, then keep going ◐ ongoing

Where to go next - your roadmap past this course
  • polars - when pandas gets slow, polars handles bigger data far faster with a similar API.
  • Statistical testing - move from "looks different" to "significantly different" (statsmodels, scipy.stats).
  • Dashboards - turn a report into an interactive app with Streamlit so people can explore, not just read.
  • The sibling course - learn-sql-with-phoebe for pulling data straight from the warehouse before it ever hits pandas.
Check yourself

Three questions before you graduate 🎯 ◐ 90 seconds

1 · Why split the data into separate train and test sets?

A model can memorize its training rows and look brilliant, then fall apart on anything new. The held-out test set is the only honest scoreboard.

2 · Your model reports an MAE of $18k. What does that mean?

MAE is the mean absolute error in the target's own units. Dollars in, dollars out - no interpretation needed, which is why it's the friendliest metric to report.

3 · What makes a finding shareable, versus just output?

Output is what the computer prints; a finding is what a human should do about it. Subject, number, so-what, caveat - led by the headline. That's the whole last mile.

Source material

Official sources covered

This finale teaches the working content of Kaggle's free Intro to Machine Learning micro-course, the scikit-learn basics, and Wes McKinney's open-access Python for Data Analysis (3e). Certificates and graded exercises stay on Kaggle - links provided.

Kaggle Intro to ML L1-L4how models work, exploration, first model, validation - Parts 2-3
scikit-learn basicstrain_test_split, LinearRegression, MAE + R2 metrics - Demo 2
McKinney ch13 - Data Analysis Examplesfull analyses worked end to end, output to insight - Part 1
McKinney ch12 - Modeling Librariesstatsmodels named for statistical testing - a next-step pointer
Random forests, cross-validation, feature engineeringfirst taste only - full treatment in Kaggle Intermediate ML
Certificates live on Kaggle This course teaches the working content so you can build for real. The graded exercises, badges, and certificates are on Kaggle's platform - take the two ML micro-courses to earn them.

Session 8 cheat sheet · pin this

A findingsubject + number + so-what + caveat, led by the headline. Not a raw table.
Encode categoricalspd.get_dummies(X, drop_first=True) - turns words into 0/1 columns a model can read.
Hold out datatrain_test_split(X, y, test_size=0.2, random_state=42). Score only on the test set.
Fit a modelLinearRegression().fit(X_train, y_train); then .predict(X_test).
Judge honestlyMAE = avg dollars off · R2 = share of variance explained · both vs a mean baseline.
Ship itRestart & Run All, then jupyter nbconvert --to html. Post it. Credit yourself.