Worked solution: audit a cultural collection
On this page
The solution separates parsing, validation and classification. It rejects an ambiguous object identity immediately, but retains unresolved dates for review. This distinction prevents a convenient cleanup from redefining the collection silently.
Read the masks in order. valid requires both finite bounds and earliest≤latest. The mutually exclusive period conditions preserve crossing intervals. The output span describes the catalogue interval; no midpoint is imputed. The bar plot is a diagnostic summary of selected records, not a claim about the entire museum.
Review the returned audit and independently inspect one record in each nonempty period. Check its source URL, compare both year bounds with the cutoff, and reconcile the three period counts with the validated count. Then use the intentionally corrupted fixture to test failure handling.
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 museum_analysis(path,out):
out=Path(out);out.mkdir(parents=True,exist_ok=True)
raw=pd.read_csv(path,keep_default_na=False)
required={'id','title','object_type','earliest_year','latest_year'}
if not required.issubset(raw):raise ValueError('Missing museum fields')
if raw.id.duplicated().any():raise ValueError('Museum IDs are not unique')
df=raw.copy()
for col in ['earliest_year','latest_year']:df[col]=pd.to_numeric(df[col],errors='coerce')
valid=df.earliest_year.notna() & df.latest_year.notna() & df.earliest_year.le(df.latest_year)
valid=valid & np.isfinite(df.earliest_year) & np.isfinite(df.latest_year)
rejected=df.loc[~valid].copy();clean=df.loc[valid].copy()
clean['period']=np.select([clean.latest_year.lt(1900),clean.earliest_year.ge(1900)],['before_1900','1900_or_later'],default='crosses_1900')
clean['date_span_years']=clean.latest_year-clean.earliest_year
counts=clean.groupby('period').size().rename('records').reset_index()
result={'received':len(raw),'validated':len(clean),'unresolved':len(rejected),'periods':dict(zip(counts.period,counts.records.map(int))),'selection':'100 painting-keyword API results; not representative of the whole collection'}
clean.to_csv(out/'validated.csv',index=False);rejected.to_csv(out/'unresolved.csv',index=False);counts.to_csv(out/'periods.csv',index=False)
(out/'audit.json').write_text(json.dumps(result,indent=2))
fig,ax=plt.subplots(figsize=(7,4));ax.bar(counts.period,counts.records,color='#315b84');ax.set(xlabel='Catalogue date-bound classification',ylabel='Selected records',title='Museum snapshot: date-bound coverage');ax.tick_params(axis='x',rotation=15);save_plot(fig,out/'periods.svg')
return clean,result
result = museum_analysis(Path('data/teaching/museum.csv'), Path('outputs/museum'))
print(result[-1] if isinstance(result, tuple) else result)Explain and extend
Review the returned audit and independently inspect one record in each nonempty period. Check its source URL, compare both year bounds with the cutoff, and reconcile the three period counts with the validated count. Then use the intentionally corrupted fixture to test failure handling.
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": 100,
"validated": 100,
"unresolved": 0,
"periods": {
"1900_or_later": 45,
"before_1900": 53,
"crosses_1900": 2
},
"selection": "100 painting-keyword API results; not representative of the whole collection"
}