"""Reproduce nine charts and their data. No private data or network requests. Run: python tools/reproduce_analysis.py (requires matplotlib) """ from __future__ import annotations import csv,json,math,textwrap,re from pathlib import Path import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt from matplotlib.ticker import PercentFormatter ROOT=Path(__file__).resolve().parents[1] DATA=ROOT/'data'; OUT=ROOT/'assets/charts' DATA.mkdir(parents=True,exist_ok=True);OUT.mkdir(parents=True,exist_ok=True) def mg1_wait(rho:float,s:float,cv:float)->float: if not 0<=rho<1 or s<=0 or cv<0:raise ValueError('Require 0<=rho<1, s>0, CV>=0.') return rho/(1-rho)*(1+cv*cv)/2*s def clearance(B:float,mu:float,lam:float)->float: if min(B,lam)<0 or mu<=0:raise ValueError('Invalid backlog/rate.') return 0.0 if B==0 else math.inf if lam>=mu else B/(mu-lam) def filing(N:float,f:float,R:float,ts:float,tf:float)->tuple[float,float]: if min(N,f,R,ts,tf)<0:raise ValueError('Inputs must be nonnegative.') return R*ts/60,(N*f+R*tf)/60 def net_ai(manual:float,assisted:float,review:float,words:float,readers:float,wpm:float,followup:float=0)->float: if min(manual,assisted,readers)<0 or wpm<=0:raise ValueError('Invalid time/exposure/rate.') return manual-assisted-review-words*readers/wpm-followup MANIFEST={} def csvout(stem,rows): with (DATA/f'{stem}.csv').open('w',newline='',encoding='utf-8') as f: w=csv.DictWriter(f,fieldnames=list(rows[0]));w.writeheader();w.writerows(rows) def new(title,subtitle,left=.13): fig=plt.figure(figsize=(12,7.2),dpi=200) ax=fig.add_axes([left,.23,.94-left,.54]) fig.text(.055,.95,title,fontsize=20,fontweight='bold',va='top') fig.text(.055,.875,subtitle,fontsize=11.5,va='top') ax.tick_params(labelsize=12) for side in ('top','right'):ax.spines[side].set_visible(False) return fig,ax def save(fig,stem,n,title,kind,note,alt): note = re.sub(r'([,:;])(?=[A-Za-z0-9])', r'\1 ', note) note = re.sub(r'(?<=[A-Za-z])(?=[0-9])|(?<=[0-9])(?=[A-Za-z])', ' ', note) note = re.sub(r'([0-9])and(?=[0-9])', r'\1 and ', note) fig.text(.055,.11,'\n'.join(textwrap.wrap(note,130)),fontsize=10,va='top') fig.text(.055,.025,f'InboxCabin Research & Analysis | Figure {n} | v1.0 | 19 Oct 2026',fontsize=10) for ext in ('png','svg'):fig.savefig(OUT/f'{stem}.{ext}',dpi=200) plt.close(fig) MANIFEST[stem]={'figure':n,'title':title,'kind':kind,'note':note,'alt':alt,'csv':f'{stem}.csv'} def main(): stem='01-utilization-wait';rows=[dict(utilization_percent=p,service_minutes=3,service_cv=1,waiting_minutes=mg1_wait(p/100,3,1),evidence_type='modeled') for p in range(10,96)];csvout(stem,rows) title='Small increases in load can sharply increase waiting' fig,ax=new(title,'ILLUSTRATIVE M/G/1 MODEL · 3-minute mean handling time · service CV = 1') ax.plot([r['utilization_percent'] for r in rows],[r['waiting_minutes'] for r in rows],linewidth=3) for p in (50,80,90,95): v=mg1_wait(p/100,3,1);ax.plot(p,v,'o');ax.annotate(f'{p}%: {v:.0f} min',(p,v),xytext=(-10,12),textcoords='offset points',ha='right',fontsize=12) ax.set(xlabel='Utilization (share of processing capacity)',ylabel='Mean wait before processing (minutes)',xlim=(10,100),ylim=(0,70));ax.xaxis.set_major_formatter(PercentFormatter(100)) save(fig,stem,1,title,'modeled','Calculated: Wq = [ρ/(1−ρ)] × [(1+CV²)/2] × s. Poisson arrivals; one continuously available FCFS processor. Not measured workplace response times. Theory: Whitt (2012).','Modeled waiting rises from 12 minutes at 80% utilization to 27 minutes at 90% and 57 minutes at 95%.') stem='02-service-variability';rows=[dict(service_cv=c,utilization_percent=80,service_minutes=3,waiting_minutes=mg1_wait(.8,3,c),evidence_type='modeled') for c in (0,1,2)];csvout(stem,rows) title='Same average workload, different service-time variability' fig,ax=new(title,'ILLUSTRATIVE M/G/1 MODEL · 80% utilization · 3-minute mean handling time') bars=ax.bar(['CV = 0','CV = 1','CV = 2'],[r['waiting_minutes'] for r in rows],width=.55);ax.bar_label(bars,labels=['6 minutes','12 minutes','30 minutes'],padding=8,fontsize=13) ax.set(xlabel='Assumed coefficient of variation in handling time',ylabel='Mean wait before processing (minutes)',ylim=(0,36)) save(fig,stem,2,title,'modeled','CV is standard deviation divided by mean. These are hypothetical service distributions, not worker groups. Formula and continuous-processing assumptions as in Figure 1. Theory: Whitt (2012).','At 80% utilization, modeled waits are 6,12,and30 minutes for assumed service CV values of0,1,and2.') stem='03-backlog-clearance';rows=[dict(arrivals_per_hour=a,capacity_per_hour=20,starting_backlog=20,clearance_hours=clearance(20,20,a),evidence_type='modeled') for a in (10,14,16,18,19)];csvout(stem,rows) title='Backlog clears using spare capacity—not total capacity' fig,ax=new(title,'ILLUSTRATIVE FLUID MODEL · 20 waiting requests · capacity: 20 requests/hour') ax.plot([r['arrivals_per_hour'] for r in rows],[r['clearance_hours'] for r in rows],marker='o',linewidth=3) for r in rows:ax.annotate(f"{r['clearance_hours']:.1f} h",(r['arrivals_per_hour'],r['clearance_hours']),xytext=(0,10),textcoords='offset points',ha='center',fontsize=12) ax.set(xlabel='New requests per hour',ylabel='Hours to clear starting backlog',xlim=(9,20),ylim=(0,24)) save(fig,stem,3,title,'modeled','Calculated: starting backlog ÷ (capacity − arrivals). Constant rates and continuous availability; no finite clearance at or above capacity. This is not a stochastic expected hitting-time estimate.','With capacity20/hour, a20-request backlog clears in2 hours at10 arrivals/hour but20 hours at19 arrivals/hour.') stem='04-refinding-observed';rows=[dict(group=g,mean_sequence_seconds=m,sd_seconds=sd,mean_success=.88,source_table='5',doi='10.1145/1978942.1979457',evidence_type='published_observational_summary') for g,m,sd in [('Higher filing',72.87,38.05),('Lower filing',66.07,26.64)]];csvout(stem,rows) title='More filing did not produce faster refinding in this sample' fig,ax=new(title,'PUBLISHED OBSERVATIONAL SUMMARY · Complete finding sequences · not a randomized methods test') bars=ax.bar([r['group'] for r in rows],[r['mean_sequence_seconds'] for r in rows],width=.52);ax.bar_label(bars,labels=['72.87 seconds','66.07 seconds'],padding=9,fontsize=14);ax.set(ylabel='Mean finding-sequence duration (seconds)',ylim=(0,100)) save(fig,stem,4,title,'published_observational_summary','Source: Whittaker et al. (2011), Table 5. SDs:38.05 and26.64 seconds; no confidence intervals shown. Both mean success rates0.88. Filing groups are self-selected, not experimentally assigned.','Higher filers averaged72.87 seconds per finding sequence and lower filers66.07 seconds in this2011 sample; success averaged88% in both.') stem='05-revisit-intent';rows=[dict(intent=l,percent=p,survey_respondents=395,source_table='4',doi='10.1145/3176349.3176398',evidence_type='published_survey_summary') for l,p in [('Find information',73.4),('Act on the email',20.6),('Organize or clean up',.6),('Other',5.4)]];csvout(stem,rows) title='Email revisits were usually about recovering information' fig,ax=new(title,'PUBLISHED SURVEY SUMMARY · Recalled recent email revisit · 395 Microsoft employees',left=.25) bars=ax.barh([r['intent'] for r in rows],[r['percent'] for r in rows],height=.55);ax.invert_yaxis();ax.bar_label(bars,labels=[f"{r['percent']:.1f}%" for r in rows],padding=7,fontsize=12);ax.set(xlabel='Share of reported revisit intents',xlim=(0,85));ax.xaxis.set_major_formatter(PercentFormatter(100)) save(fig,stem,5,title,'published_survey_summary','Source: Alrashed et al. (2018), Table 4. Self-reported intent in a395-person survey, not the separate100,000-user log sample. Rounded percentages are not exact counts or a universal email distribution.','Survey revisit motives:73.4% information,20.6% action,0.6% organization,5.4% other.') stem='06-filing-break-even';rows=[] for r in range(61): a,b=filing(60,4,r,28,20);rows.append(dict(retrievals_per_day=r,messages=60,filing_seconds=4,search_seconds=28,folder_seconds=20,search_minutes=a,filing_minutes=b,evidence_type='modeled')) csvout(stem,rows);title='Filing pays back when retrieval savings cover preparation' fig,ax=new(title,'ILLUSTRATIVE MODEL · 60 messages × 4 seconds filing · search:28 sec · folder retrieval:20 sec') ax.plot(range(61),[r['search_minutes'] for r in rows],linewidth=3,label='Search-first: retrieval');ax.plot(range(61),[r['filing_minutes'] for r in rows],linewidth=3,linestyle='--',label='File-first: preparation + retrieval');ax.plot(30,14,'o');ax.annotate('Break-even:30 retrievals/day',(30,14),xytext=(3,-29),textcoords='offset points',fontsize=12);ax.set(xlabel='Retrieval events per day',ylabel='Total modeled effort (minutes/day)',xlim=(0,60),ylim=(0,32));ax.legend(frameon=False,loc='upper left',fontsize=11) save(fig,stem,6,title,'modeled','Inputs are hypothetical and independent of the empirical figures. Equal retrieval success and rework are assumed. Break-even:N×f = R×(search time−folder time). Counts represent retrieval events, not necessarily unique messages.','Under hypothetical costs, file-first and search-first tie at30 retrievals/day; at15 retrievals search costs7 minutes and filing9 minutes.') stem='07-ai-workplace-estimates';rows=[dict(estimand=e,reported_change_hours=-v,reduction_hours=v,se_hours=se,normal_95_low=v-1.96*se,normal_95_high=v+1.96*se,email_workers=6441,source_table='2',source='https://arxiv.org/html/2504.11436v4',evidence_type='published_estimate_derived_interval') for e,v,se in [('Assigned access (ITT)',1.37,.13),('Induced use (IV / LATE)',2.03,.19)]];csvout(stem,rows) title='One workplace AI trial, two different effect estimates' fig,ax=new(title,'PUBLISHED ESTIMATES · Weekly Outlook-session reduction · email outcome sample:6,441',left=.27) ax.errorbar([r['reduction_hours'] for r in rows],[1,0],xerr=[1.96*r['se_hours'] for r in rows],fmt='o',markersize=10,capsize=7,elinewidth=2);ax.set_yticks([1,0],[r['estimand'] for r in rows]);ax.set(xlabel='Hours per week (positive values mean reductions)',xlim=(0,2.7),ylim=(-.5,1.7)) for y,r in zip([1,0],rows):ax.annotate(f"{r['reduction_hours']:.2f} h",(r['reduction_hours'],y),xytext=(0,18),textcoords='offset points',ha='center',fontsize=14) save(fig,stem,7,title,'published_estimate_derived_interval','Source:Dillon et al., arXiv v4, Table2. Intervals=estimate±1.96SE, unadjusted normal approximations, not multiplicity-adjusted or individual predictions. ITT and IV/LATE are not independent studies.','Reductions:1.37hours for assignment and2.03hours for induced use, with reconstructed normal95%intervals fromSEs0.13 and0.19hours.') stem='08-ai-recipient-break-even';rows=[dict(full_reading_equivalents=n,added_words=180,drafting_saved_minutes=3,extra_review_minutes=.5,reading_wpm=w,net_minutes_saved=net_ai(6,3,.5,180,n,w),evidence_type='modeled') for w in (150,238,350) for n in range(11)];csvout(stem,rows) title='A faster draft can shift work to its recipients' fig,ax=new(title,'ILLUSTRATIVE MODEL · 3 minutes drafting saved · 0.5 minutes extra review · 180 added words') for w,style in zip((150,238,350),(':','-','--')): rr=[r for r in rows if r['reading_wpm']==w];ax.plot([r['full_reading_equivalents'] for r in rr],[r['net_minutes_saved'] for r in rr],linewidth=3,linestyle=style,label=f'{w} words/minute') ax.axhline(0,linestyle='--',linewidth=1);ax.set(xlabel='Full-reading equivalents across recipients',ylabel='Net team minutes saved per message',xlim=(0,10),ylim=(-10,4));ax.legend(frameon=False,loc='lower left',fontsize=11) save(fig,stem,8,title,'modeled','Positive=savings; negative=extra effort. No change in follow-up assumed.238wpm is a general English nonfiction reference (Brysbaert,2019), not an email-comprehension benchmark.150 and350 are sensitivity assumptions.','At238wpm,the assumed three-minute drafting saving is erased around3.31full-reading equivalents after30seconds review and180extra words.') stem='09-ai-length-scenarios';cases=[('Same length\n1 reader',0,1),('180 words added\n3 readers',180,3),('180 words added\n8 readers',180,8),('180 words removed\n8 readers',-180,8)];rows=[dict(scenario=l.replace('\n','; '),added_words=w,full_reading_equivalents=n,reading_wpm=238,drafting_saved_minutes=3,extra_review_minutes=.5,net_minutes_saved=net_ai(6,3,.5,w,n,238),evidence_type='modeled') for l,w,n in cases];csvout(stem,rows) title='Message design can reverse the net time result' fig,ax=new(title,'ILLUSTRATIVE MODEL · Same drafting/review assumptions · assumed reading rate:238wpm') bars=ax.bar([c[0] for c in cases],[r['net_minutes_saved'] for r in rows],width=.55);ax.bar_label(bars,labels=[f"{r['net_minutes_saved']:+.2f} min" for r in rows],padding=7,fontsize=12);ax.axhline(0,linestyle='--',linewidth=1);ax.set(ylabel='Net team minutes saved per message',ylim=(-5.8,10.5)) save(fig,stem,9,title,'modeled','Hypothetical scenarios, not observed AI effects. Required meaning and quality must be preserved. Shorter is not automatically better; include any change in clarification, errors, or avoided downstream work separately.','The four scenarios yield2.50,0.23,−3.55,and8.55net minutes saved; these are hypothetical inputs, not measured treatment effects.') (DATA/'chart_manifest.json').write_text(json.dumps(MANIFEST,indent=2)) tests={'mg1_80':math.isclose(mg1_wait(.8,3,1),12),'mg1_90':math.isclose(mg1_wait(.9,3,1),27),'mg1_cv2':math.isclose(mg1_wait(.8,3,2),30),'backlog':clearance(20,20,18)==10,'unstable':math.isinf(clearance(20,20,20)),'file_break_even':filing(60,4,30,28,20)==(14,14),'ai_four':math.isclose(net_ai(6,3,.5,180,4,238),-.5252100840336134),'ai_shortening':math.isclose(net_ai(6,3,.5,-180,8,238),8.550420168067227),'survey_sum':math.isclose(sum([73.4,20.6,.6,5.4]),100),'nine_charts':len(MANIFEST)==9} (ROOT/'qa/calculation_tests.json').write_text(json.dumps(tests,indent=2)) assert all(tests.values()),tests print('Created nine PNG/SVG chart pairs and nine CSV datasets. Ten calculation checks passed.') if __name__=='__main__':main()