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
}
}