Data Analysis with Python: From Raw Data to Real-World Insights
BOOK 2 · PROJECTS · CHAPTER 16

Worked solution: measured data, missingness and subgroup comparisons

On this page

The reference solution uses task-specific completeness rather than blanket deletion. It retains observations with usable mass and flipper measurements even when sex is unresolved. The source-row number is stable for this frozen file, but it is not asserted to identify the same animal across an independently acquired dataset.

Inspect the two grouped tables together. Support counts make the denominator visible; the missing-sex group remains explicit through dropna=False. The mean and median support a discussion of distribution shape, not a demand that they match. The scatterplot shows group structure without fitting an unexplained regression.

After running the solution, independently reconcile 342 retained plus two unresolved with the 344 source rows. Inspect the original missingness counts and compare them with a blanket complete-case table. State the selection difference in your report. A physiological relationship requires scientific context; these calculations alone do not establish a causal explanation.

Complete implementation

The following function is the implementation supplied in code/book2lab.py. It receives an explicit source path and output folder. The companion script imports the required libraries and saves selectable SVG plots.

from pathlib import Path
import json
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from book2lab import save_plot

def penguin_analysis(path,out):
    out=Path(out);out.mkdir(parents=True,exist_ok=True);raw=pd.read_csv(path)
    if not {'species','body_mass_g','flipper_length_mm','sex'}.issubset(raw):raise ValueError('Missing penguin fields')
    df=raw.assign(snapshot_row_id=np.arange(1,len(raw)+1))
    usable=df.body_mass_g.notna() & df.flipper_length_mm.notna()
    valid=df.loc[usable].copy();rejected=df.loc[~usable].copy()
    if not valid.body_mass_g.gt(0).all() or not valid.flipper_length_mm.gt(0).all():raise ValueError('Non-positive physical measure')
    grouped=valid.groupby('species').agg(n=('body_mass_g','size'),mean_mass_g=('body_mass_g','mean'),median_mass_g=('body_mass_g','median'),mean_flipper_mm=('flipper_length_mm','mean'))
    subgroup=valid.groupby(['species','sex'],dropna=False).agg(n=('body_mass_g','size'),mean_mass_g=('body_mass_g','mean'))
    result={'received':len(raw),'measurement_complete':len(valid),'measurement_unresolved':len(rejected),'sex_missing_all':int(raw.sex.isna().sum()),'groups':{str(k):int(v) for k,v in grouped.n.items()}}
    valid.to_csv(out/'measurement_complete.csv',index=False);rejected.to_csv(out/'measurement_unresolved.csv',index=False);grouped.to_csv(out/'species_summary.csv');subgroup.to_csv(out/'species_sex_summary.csv');(out/'audit.json').write_text(json.dumps(result,indent=2))
    fig,ax=plt.subplots(figsize=(7,4))
    for species,g in valid.groupby('species'):ax.scatter(g.flipper_length_mm,g.body_mass_g,label=species,alpha=.65,s=22)
    ax.set(xlabel='Flipper length (mm)',ylabel='Body mass (g)',title='Penguins: inspect species before pooling');ax.legend();save_plot(fig,out/'relationship.svg')
    return valid,grouped,result

result = penguin_analysis(Path('data/teaching/penguins.csv'), Path('outputs/penguins'))
print(result[-1] if isinstance(result, tuple) else result)

Explain and extend

After running the solution, independently reconcile 342 retained plus two unresolved with the 344 source rows. Inspect the original missingness counts and compare them with a blanket complete-case table. State the selection difference in your report. A physiological relationship requires scientific context; these calculations alone do not establish a causal explanation.

Return to the project brief and compare the acceptance evidence with your own outputs. Do not replace a valid alternative merely because its variable names differ.

Inspect the verified output

{
  "received": 344,
  "measurement_complete": 342,
  "measurement_unresolved": 2,
  "sex_missing_all": 11,
  "groups": {
    "Adelie": 151,
    "Chinstrap": 68,
    "Gentoo": 123
  }
}
image/svg+xml Matplotlib v3.10.0, https://matplotlib.org/ 170 180 190 200 210 220 230 Flipper length (mm) 3000 3500 4000 4500 5000 5500 6000 Body mass (g) Penguins: inspect species before pooling Adelie Chinstrap Gentoo

Teaching edition · verified experiments · PDF and EPUB editions not generated.