<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Alan Matthew</title>
    <description>The latest articles on DEV Community by Alan Matthew (@alan-matthew).</description>
    <link>https://dev.to/alan-matthew</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4118963%2F9a4577c7-a028-42b0-b258-00ef08cb3d1b.png</url>
      <title>DEV Community: Alan Matthew</title>
      <link>https://dev.to/alan-matthew</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alan-matthew"/>
    <language>en</language>
    <item>
      <title>A Developer's System for Finishing Online Courses While Working Full-Time</title>
      <dc:creator>Alan Matthew</dc:creator>
      <pubDate>Wed, 21 Oct 2026 14:28:36 +0000</pubDate>
      <link>https://dev.to/alan-matthew/a-developers-system-for-finishing-online-courses-while-working-full-time-15pf</link>
      <guid>https://dev.to/alan-matthew/a-developers-system-for-finishing-online-courses-while-working-full-time-15pf</guid>
      <description>&lt;p&gt;Most people don't fail online courses because the material is too hard. They fail because the course is competing with a full-time job, and the course loses by default. The fix isn't more willpower. It's better project management.&lt;/p&gt;

&lt;p&gt;If you build software, you already have the mental models. Here's how to apply them to a course.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Scope the course like a project
&lt;/h2&gt;

&lt;p&gt;Before week one, read the syllabus the way you'd read a spec.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;List every deliverable&lt;/strong&gt; with its due date and its weight in the final grade.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Find the heavy hitters.&lt;/strong&gt; If two exams and a final project make up 70% of the grade, those are your critical path. Everything else is secondary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Note the policies&lt;/strong&gt; that can sink you: late penalties, dropped-lowest-score rules, attendance or participation requirements, proctoring requirements.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Put it all in one place, such as a spreadsheet or a Notion table with these columns:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Deliverable&lt;/th&gt;
&lt;th&gt;Due&lt;/th&gt;
&lt;th&gt;Weight&lt;/th&gt;
&lt;th&gt;Est. hours&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Quiz 1&lt;/td&gt;
&lt;td&gt;Mon&lt;/td&gt;
&lt;td&gt;5%&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;todo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Project proposal&lt;/td&gt;
&lt;td&gt;Fri&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;todo&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The "Est. hours" column matters most. Courses advertise "6 hours a week," but your real number is often double that if you're also reading, practicing, and reviewing.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Do a capacity check, not a motivation check
&lt;/h2&gt;

&lt;p&gt;Open your calendar and count the hours you can &lt;em&gt;actually&lt;/em&gt; protect each week after work, commute, sleep, family, and exercise. Be pessimistic.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the course needs 10 hours and you have 6, you have a planning problem to solve &lt;strong&gt;now&lt;/strong&gt;, not a discipline problem to solve in week five.&lt;/li&gt;
&lt;li&gt;Options at that point: drop to part-time load, switch terms, negotiate a lighter work week, or pick a self-paced alternative.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Finding this out in week one costs nothing. Finding it out in week six costs a grade or a withdrawal.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Use time-boxing instead of "study when I can"
&lt;/h2&gt;

&lt;p&gt;"I'll study when I have time" never happens. Treat study blocks like meetings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fixed slots.&lt;/strong&gt; For example, Tue/Thu 7:00 to 8:30 PM and Sat 9:00 AM to 12:00 PM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One task per block.&lt;/strong&gt; "Finish problem set 3, questions 1 to 8" beats "work on stats."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phone in another room.&lt;/strong&gt; The context-switch cost is real, and you already know it from code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Short, frequent sessions beat one marathon. Research on spaced practice consistently shows better retention when you revisit material over several days than when you cram it in one sitting.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Work in a loop: learn, apply, retrieve
&lt;/h2&gt;

&lt;p&gt;Passive reading and re-watching lectures feel productive but do little. Build a tight loop instead:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Learn:&lt;/strong&gt; watch or read the material at 1x to 1.25x, in short chunks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apply:&lt;/strong&gt; do problems or build something small &lt;em&gt;immediately&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrieve:&lt;/strong&gt; close the notes and write down what you remember. Check, and fix gaps.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For programming and data courses, this is natural. Write the function, run the test, break it, fix it. For statistics and math, work problems by hand first and then verify with code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;scipy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;

&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mf"&gt;12.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;11.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;12.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;13.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;12.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;11.9&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;mean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;sd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;std&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ddof&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# sample standard deviation
&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ci&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sd&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mean=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, sd=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sd&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, 95% CI=(&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ci&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ci&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compute the interval by hand first, then run this to check yourself. The comparison tells you exactly where your understanding is shaky.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Instrument your progress
&lt;/h2&gt;

&lt;p&gt;You wouldn't ship a service without monitoring. Don't run a course without it either.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Weekly review (15 minutes, same time every week):&lt;/strong&gt; What's due in the next 7 days? What slipped? What's the estimated time versus the actual?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track grades as they post.&lt;/strong&gt; If a quiz comes back low, treat it like a failing test: find the root cause (concept gap, careless error, ran out of time) and fix that specific thing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Look at leading indicators,&lt;/strong&gt; not just grades: Did you hit your study blocks? Did you finish the practice problems before the graded ones?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. Debug early, ask for help early
&lt;/h2&gt;

&lt;p&gt;When you're stuck, apply the same discipline you'd use on a bug:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reproduce:&lt;/strong&gt; state the exact problem you can't solve.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Isolate:&lt;/strong&gt; find the smallest example that still confuses you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search:&lt;/strong&gt; the textbook, course forum, and documentation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask a good question:&lt;/strong&gt; say what you tried, what you expected, and what happened.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Instructors and TAs respond well to specific questions. Office hours are underused in online courses, and a 10-minute call can save you three hours of flailing. Study groups and communities (Discord servers, subreddits, course Slack channels) work the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Protect the critical path
&lt;/h2&gt;

&lt;p&gt;When the week goes sideways, because work spikes or something personal comes up, don't try to do everything at half quality. Triage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Protect:&lt;/strong&gt; high-weight items and anything with hard deadlines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reduce:&lt;/strong&gt; low-weight practice work, though not by much.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Communicate:&lt;/strong&gt; email your instructor &lt;em&gt;before&lt;/em&gt; a deadline you'll miss. Many will work with you, but almost none will if you disappear.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  8. Avoid the traps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Falling behind in week 2 to 3.&lt;/strong&gt; The first few weeks set the pace. Recovering from a two-week hole is far harder than staying level.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Over-enrolling.&lt;/strong&gt; One course done well beats two done badly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Perfectionism on low-weight work.&lt;/strong&gt; A solid 90% on a 5% assignment isn't worth three extra hours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating the course as separate from the rest of your life.&lt;/strong&gt; Tell your manager, partner, or family what your heavy weeks look like.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A one-page checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Every deliverable logged with due date, weight, and time estimate&lt;/li&gt;
&lt;li&gt;[ ] Protected study blocks on the calendar for the full term&lt;/li&gt;
&lt;li&gt;[ ] Capacity check done: hours needed ≤ hours available&lt;/li&gt;
&lt;li&gt;[ ] Weekly 15-minute review scheduled&lt;/li&gt;
&lt;li&gt;[ ] Instructor/TA contact info and office hours saved&lt;/li&gt;
&lt;li&gt;[ ] A plan for the two heaviest weeks (exams, final project)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;A course is a project with a spec, a deadline, and a limited budget of your time. If you scope it honestly, schedule it ruthlessly, and monitor it weekly, the odds shift heavily in your favor. And if the numbers don't add up at the start, adjusting your load early is the best decision you can make.&lt;/p&gt;

&lt;p&gt;If someone needs &lt;a href="https://takemycourseforme.com/" rel="noopener noreferrer"&gt;help with online classes&lt;/a&gt;, Please do connect.&lt;/p&gt;

&lt;p&gt;What's worked for you when balancing coursework and a full-time job? Share it in the comments.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>career</category>
      <category>learning</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Getting Help With Your Thesis Without Risking Your Degree: A Practical Guide</title>
      <dc:creator>Alan Matthew</dc:creator>
      <pubDate>Tue, 20 Oct 2026 10:50:27 +0000</pubDate>
      <link>https://dev.to/alan-matthew/getting-help-with-your-thesis-without-risking-your-degree-a-practical-guide-2epf</link>
      <guid>https://dev.to/alan-matthew/getting-help-with-your-thesis-without-risking-your-degree-a-practical-guide-2epf</guid>
      <description>&lt;p&gt;Writing a thesis or dissertation is hard, and most students need help at some point. The question that causes trouble is &lt;em&gt;what kind&lt;/em&gt; of help is acceptable. This guide covers the support options that universities generally allow, how to disclose them, and the red flags that should make you walk away.&lt;/p&gt;

&lt;p&gt;Rules vary by institution and programme, so always check your own handbook. Treat this as a starting point, not a substitute for your university's policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Help that is almost always fine
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Your supervisor and committee
&lt;/h3&gt;

&lt;p&gt;Regular meetings are the best use of your time. Bring a one-page summary of progress, a list of specific questions, and a draft section to discuss. Specific questions get specific answers.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. University writing centres
&lt;/h3&gt;

&lt;p&gt;Most universities offer free sessions on structure, argument, and clarity. They are designed for exactly this stage.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Statistics and methods clinics
&lt;/h3&gt;

&lt;p&gt;Many departments run consulting hours. Use them to confirm that your test choice matches your research question and that you've checked assumptions.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Peer review
&lt;/h3&gt;

&lt;p&gt;Swap chapters with a classmate. Reading someone else's work shows you problems in your own.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Proofreading and language editing
&lt;/h3&gt;

&lt;p&gt;Editing for grammar and clarity is commonly allowed, especially for students writing in a second language. The key rule: the ideas, analysis, and arguments must remain yours. Many universities ask you to declare editing help, so check.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Tools
&lt;/h3&gt;

&lt;p&gt;Reference managers (Zotero, Mendeley), version control (Git), LaTeX or Pandoc templates, and grammar checkers are all standard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Help that puts your degree at risk
&lt;/h2&gt;

&lt;p&gt;Paying someone to write chapters, produce an analysis you can't explain, or generate text you then submit as your own is treated as academic misconduct at virtually every university. Consequences can include failing the module, degree revocation, and a permanent record, sometimes years after graduation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Red flags when evaluating any thesis service
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Promises of a grade or a guaranteed similarity score.&lt;/strong&gt; No honest provider can control either.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offers to beat plagiarism or AI detectors.&lt;/strong&gt; This signals the work is meant to deceive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No contact with the actual author.&lt;/strong&gt; You should be able to ask questions about any analysis you'll defend.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pressure to hide the help from your supervisor.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delivery of finished chapters for you to submit.&lt;/strong&gt; Feedback and models are one thing; ready-to-submit text is another.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No written scope of what is and isn't included.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A simple self-test
&lt;/h2&gt;

&lt;p&gt;Before accepting any help, ask:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Could I explain every argument and analysis in my thesis to my examiners, unprepared?&lt;/li&gt;
&lt;li&gt;Would I be comfortable if my supervisor saw exactly what help I received?&lt;/li&gt;
&lt;li&gt;Does my university's policy permit this type of support?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If any answer is "no," don't do it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disclosing help properly
&lt;/h2&gt;

&lt;p&gt;When your programme asks you to declare support, be specific and honest: "Language editing of chapters 2-4 by [name/service]" or "Statistical consultation on regression model selection." Disclosure protects you. Undisclosed help that later comes to light does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  A workflow that reduces the need for outside help
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Draft in Markdown or LaTeX and keep it in a Git repository, so you can track every change.&lt;/li&gt;
&lt;li&gt;Write a little every day, even 300 words, instead of waiting for long sessions.&lt;/li&gt;
&lt;li&gt;Keep a running reading log with a one-paragraph summary of each paper.&lt;/li&gt;
&lt;li&gt;Set chapter deadlines with your supervisor and send drafts early.&lt;/li&gt;
&lt;li&gt;Run your analysis as a script, so it's reproducible and easy to explain.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;Good help makes you a stronger researcher. The test is simple: after the help, can you stand behind and defend the work as entirely your own? If so, you're on solid ground.&lt;/p&gt;

&lt;p&gt;And if you are confused and unsure what to do, then here is the &lt;a href="https://dissertationwritingservices.qa/" rel="noopener noreferrer"&gt;Dissertation Writing Service Help&lt;/a&gt; just take it then...&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What has helped you most during your thesis? Share it in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>career</category>
      <category>productivity</category>
      <category>beginners</category>
      <category>writing</category>
    </item>
    <item>
      <title>Building Ecological Simulations</title>
      <dc:creator>Alan Matthew</dc:creator>
      <pubDate>Mon, 19 Oct 2026 09:53:48 +0000</pubDate>
      <link>https://dev.to/alan-matthew/building-ecological-simulations-3gl5</link>
      <guid>https://dev.to/alan-matthew/building-ecological-simulations-3gl5</guid>
      <description>&lt;h1&gt;
  
  
  Building Ecological Simulations: How to Model Population Growth &amp;amp; Carrying Capacity
&lt;/h1&gt;

&lt;p&gt;When building simulations—whether you are working on game economies, agent-based modeling (ABM), resource management systems, or ecological software—modeling how populations or entities grow over time is a common challenge. &lt;/p&gt;

&lt;p&gt;While simple linear or exponential growth models are easy to implement, they fail in real-world scenarios where resources are finite. Unchecked exponential growth leads to infinite numbers, which breaks simulations and game loops.&lt;/p&gt;

&lt;p&gt;To solve this, developers rely on the &lt;strong&gt;Verhulst Logistic Growth Model&lt;/strong&gt;, which introduces a hard ceiling known as &lt;strong&gt;Carrying Capacity (K)&lt;/strong&gt;. In this post, we’ll break down the mathematics behind logistic growth, examine how the rate of change behaves under resource constraints, and explore an interactive &lt;a href="https://takemybiologyclass.us/tools/carrying-capacity-calculator" rel="noopener noreferrer"&gt;Carrying Capacity Calculator&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. From Exponential J-Curves to Logistic S-Curves
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Exponential Baseline
&lt;/h3&gt;

&lt;p&gt;In an environment with unlimited resources, a population ($N$) grows at a rate proportional to its current size. This is expressed by the differential equation:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;dN/dt = rN&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;dN/dt&lt;/strong&gt;: The instantaneous rate of population change over time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;N&lt;/strong&gt;: Current population size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;r&lt;/strong&gt;: Intrinsic per capita growth rate (biotic potential).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Integrating this yields the exponential growth formula &lt;code&gt;Nt = N0 * e^(rt)&lt;/code&gt;, which results in an unrealistic J-curve where growth accelerates infinitely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding the Environmental Brake: Logistic Growth
&lt;/h3&gt;

&lt;p&gt;To fix this, Pierre François Verhulst introduced a scaling feedback factor in 1838: &lt;code&gt;(K - N) / K&lt;/code&gt;. This term represents the unused capacity of the environment.&lt;/p&gt;

&lt;p&gt;The complete logistic differential equation is:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;dN/dt = rN * (K - N) / K&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;K&lt;/strong&gt;: &lt;strong&gt;Carrying capacity&lt;/strong&gt; (the maximum sustainable population size determined by available resources).&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  How the Braking Term Behaves:
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;When N &amp;lt;&amp;lt; K (Low Density):&lt;/strong&gt; The fraction &lt;code&gt;(K - N) / K&lt;/code&gt; is approximately 1, meaning the population grows near its exponential rate (&lt;code&gt;rN&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When N -&amp;gt; K (Approaching Capacity):&lt;/strong&gt; The fraction approaches 0, causing the growth rate &lt;code&gt;dN/dt&lt;/code&gt; to decelerate smoothly to zero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When N &amp;gt; K (Overshoot):&lt;/strong&gt; The fraction becomes negative, driving &lt;code&gt;dN/dt &amp;lt; 0&lt;/code&gt;, which simulates a population dieback or crash back down toward the threshold.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  2. Analytical Solution vs. Numerical Simulation
&lt;/h2&gt;

&lt;p&gt;When writing code to simulate population growth over discrete time steps, developers often use Euler’s method (iterative loops). However, numerical step approximations introduce cumulative rounding errors. &lt;/p&gt;

&lt;p&gt;To calculate the exact population size (&lt;code&gt;Nt&lt;/code&gt;) at any arbitrary future time &lt;code&gt;t&lt;/code&gt; instantly, we use the analytical integral of the logistic differential equation:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Nt = K / (1 + ((K - N0) / N0) * e^(-rt))&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;N0&lt;/strong&gt;: Initial population size at &lt;code&gt;t = 0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;e&lt;/strong&gt;: Euler's number (~2.71828).&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. Finding the Inflection Point &amp;amp; Peak Growth Rate
&lt;/h2&gt;

&lt;p&gt;In system dynamics and resource planning, finding the inflection point on an S-curve is critical. This is the exact moment where population growth transitions from acceleration to deceleration.&lt;/p&gt;

&lt;p&gt;Maximum population growth always occurs when the population is exactly half of the carrying capacity:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;N_inflection = K / 2&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Substituting this back into the logistic equation gives the maximum theoretical growth rate (Maximum Sustainable Yield):&lt;/p&gt;

&lt;p&gt;&lt;code&gt;max(dN/dt) = rK / 4&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This principle is widely used in fisheries management, wildlife conservation, and capacity planning to harvest or scale resources without destabilizing the system.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Implementation Example (JavaScript)
&lt;/h2&gt;

&lt;p&gt;Here is a clean JavaScript implementation of the integrated logistic growth formula that you can plug straight into a dashboard or simulation script:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
javascript
/**
 * Calculates future population size using the integrated logistic growth model.
 * 
 * @param {number} N0 - Initial population size
 * @param {number} K  - Carrying capacity
 * @param {number} r  - Intrinsic growth rate
 * @param {number} t  - Time elapsed
 * @returns {number}  - Population size at time t
 */
function calculateLogisticGrowth(N0, K, r, t) {
    if (N0 &amp;lt;= 0 || K &amp;lt;= 0 || r &amp;lt;= 0 || t &amp;lt; 0) {
        throw new Error("Invalid parameters provided.");
    }

    const numerator = K;
    const denominator = 1 + ((K - N0) / N0) * Math.exp(-r * t);

    return numerator / denominator;
}

// Example: Starting with 50 individuals, K = 1000, r = 0.15, over 25 time units
const populationAtT25 = calculateLogisticGrowth(50, 1000, 0.15, 25);
console.log(`Population at t=25: ${Math.round(populationAtT25)}`);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
    </item>
    <item>
      <title>One-Way ANOVA Explained: The F-Test, a Worked Example, Tukey HSD, and Code in Python/R</title>
      <dc:creator>Alan Matthew</dc:creator>
      <pubDate>Sun, 18 Oct 2026 09:41:40 +0000</pubDate>
      <link>https://dev.to/alan-matthew/one-way-anova-explained-the-f-test-a-worked-example-tukey-hsd-and-code-in-pythonr-5l0</link>
      <guid>https://dev.to/alan-matthew/one-way-anova-explained-the-f-test-a-worked-example-tukey-hsd-and-code-in-pythonr-5l0</guid>
      <description>&lt;p&gt;You have three or more groups and one number measured on each: exam scores under three study methods, page load times across three CDNs, conversion rates across four landing pages. The natural question is "do the group means differ?"&lt;/p&gt;

&lt;p&gt;With two groups you'd use a t-test. With three or more, the standard tool is &lt;strong&gt;one-way ANOVA&lt;/strong&gt; (analysis of variance). This post walks through why it exists, how it works, a worked example you can verify by hand, and how to reproduce it in code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just run a bunch of t-tests?
&lt;/h2&gt;

&lt;p&gt;Because the false-positive rate adds up. Each t-test at α = 0.05 has a 5% chance of a false alarm when there is no real difference. Run several and the chance that &lt;em&gt;at least one&lt;/em&gt; is a false alarm climbs fast.&lt;/p&gt;

&lt;p&gt;I simulated this: 5 groups of 10 observations, all drawn from the &lt;strong&gt;same&lt;/strong&gt; normal distribution, so there is nothing to find. Over 10,000 repetitions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Runs with a "significant" result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;All 10 pairwise t-tests (any p &amp;lt; .05)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;28.6%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One-way ANOVA (p &amp;lt; .05)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.9%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pairwise approach gives you a false finding in more than a quarter of experiments. ANOVA asks one question ("is there any difference at all?") and keeps the error rate near the 5% you chose.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea: split the variance into two parts
&lt;/h2&gt;

&lt;p&gt;ANOVA compares two kinds of variation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Between-group variation:&lt;/strong&gt; how far the group means are from the overall (grand) mean. If the groups really differ, this is large.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Within-group variation:&lt;/strong&gt; how spread out the individual values are inside each group. This is the natural noise.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the group means are all equal in the population, then both of these estimate the same noise level, and their ratio should be near 1. If the means differ, the between-group part gets inflated and the ratio gets bigger.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hypotheses
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;H0: μ1 = μ2 = ... = μk       (all population means are equal)
Ha: at least one mean differs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Note that ANOVA is an &lt;strong&gt;omnibus&lt;/strong&gt; test. A significant result says "something differs", not &lt;em&gt;which&lt;/em&gt; groups differ. That's what post-hoc tests are for.&lt;/p&gt;
&lt;h3&gt;
  
  
  The formulas
&lt;/h3&gt;

&lt;p&gt;Let &lt;code&gt;k&lt;/code&gt; be the number of groups, &lt;code&gt;N&lt;/code&gt; the total number of observations, &lt;code&gt;n_j&lt;/code&gt; the size of group &lt;code&gt;j&lt;/code&gt;, &lt;code&gt;x̄_j&lt;/code&gt; the group mean, and &lt;code&gt;x̄&lt;/code&gt; the grand mean.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SS_between = Σ n_j × (x̄_j − x̄)²
SS_within  = Σ Σ (x_ij − x̄_j)²
SS_total   = SS_between + SS_within

df_between = k − 1
df_within  = N − k

MS_between = SS_between / df_between
MS_within  = SS_within  / df_within

F = MS_between / MS_within
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Under the null hypothesis, &lt;code&gt;F&lt;/code&gt; follows an F-distribution with &lt;code&gt;(k − 1, N − k)&lt;/code&gt; degrees of freedom. A large &lt;code&gt;F&lt;/code&gt; produces a small p-value.&lt;/p&gt;
&lt;h2&gt;
  
  
  Worked example
&lt;/h2&gt;

&lt;p&gt;Twelve students are randomly assigned to one of three study methods, four per group. Here are their exam scores:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flashcards&lt;/th&gt;
&lt;th&gt;Summarizing&lt;/th&gt;
&lt;th&gt;Practice tests&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;82&lt;/td&gt;
&lt;td&gt;75&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;85&lt;/td&gt;
&lt;td&gt;72&lt;/td&gt;
&lt;td&gt;92&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;78&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;88&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;88&lt;/td&gt;
&lt;td&gt;77&lt;/td&gt;
&lt;td&gt;94&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(Four per group is far too small for real research. It's here so you can check every number by hand.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: group sums and means&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Flashcards:   sum = 333, mean = 83.25
Summarizing:  sum = 304, mean = 76.00
Practice:     sum = 364, mean = 91.00

Grand sum = 1001, N = 12, grand mean = 83.4167
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Step 2: between-group sum of squares&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SS_between = 4(83.25 − 83.4167)² + 4(76.00 − 83.4167)² + 4(91.00 − 83.4167)²
           = 0.111 + 220.028 + 230.028
           = 450.167
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Step 3: within-group sum of squares&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Flashcards:   (−1.25)² + (1.75)² + (−5.25)² + (4.75)² = 54.75
Summarizing:  (−1)² + (−4)² + (4)² + (1)²             = 34.00
Practice:     (−1)² + (1)² + (−3)² + (3)²             = 20.00

SS_within = 54.75 + 34.00 + 20.00 = 108.75
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Check: &lt;code&gt;SS_total = 450.167 + 108.75 = 558.917&lt;/code&gt;, which matches summing squared deviations from the grand mean directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: degrees of freedom and mean squares&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;df_between = 3 − 1  = 2
df_within  = 12 − 3 = 9

MS_between = 450.167 / 2 = 225.083
MS_within  = 108.75  / 9 = 12.083
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Step 5: the F statistic&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;F = 225.083 / 12.083 = 18.63
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Step 6: significance&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;df = (2, 9)&lt;/code&gt;, the critical F at α = .05 is &lt;strong&gt;4.26&lt;/strong&gt;. Our F of 18.63 is far beyond it, and the p-value is about &lt;strong&gt;.0006&lt;/strong&gt;. We reject the null: at least one study method has a different mean.&lt;/p&gt;

&lt;p&gt;Here is the summary table, in the layout most software prints:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;SS&lt;/th&gt;
&lt;th&gt;df&lt;/th&gt;
&lt;th&gt;MS&lt;/th&gt;
&lt;th&gt;F&lt;/th&gt;
&lt;th&gt;p&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Between groups&lt;/td&gt;
&lt;td&gt;450.17&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;225.08&lt;/td&gt;
&lt;td&gt;18.63&lt;/td&gt;
&lt;td&gt;.0006&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Within groups&lt;/td&gt;
&lt;td&gt;108.75&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;12.08&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total&lt;/td&gt;
&lt;td&gt;558.92&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  How big is the effect?
&lt;/h3&gt;

&lt;p&gt;A p-value tells you whether there is an effect, not how large it is. The usual effect size for one-way ANOVA is &lt;strong&gt;eta squared&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;η² = SS_between / SS_total = 450.17 / 558.92 ≈ 0.81
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;About 81% of the variance in scores is associated with the study method. Eta squared is biased upward in small samples, so many people prefer &lt;strong&gt;omega squared&lt;/strong&gt;, which for this data is about &lt;strong&gt;0.75&lt;/strong&gt;. Either way, the effect is large (though remember, &lt;code&gt;n = 4&lt;/code&gt; per group).&lt;/p&gt;
&lt;h2&gt;
  
  
  Which groups differ? Tukey HSD
&lt;/h2&gt;

&lt;p&gt;Now the follow-up. Tukey's Honestly Significant Difference test compares every pair of groups while keeping the overall false-positive rate at 5%.&lt;/p&gt;

&lt;p&gt;For a balanced design, two means differ significantly if their gap exceeds:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HSD = q_crit × √(MS_within / n)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;For &lt;code&gt;k = 3&lt;/code&gt; groups and &lt;code&gt;df_within = 9&lt;/code&gt;, &lt;code&gt;q_crit ≈ 3.948&lt;/code&gt;, so:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HSD = 3.948 × √(12.083 / 4) ≈ 6.86
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Any pair of means more than 6.86 points apart is significant:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Comparison&lt;/th&gt;
&lt;th&gt;Mean difference&lt;/th&gt;
&lt;th&gt;Adjusted p&lt;/th&gt;
&lt;th&gt;Significant?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Practice − Flashcards&lt;/td&gt;
&lt;td&gt;7.75&lt;/td&gt;
&lt;td&gt;.028&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flashcards − Summarizing&lt;/td&gt;
&lt;td&gt;7.25&lt;/td&gt;
&lt;td&gt;.039&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Practice − Summarizing&lt;/td&gt;
&lt;td&gt;15.00&lt;/td&gt;
&lt;td&gt;.0005&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So all three methods differ from one another, with practice tests ahead of both and summarizing at the bottom.&lt;/p&gt;
&lt;h2&gt;
  
  
  Check the assumptions
&lt;/h2&gt;

&lt;p&gt;One-way ANOVA assumes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Independence.&lt;/strong&gt; Observations don't influence each other. This comes from how you collected the data, not from a test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Approximate normality within each group&lt;/strong&gt; (more precisely, of the residuals).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Equal variances across groups&lt;/strong&gt; (homogeneity of variance).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For the example data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Levene's test&lt;/strong&gt; for equal variances: p = .59, no evidence of unequal variances.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shapiro-Wilk on the residuals:&lt;/strong&gt; p = .90, no evidence of non-normality.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(With only 12 data points, these tests have very little power, so "no evidence of a problem" is weaker than it sounds. Look at plots too.)&lt;/p&gt;

&lt;p&gt;ANOVA is reasonably robust to mild departures, especially when group sizes are equal. If the variances are clearly unequal, switch to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Welch's ANOVA&lt;/strong&gt;, which doesn't assume equal variances. On our data it gives &lt;code&gt;F(2, 5.77) = 22.79, p = .0018&lt;/code&gt;, the same conclusion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kruskal-Wallis&lt;/strong&gt;, a rank-based test for when normality is doubtful. Here &lt;code&gt;H = 8.98, p = .011&lt;/code&gt;. It is less powerful than ANOVA when ANOVA's assumptions hold, which is why its p-value is larger.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Python with SciPy
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;scipy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;

&lt;span class="n"&gt;flashcards&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;82&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;78&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;88&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;summarizing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;72&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;77&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;practice&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;92&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;88&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;94&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# The omnibus test
&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;f_oneway&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flashcards&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;summarizing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;practice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;F = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, p = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# F = 18.63, p = 0.0006
&lt;/span&gt;
&lt;span class="c1"&gt;# Post-hoc
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tukey_hsd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flashcards&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;summarizing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;practice&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;# Assumption checks
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;levene&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flashcards&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;summarizing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;practice&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;# p ≈ 0.59
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;stats.tukey_hsd&lt;/code&gt; needs SciPy 1.8 or later.&lt;/p&gt;
&lt;h3&gt;
  
  
  Python with statsmodels (full ANOVA table + Tukey)
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;statsmodels.api&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;sm&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;statsmodels.formula.api&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;smf&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;statsmodels.stats.multicomp&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pairwise_tukeyhsd&lt;/span&gt;

&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flashcards&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;summarizing&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;practice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flashcards&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;summarizing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;practice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;smf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ols&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score ~ C(method)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;anova_lm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;typ&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;pairwise_tukeyhsd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Python with pingouin (effect size and Welch in one line each)
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pingouin&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pg&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;anova&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dv&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;between&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detailed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;# includes np2 (η²)
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;welch_anova&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dv&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;between&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pairwise_tukey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dv&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;between&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  R
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight r"&gt;&lt;code&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;c&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;82&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;78&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;88&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;72&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;77&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;92&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;88&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;94&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;factor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;rep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;c&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"flashcards"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"summarizing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"practice"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;each&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;fit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;aov&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;TukeyHSD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;oneway.test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="c1"&gt;# Welch's ANOVA (var.equal = FALSE is the default)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;kruskal.test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Excel
&lt;/h3&gt;

&lt;p&gt;Enable the Analysis ToolPak (File &amp;gt; Options &amp;gt; Add-ins), then &lt;strong&gt;Data &amp;gt; Data Analysis &amp;gt; Anova: Single Factor&lt;/strong&gt;. It returns the ANOVA table with F and p. It does &lt;strong&gt;not&lt;/strong&gt; run Tukey, so for post-hoc comparisons you need another tool.&lt;/p&gt;
&lt;h3&gt;
  
  
  SPSS
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Analyze &amp;gt; Compare Means &amp;gt; One-Way ANOVA&lt;/code&gt;, put the outcome in &lt;em&gt;Dependent List&lt;/em&gt; and the grouping variable in &lt;em&gt;Factor&lt;/em&gt;, then click &lt;strong&gt;Post Hoc&lt;/strong&gt; and tick &lt;strong&gt;Tukey&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  A neat connection: two groups
&lt;/h2&gt;

&lt;p&gt;ANOVA on two groups is just a t-test in disguise: &lt;code&gt;F = t²&lt;/code&gt; and the p-values are identical. Using the flashcards and summarizing groups from above, the pooled two-sample t-test gives &lt;code&gt;t = 2.67&lt;/code&gt;, so &lt;code&gt;t² = 7.11&lt;/code&gt;, and &lt;code&gt;f_oneway&lt;/code&gt; on the same two groups gives &lt;code&gt;F = 7.11&lt;/code&gt; with &lt;code&gt;p = .037&lt;/code&gt;. Same answer, two routes.&lt;/p&gt;
&lt;h2&gt;
  
  
  Try it without writing code
&lt;/h2&gt;

&lt;p&gt;If you want to paste in numbers and get the ANOVA table without setting up a script, this free tool does it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://takemystatisticsclassforme.us/tools/one-way-anova-calculator" rel="noopener noreferrer"&gt;One-Way ANOVA Calculator&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://takemystatisticsclassforme.us/tools/one-way-anova-calculator" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Ftakemystatisticsclassforme.us%2Fassets%2Fimages%2Flogo.webp" height="177" class="m-0" width="720"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://takemystatisticsclassforme.us/tools/one-way-anova-calculator" rel="noopener noreferrer" class="c-link"&gt;
            One-Way ANOVA Calculator: One Way Analysis of Variance Calculator with Steps
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Free online One-Way ANOVA calculator. Supports raw and summary statistics, renders step-by-step LaTeX derivations, visualizes F-distribution curves, and computes Tukey HSD comparisons.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Ftakemystatisticsclassforme.us%2Ffavicon.ico" width="64" height="64"&gt;
          takemystatisticsclassforme.us
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;What it offers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Raw data&lt;/strong&gt; input, or &lt;strong&gt;summary statistics&lt;/strong&gt; (n, mean, standard deviation per group) if you only have what a textbook problem gives you&lt;/li&gt;
&lt;li&gt;A significance level you can set (default 0.05)&lt;/li&gt;
&lt;li&gt;Group descriptive statistics, the full ANOVA summary table, and the F statistic, p-value and critical F&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tukey HSD&lt;/strong&gt; pairwise comparisons&lt;/li&gt;
&lt;li&gt;A shaded F-distribution curve, step-by-step math, and an APA-style write-up you can adapt&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Paste the three groups from the worked example. You should see &lt;code&gt;F(2, 9) = 18.63&lt;/code&gt;, &lt;code&gt;p ≈ .0006&lt;/code&gt;, and the three Tukey comparisons above. If you get something different, double-check that each group went in as its own set of values.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Running many t-tests instead of ANOVA.&lt;/strong&gt; See the simulation at the top.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stopping at "p &amp;lt; .05".&lt;/strong&gt; Report an effect size and follow up with a post-hoc test to say &lt;em&gt;which&lt;/em&gt; groups differ.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skipping post-hoc tests, or using one when the omnibus test isn't significant.&lt;/strong&gt; Post-hocs are for following up a significant F.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignoring unequal variances and unequal group sizes together.&lt;/strong&gt; That combination is where standard ANOVA is least reliable. Use Welch's ANOVA.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating repeated measures as independent groups.&lt;/strong&gt; If the same people appear in every condition, you need repeated-measures ANOVA or a mixed model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reading too much into non-significance.&lt;/strong&gt; A large p-value with tiny groups usually means low power, not "no effect".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tiny samples.&lt;/strong&gt; Four per group is for textbooks. Plan for enough observations per group to have reasonable power.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comparing more than one factor with one-way ANOVA.&lt;/strong&gt; Two grouping variables call for a two-way ANOVA (or a regression with interaction terms).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How to report it
&lt;/h2&gt;

&lt;p&gt;A concise APA-style write-up of the example:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A one-way ANOVA showed a significant effect of study method on exam score, &lt;em&gt;F&lt;/em&gt;(2, 9) = 18.63, &lt;em&gt;p&lt;/em&gt; &amp;lt; .001, η² = .81. Tukey HSD comparisons indicated that practice tests (&lt;em&gt;M&lt;/em&gt; = 91.00, &lt;em&gt;SD&lt;/em&gt; = 2.58) outperformed both flashcards (&lt;em&gt;M&lt;/em&gt; = 83.25, &lt;em&gt;SD&lt;/em&gt; = 4.27) and summarizing (&lt;em&gt;M&lt;/em&gt; = 76.00, &lt;em&gt;SD&lt;/em&gt; = 3.37), and flashcards outperformed summarizing (all &lt;em&gt;p&lt;/em&gt; &amp;lt; .05).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is ANOVA a test of variances or of means?&lt;/strong&gt;&lt;br&gt;
Of means. It's called "analysis of variance" because it tests the means by comparing two variance estimates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What does a large F mean?&lt;/strong&gt;&lt;br&gt;
The between-group variation is large compared to the within-group noise. Under the null, F hovers around 1.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My ANOVA is significant. Do all the groups differ?&lt;/strong&gt;&lt;br&gt;
Not necessarily. At least one differs. Use Tukey HSD (or another post-hoc method) to see which pairs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need equal group sizes?&lt;/strong&gt;&lt;br&gt;
No, but equal sizes make ANOVA more robust to unequal variances. Tukey's procedure also has a variant for unbalanced groups (Tukey-Kramer).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if my data are ordinal or clearly non-normal?&lt;/strong&gt;&lt;br&gt;
Consider the Kruskal-Wallis test.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Many t-tests inflate false positives. ANOVA tests "any difference" in one go.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;F = MS_between / MS_within&lt;/code&gt;; a big F means the group means are spread further than noise explains.&lt;/li&gt;
&lt;li&gt;A significant F is &lt;strong&gt;omnibus&lt;/strong&gt;. Follow up with Tukey HSD and report an effect size (η² or ω²).&lt;/li&gt;
&lt;li&gt;Check independence, normality and equal variances. Use Welch's ANOVA or Kruskal-Wallis when assumptions fail.&lt;/li&gt;
&lt;li&gt;For two groups, &lt;code&gt;F = t²&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you try the &lt;a href="https://takemystatisticsclassforme.us/tools/one-way-anova-calculator" rel="noopener noreferrer"&gt;calculator&lt;/a&gt; or run the code on your own data, I'd like to hear what you get. Questions about post-hoc choices, unequal variances or effect sizes are welcome in the comments.&lt;/p&gt;

</description>
      <category>statistics</category>
      <category>python</category>
      <category>datascience</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Finding Outliers with the IQR Method: Formula, Quartile Gotchas, Code in Python/R/SQL, and a Free Calculator</title>
      <dc:creator>Alan Matthew</dc:creator>
      <pubDate>Sun, 18 Oct 2026 09:36:23 +0000</pubDate>
      <link>https://dev.to/alan-matthew/finding-outliers-with-the-iqr-method-formula-quartile-gotchas-code-in-pythonrsql-and-a-free-dcj</link>
      <guid>https://dev.to/alan-matthew/finding-outliers-with-the-iqr-method-formula-quartile-gotchas-code-in-pythonrsql-and-a-free-dcj</guid>
      <description>&lt;p&gt;Every dataset eventually produces a value that makes you stop and ask, "is that real?" A $9,000 order in a column of $40 orders. A heart rate of 300. A test score of 120 out of 100. Before you average, model, or plot anything, you need a defensible way to flag those values.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;IQR method&lt;/strong&gt; (Tukey's fences) is the most widely used rule for this. It's simple, it needs no assumption about the shape of your data, and it's what most box plots are drawing under the hood.&lt;/p&gt;

&lt;p&gt;This post covers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What the IQR and the fences are&lt;/li&gt;
&lt;li&gt;A fully worked example you can verify by hand&lt;/li&gt;
&lt;li&gt;A gotcha that trips up almost everyone: &lt;strong&gt;different tools compute quartiles differently&lt;/strong&gt;, and that can change which points get flagged&lt;/li&gt;
&lt;li&gt;Code in Python, pandas, R, Excel and SQL&lt;/li&gt;
&lt;li&gt;How the IQR rule compares with z-scores and MAD&lt;/li&gt;
&lt;li&gt;Where the IQR rule fails, and what to do with an outlier once you've found one&lt;/li&gt;
&lt;li&gt;A free calculator if you want to paste data and see the result&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The building blocks: quartiles and the IQR
&lt;/h2&gt;

&lt;p&gt;Sort your data. The quartiles split it into four chunks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Q1&lt;/strong&gt; (25th percentile): a quarter of the values fall below it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Q2&lt;/strong&gt; (the median): half fall below it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Q3&lt;/strong&gt; (75th percentile): three quarters fall below it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;strong&gt;interquartile range&lt;/strong&gt; is the width of the middle half of your data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;IQR = Q3 − Q1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Why does this matter for outlier detection? Because Q1 and Q3 barely move when you add one wild value at the end. The mean and standard deviation, by contrast, get dragged toward the outlier. A yardstick that isn't distorted by the thing you're measuring is exactly what you want.&lt;/p&gt;
&lt;h2&gt;
  
  
  Tukey's fences
&lt;/h2&gt;

&lt;p&gt;John Tukey proposed drawing "fences" a fixed multiple of the IQR beyond the quartiles:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Lower inner fence = Q1 − 1.5 × IQR
Upper inner fence = Q3 + 1.5 × IQR

Lower outer fence = Q1 − 3 × IQR
Upper outer fence = Q3 + 3 × IQR
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Then classify each point:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Where the value falls&lt;/th&gt;
&lt;th&gt;Label&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Inside the inner fences&lt;/td&gt;
&lt;td&gt;Normal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Beyond an inner fence, but inside the outer fence&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Mild outlier&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Beyond an outer fence&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Extreme outlier&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On a box plot, the whiskers stop at the most extreme points that are still inside the inner fences, and anything beyond them is drawn as an individual dot.&lt;/p&gt;
&lt;h3&gt;
  
  
  Where does 1.5 come from?
&lt;/h3&gt;

&lt;p&gt;It's a convention, not a theorem. Tukey picked it as a practical balance. For normally distributed data, the 1.5 × IQR fences sit at about ±2.7 standard deviations from the mean, so roughly &lt;strong&gt;0.7%&lt;/strong&gt; of perfectly ordinary observations get flagged. I checked this by simulating 100,000 normal values and got 0.74%. The 3 × IQR fences sit near ±4.7 standard deviations, flagging only about &lt;strong&gt;0.0002%&lt;/strong&gt; (two in a million) of normal data.&lt;/p&gt;

&lt;p&gt;That last point matters: &lt;strong&gt;on normal data, the 1.5 rule will flag roughly 1 in 140 observations even though nothing is wrong.&lt;/strong&gt; With 10,000 rows you should expect around 70 "outliers" from pure chance. Treat the flags as candidates for inspection, not verdicts.&lt;/p&gt;
&lt;h2&gt;
  
  
  Worked example
&lt;/h2&gt;

&lt;p&gt;A teacher records 12 test scores:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;55, 62, 68, 70, 72, 75, 78, 80, 85, 88, 92, 120
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;1. Sort.&lt;/strong&gt; Already sorted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Median.&lt;/strong&gt; With &lt;code&gt;n = 12&lt;/code&gt;, average the 6th and 7th values:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Q2 = (75 + 78) / 2 = 76.5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;3. Q1.&lt;/strong&gt; Median of the lower half (55, 62, 68, 70, 72, 75):&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Q1 = (68 + 70) / 2 = 69
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;4. Q3.&lt;/strong&gt; Median of the upper half (78, 80, 85, 88, 92, 120):&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Q3 = (85 + 88) / 2 = 86.5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;5. IQR.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;IQR = 86.5 − 69 = 17.5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;6. Inner fences.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Lower = 69   − 1.5 × 17.5 = 42.75
Upper = 86.5 + 1.5 × 17.5 = 112.75
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;7. Outer fences.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Lower = 69   − 3 × 17.5 = 16.5
Upper = 86.5 + 3 × 17.5 = 139
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;8. Classify.&lt;/strong&gt; Everything from 55 to 92 is between 42.75 and 112.75, so it's normal. The score &lt;strong&gt;120&lt;/strong&gt; is above 112.75 but below 139, so it's a &lt;strong&gt;mild outlier&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Is 120 a typo, extra credit, or a legitimately great student? The method can't tell you. It only tells you where to look.&lt;/p&gt;
&lt;h2&gt;
  
  
  The gotcha: there is no single "the" quartile
&lt;/h2&gt;

&lt;p&gt;Here is the part that causes the most confusion. There are many accepted definitions of a quartile, and software disagrees.&lt;/p&gt;

&lt;p&gt;For the 12 scores above, here is what different conventions give. I computed these with NumPy's &lt;code&gt;percentile&lt;/code&gt; and its &lt;code&gt;method&lt;/code&gt; argument:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Q1&lt;/th&gt;
&lt;th&gt;Q3&lt;/th&gt;
&lt;th&gt;IQR&lt;/th&gt;
&lt;th&gt;Lower fence&lt;/th&gt;
&lt;th&gt;Upper fence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Median of halves (textbook)&lt;/td&gt;
&lt;td&gt;69&lt;/td&gt;
&lt;td&gt;86.5&lt;/td&gt;
&lt;td&gt;17.5&lt;/td&gt;
&lt;td&gt;42.75&lt;/td&gt;
&lt;td&gt;112.75&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linear interpolation (NumPy default, Excel &lt;code&gt;QUARTILE.INC&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;69.5&lt;/td&gt;
&lt;td&gt;85.75&lt;/td&gt;
&lt;td&gt;16.25&lt;/td&gt;
&lt;td&gt;45.125&lt;/td&gt;
&lt;td&gt;110.125&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;weibull&lt;/code&gt; (Excel &lt;code&gt;QUARTILE.EXC&lt;/code&gt;, R &lt;code&gt;type = 6&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;68.5&lt;/td&gt;
&lt;td&gt;87.25&lt;/td&gt;
&lt;td&gt;18.75&lt;/td&gt;
&lt;td&gt;40.375&lt;/td&gt;
&lt;td&gt;115.375&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For this dataset, 120 is flagged under all three. But watch what happens when the borderline value is a little smaller. Change 120 to &lt;strong&gt;111&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Upper fence&lt;/th&gt;
&lt;th&gt;Is 111 flagged?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Median of halves&lt;/td&gt;
&lt;td&gt;112.75&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linear (NumPy default)&lt;/td&gt;
&lt;td&gt;110.125&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;weibull&lt;/code&gt; (Excel &lt;code&gt;.EXC&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;115.375&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same data, same "1.5 × IQR rule", different conclusions. This is why you should:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Know which quartile convention your course, tool or team uses&lt;/strong&gt;, and say so when you report results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be suspicious of points sitting right at a fence.&lt;/strong&gt; A value that is flagged under one convention and not another is a borderline case, not a clear outlier.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Odd sample sizes
&lt;/h3&gt;

&lt;p&gt;When &lt;code&gt;n&lt;/code&gt; is odd there is a real median value, and textbooks split on what to do with it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Inclusive:&lt;/strong&gt; include the median in both halves&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exclusive:&lt;/strong&gt; leave it out of both halves&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Take 11 values: &lt;code&gt;10, 12, 14, 15, 18, 21, 22, 23, 24, 25, 95&lt;/code&gt;. The median is 21.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Convention&lt;/th&gt;
&lt;th&gt;Q1&lt;/th&gt;
&lt;th&gt;Q3&lt;/th&gt;
&lt;th&gt;IQR&lt;/th&gt;
&lt;th&gt;Upper inner fence&lt;/th&gt;
&lt;th&gt;Upper outer fence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Median included in both halves&lt;/td&gt;
&lt;td&gt;14.5&lt;/td&gt;
&lt;td&gt;23.5&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;td&gt;50.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median excluded&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;39&lt;/td&gt;
&lt;td&gt;54&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here 95 is an extreme outlier under both, but the fences move by a few units. If you're checking homework against an answer key, match the convention the key uses.&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Python (NumPy)
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;iqr_outliers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;linear&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;q1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;iqr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;q3&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;q1&lt;/span&gt;
    &lt;span class="n"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;upper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;q1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;iqr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q3&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;iqr&lt;/span&gt;
    &lt;span class="n"&gt;mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;q1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;q1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;q3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;q3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;iqr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;iqr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lower&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;upper&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;outliers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;62&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;68&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;72&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;78&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;88&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;92&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# "hazen" matches the median-of-halves result for this dataset
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;iqr_outliers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hazen&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="c1"&gt;# {'q1': 69.0, 'q3': 86.5, 'iqr': 17.5, 'lower': 42.75, 'upper': 112.75, 'outliers': [120.0]}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A note on &lt;code&gt;method=&lt;/code&gt;: it needs NumPy 1.22 or later. On older versions the argument is called &lt;code&gt;interpolation&lt;/code&gt; and has fewer options. Also, &lt;code&gt;hazen&lt;/code&gt; happens to reproduce the median-of-halves quartiles for this even-sized dataset, but it is not the same rule in general. For odd &lt;code&gt;n&lt;/code&gt; and other layouts it can differ, so check it against a hand calculation for your own data.&lt;/p&gt;

&lt;p&gt;To get mild and extreme labels:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q3&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;iqr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;q3&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;q1&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;q1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;iqr&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;q3&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;iqr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;extreme&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;q1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;iqr&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;q3&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;iqr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mild&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;normal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;iqr_outliers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hazen&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;q1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;q3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  pandas: do it per group
&lt;/h3&gt;

&lt;p&gt;This is the mistake I see most often in real data work. If your dataset has groups with very different scales, a single global IQR will either miss obvious problems or flag whole groups. Compute fences &lt;strong&gt;within&lt;/strong&gt; each group:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;

&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;group&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;102&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;105&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;98&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;160&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;q1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;quantile&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mf"&gt;0.25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.75&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;iqr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;q3&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;q1&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;q1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;iqr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;q3&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;iqr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;outlier_global&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;outlier_by_group&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;group&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;On this toy data, the global rule flags &lt;strong&gt;nothing&lt;/strong&gt;, because group &lt;code&gt;b&lt;/code&gt; is so much larger than group &lt;code&gt;a&lt;/code&gt; that the pooled IQR is huge. The grouped rule correctly flags &lt;code&gt;40&lt;/code&gt; (in group a) and &lt;code&gt;160&lt;/code&gt; (in group b).&lt;/p&gt;
&lt;h3&gt;
  
  
  R
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight r"&gt;&lt;code&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;c&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;55&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;62&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;68&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;72&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;78&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;88&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;92&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;120&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;boxplot.stats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c1"&gt;# uses Tukey hinges, the same as the box plot&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c1"&gt;# manual version (quantile() defaults to type = 7, i.e. linear interpolation)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quantile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;c&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0.75&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;iqr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1.5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;iqr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1.5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;iqr&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Excel / Google Sheets
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Q1:    =QUARTILE.INC(A1:A12, 1)
Q3:    =QUARTILE.INC(A1:A12, 3)
IQR:   =Q3 - Q1
Lower: =Q1 - 1.5*IQR
Upper: =Q3 + 1.5*IQR
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Swap in &lt;code&gt;QUARTILE.EXC&lt;/code&gt; if you need the exclusive convention. Use conditional formatting with a formula like &lt;code&gt;=OR(A1&amp;lt;$lower, A1&amp;gt;$upper)&lt;/code&gt; to highlight flagged cells.&lt;/p&gt;
&lt;h3&gt;
  
  
  SQL (PostgreSQL)
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;percentile_cont&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;WITHIN&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;q1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;percentile_cont&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;WITHIN&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;q3&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;exam_scores&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;exam_scores&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;q1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;q3&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;q1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;q3&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;q3&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;q1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;percentile_cont&lt;/code&gt; uses linear interpolation, so it matches NumPy's default and Excel's &lt;code&gt;QUARTILE.INC&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  IQR vs z-score vs MAD
&lt;/h2&gt;

&lt;p&gt;You'll often see a second outlier rule: flag anything more than 2 or 3 standard deviations from the mean. It has a structural weakness: &lt;strong&gt;the outliers contaminate the mean and the standard deviation you use to detect them.&lt;/strong&gt; This is called masking.&lt;/p&gt;

&lt;p&gt;Try it on this small sample:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10, 11, 12, 12, 13, 13, 14, 15, 60, 65
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Two values are obviously far from the rest. But:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mean = 22.5, standard deviation = 21.16&lt;/li&gt;
&lt;li&gt;The z-score of 65 is only &lt;strong&gt;2.01&lt;/strong&gt;, and 60 gets &lt;strong&gt;1.77&lt;/strong&gt;. A "|z| &amp;gt; 3" rule flags &lt;strong&gt;nothing&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;In fact, for a sample of 10 the largest possible |z| is &lt;code&gt;(n − 1) / √n ≈ 2.85&lt;/code&gt;, so a cutoff of 3 can &lt;em&gt;never&lt;/em&gt; fire.&lt;/li&gt;
&lt;li&gt;The IQR rule (linear quartiles) gives Q1 = 12, Q3 = 14.75, IQR = 2.75, and fences of 7.875 and 18.875. It flags &lt;strong&gt;both&lt;/strong&gt; 60 and 65.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A third option is the &lt;strong&gt;modified z-score&lt;/strong&gt; based on the median absolute deviation (MAD):&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;M = 0.6745 × (x − median) / MAD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The common cutoff is |M| &amp;gt; 3.5. On the same sample the median is 13 and the MAD is 1.5, which gives M ≈ 21 for 60 and M ≈ 23 for 65. Both are flagged emphatically.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;

&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;65&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;med&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;median&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;mad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;median&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;med&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.6745&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;med&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;mad&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;3.5&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;   &lt;span class="c1"&gt;# [60 65]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Quick comparison:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Assumes normality?&lt;/th&gt;
&lt;th&gt;Resistant to the outliers themselves?&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;z-score&lt;/td&gt;
&lt;td&gt;Yes, roughly&lt;/td&gt;
&lt;td&gt;No (masking)&lt;/td&gt;
&lt;td&gt;Needs large &lt;code&gt;n&lt;/code&gt; for a cutoff of 3 to be usable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IQR / Tukey&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Symmetric fences, so can struggle with heavy skew&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MAD / modified z&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Good default for small, messy samples&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  Where the IQR rule breaks down
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Skewed data.&lt;/strong&gt; The fences are symmetric around the middle half of the data. For a right-skewed variable (income, response times, order values), the long tail looks like a pile of outliers. I generated 1,000 values from a lognormal distribution (a standard model for skewed positive data) and the 1.5 × IQR rule flagged &lt;strong&gt;92 of them (9.2%)&lt;/strong&gt;, none of which were errors. After taking the log of the data, which makes it normal, the same rule flagged &lt;strong&gt;9 (0.9%)&lt;/strong&gt;. If your variable is naturally skewed, transform first or use a method designed for skew, such as the adjusted boxplot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Tiny samples.&lt;/strong&gt; With &lt;code&gt;n&lt;/code&gt; of 4 or 5, quartiles are hardly defined, and one value can swing Q1 or Q3 a lot. The results are fragile.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Multimodal data.&lt;/strong&gt; If you have two real clusters, the IQR spans the gap between them and the fences become meaningless. Split by group first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Discrete or heavily tied data.&lt;/strong&gt; If most values are identical, the IQR can be zero, which makes the fences collapse onto the quartiles and flags every other value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Time series.&lt;/strong&gt; A value can be perfectly normal for the series overall but anomalous for its season or trend. Detrend or de-seasonalize first.&lt;/p&gt;
&lt;h2&gt;
  
  
  You found an outlier. Now what?
&lt;/h2&gt;

&lt;p&gt;Detection is the easy part. Before touching the value:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check for an error.&lt;/strong&gt; Typos, unit mix-ups (kg vs lb), sensor glitches and import bugs cause a lot of outliers. If you can trace it to an error, fix it or remove it, and write down why.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check whether it's real.&lt;/strong&gt; A billionaire in an income survey is a genuine observation. Deleting it changes what your analysis says about the population.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decide based on your goal.&lt;/strong&gt; Options, roughly from least to most invasive:

&lt;ul&gt;
&lt;li&gt;Keep it and use robust statistics (median, IQR, trimmed mean)&lt;/li&gt;
&lt;li&gt;Keep it and run the analysis both with and without, reporting both&lt;/li&gt;
&lt;li&gt;Transform the variable (log, square root)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Winsorize&lt;/strong&gt; (cap values at a percentile)&lt;/li&gt;
&lt;li&gt;Remove it, with documented justification&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document the rule.&lt;/strong&gt; Report the multiplier, the quartile convention and the number of points affected, so someone else can reproduce your result.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Removing a point just because it failed a rule, with no investigation, is how analyses quietly become wrong.&lt;/p&gt;
&lt;h2&gt;
  
  
  Try it without writing code
&lt;/h2&gt;

&lt;p&gt;If you just want to paste a list of numbers and see the fences, this free tool does it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://takemystatisticsclassforme.us/tools/iqr-outlier-calculator" rel="noopener noreferrer"&gt;IQR Outlier Calculator&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://takemystatisticsclassforme.us/tools/iqr-outlier-calculator" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Ftakemystatisticsclassforme.us%2Fassets%2Fimages%2Flogo.webp" height="177" class="m-0" width="720"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://takemystatisticsclassforme.us/tools/iqr-outlier-calculator" rel="noopener noreferrer" class="c-link"&gt;
            IQR Outlier Calculator: Find Outliers with Q1, Q3 &amp;amp; Fences
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Free IQR outlier calculator instantly detects mild and extreme outliers. Returns Q1, Q3, IQR, inner and outer fences, and step-by-step LaTeX formulas for any dataset.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Ftakemystatisticsclassforme.us%2Ffavicon.ico" width="64" height="64"&gt;
          takemystatisticsclassforme.us
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;What it offers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Paste your dataset separated by commas, spaces or newlines (minimum 4 values)&lt;/li&gt;
&lt;li&gt;Choose a &lt;strong&gt;1.5 × IQR&lt;/strong&gt; fence (standard) or &lt;strong&gt;3 × IQR&lt;/strong&gt; (extreme outliers only)&lt;/li&gt;
&lt;li&gt;Choose an &lt;strong&gt;inclusive&lt;/strong&gt; or &lt;strong&gt;exclusive&lt;/strong&gt; quartile method&lt;/li&gt;
&lt;li&gt;See the five-number summary, IQR, inner and outer fences, and a box plot&lt;/li&gt;
&lt;li&gt;Every data point is classified as normal, mild outlier or extreme outlier&lt;/li&gt;
&lt;li&gt;A step-by-step breakdown of the math&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Paste the 12 test scores from the worked example above. You should get Q1 = 69, Q3 = 86.5, IQR = 17.5, fences of 42.75 and 112.75, and 120 flagged as a mild outlier. Because of the quartile-convention issue I described above, make sure the calculator's quartile setting matches the convention you're comparing against.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not sorting first&lt;/strong&gt; when computing quartiles by hand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mixing quartile conventions&lt;/strong&gt; between your textbook, calculator, Excel and Python, then wondering why the numbers differ in the second decimal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Using 1.5 × IQR when you wanted extreme outliers.&lt;/strong&gt; The standard rule flags mild ones too. Use 3 × IQR for extreme only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Running it on categorical data.&lt;/strong&gt; The rule needs meaningful numeric values.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Computing one global IQR across heterogeneous groups.&lt;/strong&gt; Do it per group.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deleting flagged points automatically&lt;/strong&gt; instead of investigating them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Applying it to strongly skewed data&lt;/strong&gt; without a transform.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reporting "outliers were removed"&lt;/strong&gt; with no rule, no count and no justification.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to report it
&lt;/h2&gt;

&lt;p&gt;A clear, reproducible statement looks like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Outliers were identified using Tukey's rule (values below Q1 − 1.5 × IQR or above Q3 + 1.5 × IQR, with quartiles computed by linear interpolation). One of 12 observations (score = 120) was flagged as a mild outlier. It was retained after verification against the original records, and the analysis was repeated without it as a sensitivity check.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is 1.5 × IQR always the right multiplier?&lt;/strong&gt;&lt;br&gt;
No. It is a convention that works reasonably well for roughly symmetric data. For small, noisy samples people sometimes use 3. For very large datasets, 1.5 will flag many perfectly good points, so you may want a larger multiplier or a different method.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use it on non-normal data?&lt;/strong&gt;&lt;br&gt;
Yes, that's one of its main advantages. It relies on order statistics, not on a normal distribution. It just works best on roughly symmetric data. For heavily skewed data, transform first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How many data points do I need?&lt;/strong&gt;&lt;br&gt;
Technically 4, which is what many tools require. Practically, treat anything below about 10 to 15 values as fragile.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the difference between a mild and an extreme outlier?&lt;/strong&gt;&lt;br&gt;
A mild outlier lies beyond the inner fence (1.5 × IQR) but inside the outer fence (3 × IQR). An extreme outlier lies beyond the outer fence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should I always remove outliers?&lt;/strong&gt;&lt;br&gt;
No. First find out whether they are errors or real observations.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;IQR = Q3 − Q1&lt;/code&gt;, and the fences are &lt;code&gt;Q1 − 1.5·IQR&lt;/code&gt; and &lt;code&gt;Q3 + 1.5·IQR&lt;/code&gt; (use 3 for extreme).&lt;/li&gt;
&lt;li&gt;The rule needs no normality assumption and isn't distorted by the outliers it's hunting, unlike a z-score cutoff.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quartile definitions differ between tools.&lt;/strong&gt; Borderline points can flip, so state your convention.&lt;/li&gt;
&lt;li&gt;Compute fences &lt;strong&gt;per group&lt;/strong&gt;, and transform skewed data first.&lt;/li&gt;
&lt;li&gt;A flag is a reason to investigate, not a reason to delete.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you try the &lt;a href="https://takemystatisticsclassforme.us/tools/iqr-outlier-calculator" rel="noopener noreferrer"&gt;calculator&lt;/a&gt; or run the code above on your own data, I'd like to hear what you find. Questions about skewed data, grouped data or choosing a multiplier are welcome in the comments.&lt;/p&gt;

</description>
      <category>statistics</category>
      <category>python</category>
      <category>datascience</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Cronbach's Alpha Explained: Formula, Worked Example, Python/R Code, and a Free Calculator</title>
      <dc:creator>Alan Matthew</dc:creator>
      <pubDate>Sun, 18 Oct 2026 09:26:59 +0000</pubDate>
      <link>https://dev.to/alan-matthew/cronbachs-alpha-explained-formula-worked-example-pythonr-code-and-a-free-calculator-103e</link>
      <guid>https://dev.to/alan-matthew/cronbachs-alpha-explained-formula-worked-example-pythonr-code-and-a-free-calculator-103e</guid>
      <description>&lt;p&gt;You built a survey with several questions that are supposed to measure the same thing (say, "job satisfaction" or "stats anxiety"). Before you average those items into one score, you should check that they actually hang together. That is what &lt;strong&gt;Cronbach's alpha&lt;/strong&gt; does: it estimates the &lt;strong&gt;internal consistency&lt;/strong&gt; of a multi-item scale.&lt;/p&gt;

&lt;p&gt;This post covers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What alpha measures, and what it doesn't&lt;/li&gt;
&lt;li&gt;The formula&lt;/li&gt;
&lt;li&gt;A worked example with real numbers (including item-deleted analysis)&lt;/li&gt;
&lt;li&gt;Code in Python and R to reproduce it&lt;/li&gt;
&lt;li&gt;The mistakes that wreck your alpha&lt;/li&gt;
&lt;li&gt;A free calculator if you'd rather paste data than write code&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What alpha measures
&lt;/h2&gt;

&lt;p&gt;Alpha ranges from 0 to 1 (it can go negative when something is wrong, usually un-recoded items). Higher means the items correlate more strongly with each other, which suggests they are tapping a shared underlying trait.&lt;/p&gt;

&lt;p&gt;Common rules of thumb, which are conventions rather than laws:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Alpha&lt;/th&gt;
&lt;th&gt;Typical label&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;≥ .90&lt;/td&gt;
&lt;td&gt;Excellent (but check for redundant items)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.80 – .89&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.70 – .79&lt;/td&gt;
&lt;td&gt;Acceptable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.60 – .69&lt;/td&gt;
&lt;td&gt;Questionable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;lt; .60&lt;/td&gt;
&lt;td&gt;Poor&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Context matters. A 3-item scale with α = .68 can be fine. A 30-item scale with α = .68 means the items barely correlate, because alpha rises with the number of items.&lt;/p&gt;

&lt;h2&gt;
  
  
  The formula
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;α = (k / (k − 1)) × (1 − Σσ²ᵢ / σ²ₓ)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;k&lt;/code&gt; = number of items&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;σ²ᵢ&lt;/code&gt; = variance of item &lt;em&gt;i&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;σ²ₓ&lt;/code&gt; = variance of the total score (sum of all items per respondent)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The intuition: if items are positively correlated, the variance of the total score is much larger than the sum of the individual item variances. A big gap pushes alpha toward 1.&lt;/p&gt;

&lt;p&gt;There is also a &lt;strong&gt;standardized alpha&lt;/strong&gt;, based on the average inter-item correlation &lt;code&gt;r̄&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;α_std = (k × r̄) / (1 + (k − 1) × r̄)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Use it when your items are on very different scales. For items on the same Likert scale, raw and standardized alpha are usually close.&lt;/p&gt;
&lt;h2&gt;
  
  
  Worked example
&lt;/h2&gt;

&lt;p&gt;Eight respondents answered a 4-item scale on a 1–5 Likert scale:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Respondent&lt;/th&gt;
&lt;th&gt;Item 1&lt;/th&gt;
&lt;th&gt;Item 2&lt;/th&gt;
&lt;th&gt;Item 3&lt;/th&gt;
&lt;th&gt;Item 4&lt;/th&gt;
&lt;th&gt;Total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Step 1: sample variance of each item&lt;/strong&gt; (divide by &lt;code&gt;n − 1&lt;/code&gt;)&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Item 1: 1.4286
Item 2: 1.4286
Item 3: 1.2679
Item 4: 1.1250
Σσ²ᵢ = 5.25
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Step 2: variance of the total scores&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;σ²ₓ = 12.2143
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Step 3: plug in&lt;/strong&gt; (&lt;code&gt;k = 4&lt;/code&gt;)&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;α = (4 / 3) × (1 − 5.25 / 12.2143)
  = 1.3333 × 0.5702
  ≈ 0.760
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;An alpha of about &lt;strong&gt;.76&lt;/strong&gt; is "acceptable". But the interesting part is the item-level breakdown.&lt;/p&gt;
&lt;h3&gt;
  
  
  Item-deleted analysis
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Corrected item-total r&lt;/th&gt;
&lt;th&gt;Alpha if item deleted&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;.767&lt;/td&gt;
&lt;td&gt;.578&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;.642&lt;/td&gt;
&lt;td&gt;.655&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;.901&lt;/td&gt;
&lt;td&gt;.502&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;.057&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;.922&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Items 1 to 3 pull their weight: removing any of them lowers alpha. &lt;strong&gt;Item 4 barely correlates with the rest of the scale (r ≈ .06)&lt;/strong&gt;, and dropping it would raise alpha from .76 to about .92. That is the signal to go look at that question. Maybe it is badly worded, maybe it is reverse-keyed and you forgot to recode it, or maybe it measures something else.&lt;/p&gt;

&lt;p&gt;One caveat: don't delete items on statistics alone. If an item is conceptually essential to the construct, think hard before removing it just to bump the number.&lt;/p&gt;
&lt;h2&gt;
  
  
  Reproduce it in code
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Python (no extra libraries beyond NumPy)
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;

&lt;span class="n"&gt;X&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cronbach_alpha&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;item_vars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ddof&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;total_var&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ddof&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;item_vars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total_var&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alpha = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;cronbach_alpha&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# alpha = 0.760
&lt;/span&gt;
&lt;span class="c1"&gt;# Item-deleted alpha and corrected item-total correlation
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
    &lt;span class="n"&gt;rest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;corrcoef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;rest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Item &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: r_it = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, alpha if deleted = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;cronbach_alpha&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Python with pingouin
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pingouin&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pg&lt;/span&gt;

&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;q1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;q2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;q3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;q4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;alpha&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ci&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cronbach_alpha&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alpha&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ci&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# also gives a 95% confidence interval
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  R
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight r"&gt;&lt;code&gt;&lt;span class="n"&gt;library&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;psych&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;matrix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;c&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ncol&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;byrow&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;alpha&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;# raw alpha, standardized alpha, item-deleted stats&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  SPSS
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Analyze &amp;gt; Scale &amp;gt; Reliability Analysis&lt;/code&gt;, then tick "Scale if item deleted" under Statistics.&lt;/p&gt;
&lt;h2&gt;
  
  
  Skip the code: free online calculator
&lt;/h2&gt;

&lt;p&gt;If you just want to paste responses and get a result, this is a free tool that handles it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://takemystatisticsclassforme.us/tools/cronbach-alpha-calculator" rel="noopener noreferrer"&gt;Cronbach's Alpha Calculator&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://takemystatisticsclassforme.us/tools/cronbach-alpha-calculator" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Ftakemystatisticsclassforme.us%2Fassets%2Fimages%2Flogo.webp" height="177" class="m-0" width="720"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://takemystatisticsclassforme.us/tools/cronbach-alpha-calculator" rel="noopener noreferrer" class="c-link"&gt;
            Cronbach Alpha Calculator: Online Reliability Solver with Steps
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Calculate internal consistency reliability for Likert scales. Computes raw alpha, standardized alpha, item-deleted statistics, and APA reports.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Ftakemystatisticsclassforme.us%2Ffavicon.ico" width="64" height="64"&gt;
          takemystatisticsclassforme.us
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;What it offers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Raw data mode:&lt;/strong&gt; paste one respondent per row, items separated by spaces, tabs or commas&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Summary statistics mode:&lt;/strong&gt; if you already have &lt;code&gt;k&lt;/code&gt;, the sum of item variances and the total variance&lt;/li&gt;
&lt;li&gt;Raw alpha and standardized alpha&lt;/li&gt;
&lt;li&gt;An item-total table with corrected item-total correlations and alpha-if-item-deleted&lt;/li&gt;
&lt;li&gt;A step-by-step breakdown of the math&lt;/li&gt;
&lt;li&gt;An APA-style sentence you can adapt for your write-up&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Paste the 8×4 table from the example above and you should get α ≈ .76, with Item 4 standing out as the weak one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting to reverse-code negatively worded items.&lt;/strong&gt; If "I enjoy statistics" and "Statistics makes me anxious" are on the same scale, one of them must be flipped first. For a 1 to 5 scale, use &lt;code&gt;new = 6 − old&lt;/code&gt;. Un-recoded items create negative covariances and can push alpha toward zero or below.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating a high alpha as proof of one dimension.&lt;/strong&gt; Alpha measures consistency, not unidimensionality. A long scale made of two unrelated subscales can still produce a high alpha. Run a factor analysis if dimensionality matters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignoring scale length.&lt;/strong&gt; More items inflate alpha. Look at the average inter-item correlation too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chasing α &amp;gt; .95.&lt;/strong&gt; Very high values often mean redundant, near-duplicate questions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Computing it on tiny samples.&lt;/strong&gt; With small &lt;code&gt;N&lt;/code&gt; the estimate is unstable. Report a confidence interval, which &lt;code&gt;pingouin&lt;/code&gt; gives you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assuming alpha is a fixed property of the questionnaire.&lt;/strong&gt; It describes the scores in &lt;em&gt;your&lt;/em&gt; sample. Recompute it for each study.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Alpha's assumptions (and when to reach for omega)
&lt;/h2&gt;

&lt;p&gt;Alpha assumes &lt;strong&gt;essential tau-equivalence&lt;/strong&gt;: every item measures the construct with equal strength. In real scales the items usually have unequal factor loadings, in which case alpha tends to &lt;em&gt;underestimate&lt;/em&gt; reliability. McDonald's &lt;strong&gt;omega (ω)&lt;/strong&gt;, computed from a factor model, doesn't have that limitation, and many journals now like to see it reported alongside alpha. In R, &lt;code&gt;psych::omega()&lt;/code&gt; is a good starting point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reporting it (APA style)
&lt;/h2&gt;

&lt;p&gt;APA drops the leading zero for values that can't exceed 1:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The 4-item scale showed acceptable internal consistency (&lt;em&gt;N&lt;/em&gt; = 8, Cronbach's α = .76).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you removed items, report alpha before and after and say which items you dropped and why.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Alpha = how well a set of items hangs together as one scale.&lt;/li&gt;
&lt;li&gt;Compute it with &lt;code&gt;k/(k−1) × (1 − Σ item variances / total variance)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Always look at &lt;strong&gt;item-total correlations&lt;/strong&gt; and &lt;strong&gt;alpha-if-deleted&lt;/strong&gt;, not just the headline number.&lt;/li&gt;
&lt;li&gt;Reverse-code first, check dimensionality, and consider omega.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you try the &lt;a href="https://takemystatisticsclassforme.us/tools/cronbach-alpha-calculator" rel="noopener noreferrer"&gt;calculator&lt;/a&gt; or run your own data through the code above, I'd like to hear what you get. Questions or edge cases (ordinal data, missing responses, subscales) are welcome in the comments.&lt;/p&gt;

</description>
      <category>statistics</category>
      <category>python</category>
      <category>datascience</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Confidence Intervals for a Mean: z vs t, a Worked Example, and a Free Calculator</title>
      <dc:creator>Alan Matthew</dc:creator>
      <pubDate>Sun, 18 Oct 2026 09:25:09 +0000</pubDate>
      <link>https://dev.to/alan-matthew/confidence-intervals-for-a-mean-z-vs-t-a-worked-example-and-a-free-calculator-1825</link>
      <guid>https://dev.to/alan-matthew/confidence-intervals-for-a-mean-z-vs-t-a-worked-example-and-a-free-calculator-1825</guid>
      <description>&lt;p&gt;If you work with data, you will eventually have to say something like "the average is 71.5, give or take." A &lt;strong&gt;confidence interval (CI) for the mean&lt;/strong&gt; is how you say that properly: a point estimate plus a range that reflects the uncertainty from only having a sample.&lt;/p&gt;

&lt;p&gt;This post covers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The formula and what each piece does&lt;/li&gt;
&lt;li&gt;When to use z vs t (the part people get wrong most often)&lt;/li&gt;
&lt;li&gt;A fully worked example you can check by hand&lt;/li&gt;
&lt;li&gt;The same calculation in Python, R and Excel&lt;/li&gt;
&lt;li&gt;A free calculator that shows the steps, so you can sanity-check your own work&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The formula
&lt;/h2&gt;

&lt;p&gt;A CI for a mean is the sample mean plus or minus a margin of error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CI = x̄ ± E
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The margin of error depends on whether the population standard deviation (σ) is known.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;σ known → z-interval&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CI = x̄ ± z(α/2) · σ / √n
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;σ unknown → t-interval&lt;/strong&gt; (this is almost always your real situation)&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CI = x̄ ± t(α/2, n−1) · s / √n
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;x̄&lt;/code&gt; is the sample mean&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;n&lt;/code&gt; is the sample size&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;s&lt;/code&gt; is the sample standard deviation (divide by &lt;code&gt;n − 1&lt;/code&gt;, not &lt;code&gt;n&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;s / √n&lt;/code&gt; is the &lt;strong&gt;standard error&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;z&lt;/code&gt; or &lt;code&gt;t&lt;/code&gt; is the critical value for your confidence level&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Common z critical values: &lt;strong&gt;1.645&lt;/strong&gt; (90%), &lt;strong&gt;1.960&lt;/strong&gt; (95%), &lt;strong&gt;2.576&lt;/strong&gt; (99%).&lt;/p&gt;
&lt;h2&gt;
  
  
  z or t? A quick rule
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;σ is known&lt;/strong&gt; (rare outside textbooks): use z.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;σ is unknown and estimated from your sample&lt;/strong&gt;: use t with &lt;code&gt;n − 1&lt;/code&gt; degrees of freedom.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The t-distribution has heavier tails, which accounts for the extra uncertainty of estimating σ. As &lt;code&gt;n&lt;/code&gt; grows it converges to the normal, so for large samples the two give nearly identical answers. Many courses use &lt;code&gt;n ≥ 30&lt;/code&gt; as the point where z is a fine approximation, but using t when σ is unknown is never wrong.&lt;/p&gt;
&lt;h2&gt;
  
  
  Worked example
&lt;/h2&gt;

&lt;p&gt;Say you measure the resting heart rate (bpm) of 8 adults:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;72, 68, 75, 70, 74, 71, 69, 73
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;We want a 95% CI for the population mean. σ is unknown, so we use t.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Sample mean&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;x̄ = 572 / 8 = 71.5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;2. Sample standard deviation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The sum of squared deviations is 42.00, so:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;s = √(42 / 7) = √6 ≈ 2.4495
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;3. Standard error&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SE = 2.4495 / √8 ≈ 0.8660
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;4. Critical value&lt;/strong&gt; (df = 7, 95% confidence)&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;t = 2.3646
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;5. Margin of error&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;E = 2.3646 × 0.8660 ≈ 2.0477
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;6. Interval&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;71.5 ± 2.0477  →  (69.4523, 73.5477)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;So we are 95% confident the true mean resting heart rate lies between about &lt;strong&gt;69.45&lt;/strong&gt; and &lt;strong&gt;73.55&lt;/strong&gt; bpm.&lt;/p&gt;
&lt;h2&gt;
  
  
  Verify it in code
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Python
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;scipy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;

&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;72&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;68&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;74&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;71&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;69&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;73&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;mean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;se&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# uses ddof=1 (sample std dev)
&lt;/span&gt;
&lt;span class="n"&gt;low&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;high&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;se&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;95% CI: (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;low&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;high&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# 95% CI: (69.4523, 73.5477)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;For a z-interval with a known σ:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;sigma&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;2.5&lt;/span&gt;  &lt;span class="c1"&gt;# known population std dev
&lt;/span&gt;&lt;span class="n"&gt;se_z&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sigma&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;low&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;high&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;se_z&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  R
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight r"&gt;&lt;code&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;c&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;72&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;68&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;74&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;71&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;69&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;73&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;t.test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;conf.level&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;conf.int&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Excel / Google Sheets
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=CONFIDENCE.T(0.05, STDEV.S(A1:A8), COUNT(A1:A8))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That returns the margin of error. Add and subtract it from &lt;code&gt;AVERAGE(A1:A8)&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Try it without writing code
&lt;/h2&gt;

&lt;p&gt;If you just want to paste in numbers and see the result, I use this free tool:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://takemystatisticsclassforme.us/tools/confidence-interval-calculator-for-mean" rel="noopener noreferrer"&gt;Confidence Interval Calculator for Mean&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://takemystatisticsclassforme.us/tools/confidence-interval-calculator-for-mean" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Ftakemystatisticsclassforme.us%2Fassets%2Fimages%2Flogo.webp" height="177" class="m-0" width="720"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://takemystatisticsclassforme.us/tools/confidence-interval-calculator-for-mean" rel="noopener noreferrer" class="c-link"&gt;
            Confidence Interval Calculator for Mean: Compute CI with Steps
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Free confidence interval calculator for population mean. Supports z-interval and t-interval methods with detailed step-by-step calculations, margin of error, and LaTeX formula rendering.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Ftakemystatisticsclassforme.us%2Ffavicon.ico" width="64" height="64"&gt;
          takemystatisticsclassforme.us
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;What it does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Accepts &lt;strong&gt;raw data&lt;/strong&gt; (comma, space or newline separated) or &lt;strong&gt;summary statistics&lt;/strong&gt; (mean, n, standard deviation)&lt;/li&gt;
&lt;li&gt;Supports &lt;strong&gt;z-interval and t-interval&lt;/strong&gt;, with an auto-detect option&lt;/li&gt;
&lt;li&gt;Offers 90%, 95% and 99% confidence levels&lt;/li&gt;
&lt;li&gt;Returns the sample mean, standard error, critical value, margin of error and the interval bounds&lt;/li&gt;
&lt;li&gt;Shows the &lt;strong&gt;step-by-step working&lt;/strong&gt;, which is the useful part if you are learning or checking your own calculations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Paste the heart-rate data from the example above and you should get the same &lt;code&gt;(69.4523, 73.5477)&lt;/code&gt; interval.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Using z when σ is unknown.&lt;/strong&gt; This understates the margin of error, especially for small samples.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Misreading what "95%" means.&lt;/strong&gt; It does &lt;em&gt;not&lt;/em&gt; mean there is a 95% probability the true mean is inside your specific interval. It means that if you repeated the sampling process many times, about 95% of the intervals built this way would contain the true mean.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dividing by &lt;code&gt;n&lt;/code&gt; instead of &lt;code&gt;n − 1&lt;/code&gt;&lt;/strong&gt; when computing &lt;code&gt;s&lt;/code&gt; from a sample (Bessel's correction).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignoring assumptions.&lt;/strong&gt; The method assumes roughly normal data or a large enough sample for the Central Limit Theorem to help. With a small, clearly skewed sample, consider bootstrapping instead.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What changes the width of the interval?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sample size:&lt;/strong&gt; the standard error shrinks with &lt;code&gt;√n&lt;/code&gt;, so you need to &lt;strong&gt;quadruple&lt;/strong&gt; the sample to &lt;strong&gt;halve&lt;/strong&gt; the margin of error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Variability:&lt;/strong&gt; noisier data means a wider interval.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confidence level:&lt;/strong&gt; 99% is wider than 95%, which is wider than 90%. You trade precision for certainty.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;A CI for a mean is just &lt;code&gt;x̄ ± critical value × standard error&lt;/code&gt;. Get the z vs t choice right, use &lt;code&gt;n − 1&lt;/code&gt; for the sample standard deviation, and interpret the result as a statement about the &lt;em&gt;method&lt;/em&gt;, not a probability about one interval.&lt;/p&gt;

&lt;p&gt;If you want to double-check your work, run your numbers through the &lt;a href="https://takemystatisticsclassforme.us/tools/confidence-interval-calculator-for-mean" rel="noopener noreferrer"&gt;free confidence interval calculator&lt;/a&gt; and compare it to your Python, R or Excel output.&lt;/p&gt;

&lt;p&gt;Questions or edge cases (small samples, skewed data, two-sample intervals)? Drop them in the comments.&lt;/p&gt;

</description>
      <category>statistics</category>
      <category>datascience</category>
      <category>python</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Build an Accounts Receivable Turnover and DSO Calculator in JavaScript (Plus the Quarterly-Data Bug)</title>
      <dc:creator>Alan Matthew</dc:creator>
      <pubDate>Sat, 17 Oct 2026 10:48:14 +0000</pubDate>
      <link>https://dev.to/alan-matthew/build-an-accounts-receivable-turnover-and-dso-calculator-in-javascript-plus-the-quarterly-data-bug-4jp4</link>
      <guid>https://dev.to/alan-matthew/build-an-accounts-receivable-turnover-and-dso-calculator-in-javascript-plus-the-quarterly-data-bug-4jp4</guid>
      <description>&lt;p&gt;If you build software for small businesses, finance teams, or accounting students, sooner or later someone asks for an accounts receivable (AR) dashboard. And sooner or later you'll be asked to show two numbers on it: &lt;strong&gt;AR turnover&lt;/strong&gt; and &lt;strong&gt;days sales outstanding (DSO)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Both look trivial. One division each. But there are a handful of places where a perfectly reasonable implementation gives a wrong answer, and nobody notices because the output still looks like a plausible number. The worst offender is feeding quarterly or monthly data into a formula written for annual data.&lt;/p&gt;

&lt;p&gt;This post covers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What AR turnover and DSO measure, in plain terms&lt;/li&gt;
&lt;li&gt;The formulas, with a worked example&lt;/li&gt;
&lt;li&gt;A small, tested JavaScript module&lt;/li&gt;
&lt;li&gt;The period-conversion bug, and how to avoid it&lt;/li&gt;
&lt;li&gt;Edge cases: zero balances, new businesses, seasonality&lt;/li&gt;
&lt;li&gt;Comparing two periods, and tying DSO into the cash conversion cycle&lt;/li&gt;
&lt;li&gt;A minimal UI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you'd like a live reference to check your numbers against as you go, here's a working AR turnover calculator:&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://takemyaccountingclassforme.com/tools/accounts-receivable-turnover-calculator" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Ftakemyaccountingclassforme.com%2Fimages%2Fog-accounting.jpg" height="400" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://takemyaccountingclassforme.com/tools/accounts-receivable-turnover-calculator" rel="noopener noreferrer" class="c-link"&gt;
            Accounts Receivable Turnover Calculator - Free AR Ratio Calculator | TakeMyAccountingClassForMe.com
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Free AR turnover calculator with days sales outstanding. Perfect for accounting students and financial analysis.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Ftakemyaccountingclassforme.com%2Fimages%2Ffavicon.png" width="128" height="128"&gt;
          takemyaccountingclassforme.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  The business problem in one paragraph
&lt;/h2&gt;

&lt;p&gt;When a company sells on credit, the sale shows up as revenue immediately, but the cash arrives later. Until it does, the amount owed sits on the balance sheet as accounts receivable. The faster customers pay, the sooner that money can be used for payroll, inventory, or growth. The slower they pay, the more the company is effectively lending money to its customers for free.&lt;/p&gt;

&lt;p&gt;AR turnover and DSO are two ways of describing the same thing: &lt;strong&gt;how quickly receivables become cash.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The formulas
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Accounts receivable turnover&lt;/strong&gt; says how many times, during a period, the company collects its average receivables balance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Average AR   = (Beginning AR + Ending AR) / 2
AR Turnover  = Net Credit Sales / Average AR
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Days sales outstanding&lt;/strong&gt; converts that into a number of days:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DSO = Days in period / AR Turnover
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;For an annual period, &lt;code&gt;Days in period&lt;/code&gt; is 365 (some teams use 360). So DSO is the average number of days it takes to collect payment after a sale.&lt;/p&gt;

&lt;p&gt;A few definitions worth pinning down before writing any code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Net credit sales&lt;/strong&gt; means sales made on credit, minus returns and allowances. Cash sales are excluded, because they never create a receivable. In practice many systems only expose total revenue, which overstates the numerator for businesses with meaningful cash sales. Know which one your data source gives you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Average AR&lt;/strong&gt; uses the beginning and ending balances for the period. We'll look at the weaknesses of that later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turnover is a per-period number.&lt;/strong&gt; Turnover of 12 on annual data means twelve times a year. Turnover of 3 on &lt;em&gt;quarterly&lt;/em&gt; data means three times &lt;em&gt;per quarter&lt;/em&gt;. Remember this, because it's where the bug lives.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  A worked example
&lt;/h3&gt;

&lt;p&gt;A retailer has annual net credit sales of $600,000. AR was $45,000 at the start of the year and $55,000 at the end.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Average AR: &lt;code&gt;(45,000 + 55,000) / 2 = 50,000&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;AR turnover: &lt;code&gt;600,000 / 50,000 = 12&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;DSO: &lt;code&gt;365 / 12 ≈ 30.4 days&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So on average this company collects about 30 days after the sale. If its payment terms are "net 30", that's right on target. If its terms are "net 15", customers are paying late.&lt;/p&gt;

&lt;p&gt;A second example: a manufacturer with $2,000,000 in annual net credit sales, beginning AR of $180,000, and ending AR of $220,000.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Average AR: &lt;code&gt;200,000&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;AR turnover: &lt;code&gt;2,000,000 / 200,000 = 10&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;DSO: &lt;code&gt;365 / 10 = 36.5 days&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Average AR as a percentage of sales: &lt;code&gt;10%&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The naive implementation
&lt;/h2&gt;

&lt;p&gt;Here's what most first drafts look like:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;naive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sales&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;beginAr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;endAr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;avgAr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;beginAr&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;endAr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;turnover&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sales&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;avgAr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dso&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;365&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;turnover&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;turnover&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dso&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It gives correct answers for the two examples above. It also has several problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It hard-codes 365.&lt;/strong&gt; Feed it quarterly sales and a quarterly average AR, and the DSO is wrong by a factor of four.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It divides by zero.&lt;/strong&gt; A brand-new business, or a cash-only business, has zero average AR. You get &lt;code&gt;Infinity&lt;/code&gt; or &lt;code&gt;NaN&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It trusts its inputs.&lt;/strong&gt; Negative numbers, strings, and &lt;code&gt;undefined&lt;/code&gt; all flow straight through.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It uses floats for money.&lt;/strong&gt; For ratios that's mostly fine, but summing or averaging currency values in floating point is a habit worth breaking early.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's fix each one.&lt;/p&gt;
&lt;h2&gt;
  
  
  The quarterly-data bug
&lt;/h2&gt;

&lt;p&gt;This is the one worth understanding properly, because it produces numbers that look believable.&lt;/p&gt;

&lt;p&gt;Suppose a company reports quarterly net credit sales of $150,000, with beginning AR of $45,000 and ending AR of $55,000.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Average AR: &lt;code&gt;50,000&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;AR turnover: &lt;code&gt;150,000 / 50,000 = 3&lt;/code&gt; (three turns &lt;strong&gt;per quarter&lt;/strong&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now the wrong way to compute DSO:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dsoWrong&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;365&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 121.7 days&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That says customers take four months to pay. But we're talking about a business with the same profile as the annual example, which collects in about 30 days.&lt;/p&gt;

&lt;p&gt;The right way: DSO is the number of days in the period divided by turnover &lt;em&gt;for that period&lt;/em&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;daysInQuarter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;365&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// 91.25&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dsoRight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;daysInQuarter&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 30.4 days&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Same company, same behavior, same DSO as the annual calculation. The fix is to make the period an explicit part of the function, not an implicit assumption.&lt;/p&gt;

&lt;p&gt;A related point: if you want to compare a quarterly turnover to an annual benchmark, &lt;strong&gt;annualize it&lt;/strong&gt; first by multiplying by the number of periods per year. Three turns per quarter is twelve turns per year.&lt;/p&gt;
&lt;h2&gt;
  
  
  The tested implementation
&lt;/h2&gt;

&lt;p&gt;Here's a version that handles periods, validation, and zero balances. Money is converted to integer cents at the boundary.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PERIODS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;annual&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;perYear&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;quarterly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;perYear&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;monthly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;perYear&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;toCents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; must be a non-negative number`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;arMetrics&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;netCreditSales&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;beginningAr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;endingAr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;period&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;annual&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;daysInYear&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;365&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;PERIODS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;period&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Unknown period: "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;period&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sales&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toCents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;netCreditSales&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Net credit sales&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;begin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toCents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;beginningAr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Beginning AR&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toCents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;endingAr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Ending AR&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;avgAr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;begin&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;daysInPeriod&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;daysInYear&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;perYear&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;avgAr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;avgAr&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;turnover&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;annualizedTurnover&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;dso&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;arPercentOfSales&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="c1"&gt;// Nothing meaningful to compute without both sales and receivables.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;avgAr&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;sales&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;turnover&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sales&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;avgAr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;turnover&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;turnover&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;annualizedTurnover&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;turnover&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;perYear&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dso&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;daysInPeriod&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;turnover&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arPercentOfSales&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;avgAr&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;sales&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A few design choices to point out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;daysInYear&lt;/code&gt; is a parameter.&lt;/strong&gt; Finance teams argue about 360 versus 365. Make it configurable instead of picking a side.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Both turnover and annualized turnover are returned.&lt;/strong&gt; The raw per-period value is what DSO needs. The annualized value is what you compare to benchmarks and to other periods.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero cases return &lt;code&gt;null&lt;/code&gt;, not &lt;code&gt;0&lt;/code&gt; or &lt;code&gt;Infinity&lt;/code&gt;.&lt;/strong&gt; A turnover of zero would mean something specific (no sales collected). Missing data and undefined ratios should be displayed as a dash, not as a number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validation throws.&lt;/strong&gt; A calculator that silently produces output from garbage is worse than one that complains.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Testing it
&lt;/h2&gt;

&lt;p&gt;Accounting formulas are perfect for tests, because you can check every expected value by hand. Node's built-in runner is enough:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ar.test.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;assert&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:assert/strict&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;arMetrics&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./ar.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;retail example&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;arMetrics&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;netCreditSales&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;beginningAr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;45000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;endingAr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;55000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;avgAr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;turnover&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dso&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;30.4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;manufacturing example&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;arMetrics&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;netCreditSales&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2000000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;beginningAr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;180000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;endingAr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;220000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;turnover&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dso&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;36.5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arPercentOfSales&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;quarterly data: DSO uses days in the quarter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;arMetrics&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;netCreditSales&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;150000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;beginningAr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;45000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;endingAr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;55000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;period&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;quarterly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;turnover&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;annualizedTurnover&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dso&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;30.4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zero average AR returns nulls&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;arMetrics&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;netCreditSales&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;beginningAr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;endingAr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;turnover&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dso&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zero sales returns nulls&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;arMetrics&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;netCreditSales&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;beginningAr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;endingAr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dso&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;negative input throws&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;throws&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;arMetrics&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;netCreditSales&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;beginningAr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;endingAr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="sr"&gt;/non-negative/&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown period throws&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;throws&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;arMetrics&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;netCreditSales&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;beginningAr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;endingAr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;period&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;weekly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="sr"&gt;/Unknown period/&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Run them with &lt;code&gt;node --test&lt;/code&gt;. The quarterly test is the important one. It encodes the bug from the previous section so nobody can reintroduce it later.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where "average of two numbers" falls apart
&lt;/h2&gt;

&lt;p&gt;Averaging the beginning and ending balances is the textbook method, and it's what most calculators do. It's also a rough approximation, and it has a classic failure mode: &lt;strong&gt;seasonality.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Consider a business with $600,000 in annual credit sales whose receivables look like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;January 1: $20,000 (a quiet period)&lt;/li&gt;
&lt;li&gt;December 31: $80,000 (a big holiday push)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The two-point average is &lt;code&gt;(20,000 + 80,000) / 2 = 50,000&lt;/code&gt;, giving turnover of 12. But suppose receivables were actually $15,000 for ten months and spiked to $95,000 only in November and December. The true average balance across the year would be much lower, and the true turnover would be higher than 12.&lt;/p&gt;

&lt;p&gt;If you have more data points, use them. Month-end balances give a better estimate than two endpoints:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;averageOfBalances&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;balances&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;balances&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Need at least one balance&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;balances&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;toCents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`Balance &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;cents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Then pass the result in as both the beginning and ending balance, or refactor &lt;code&gt;arMetrics&lt;/code&gt; to accept an &lt;code&gt;averageAr&lt;/code&gt; override. Either way, the principle is the same: &lt;strong&gt;the average is only as good as the points behind it.&lt;/strong&gt; For anything seasonal, show the user which method was used.&lt;/p&gt;

&lt;p&gt;Other common input problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;New businesses.&lt;/strong&gt; With no beginning balance, the average is just half the ending balance, which understates AR and inflates turnover. Better to use the ending balance only and flag that the figure is a one-point estimate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credit balances.&lt;/strong&gt; A customer who overpays creates a negative receivable. Our validator rejects negatives. In a real accounting system you may need to net them, so decide on a policy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Total revenue versus credit sales.&lt;/strong&gt; If the data source only has total revenue, turnover is overstated for any business with significant cash sales. Label the input clearly.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Interpreting the results
&lt;/h2&gt;

&lt;p&gt;A ratio on its own doesn't mean much. Is a turnover of 8 good? It depends heavily on the industry and the company's credit terms.&lt;/p&gt;

&lt;p&gt;Rules of thumb you'll see quoted: retailers that sell mostly to consumers often turn receivables quickly, while manufacturers, healthcare providers, and companies selling to large enterprises tend to collect more slowly. But the ranges vary a lot between sources, regions, and company sizes, so treat any benchmark table as a rough guide, not a standard.&lt;/p&gt;

&lt;p&gt;Instead of baking benchmarks into logic, keep them as editable data and show the user the range you're comparing against:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Illustrative ranges only. Replace with figures from a source you trust.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BENCHMARKS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;retail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;low&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;high&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;manufacturing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;low&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;high&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;wholesale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;low&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;high&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;healthcare&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;low&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;high&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;interpret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;annualizedTurnover&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;industry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;range&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;BENCHMARKS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;industry&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;range&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;annualizedTurnover&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;annualizedTurnover&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;low&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;below typical range&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;annualizedTurnover&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;high&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;above typical range&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;within typical range&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Notice that "above typical range" isn't automatically good news. A very high turnover can mean credit terms are so strict that the company is turning away customers who would have bought on credit. The honest interpretation is "unusually fast; check whether that's by design."&lt;/p&gt;

&lt;p&gt;The most useful comparison is usually not against an industry table at all. It's against &lt;strong&gt;the company's own credit terms and its own history.&lt;/strong&gt; If terms are net 30 and DSO is 52, that gap is the story, whatever the industry average says.&lt;/p&gt;
&lt;h2&gt;
  
  
  Comparing two periods
&lt;/h2&gt;

&lt;p&gt;Trends beat snapshots. A DSO of 38 days means little, but a DSO that climbed from 30 to 38 over two quarters is a warning sign.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;compareCycles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dso&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dso&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;dsoChangeDays&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dso&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dso&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;turnoverChange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;annualizedTurnover&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;annualizedTurnover&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;collectingFaster&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dso&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dso&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Use the annualized turnover so a quarterly period and an annual period can be compared on the same footing, and compare DSO in days, which is already period-independent.&lt;/p&gt;

&lt;p&gt;Example: same sales as the retail case, but AR grows from $55,000 to $65,000 over the following year.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Period A: average AR $50,000, turnover 12, DSO about 30.4 days&lt;/li&gt;
&lt;li&gt;Period B: average AR $60,000, turnover 10, DSO 36.5 days&lt;/li&gt;
&lt;li&gt;Change: DSO up by roughly 6 days, so customers are paying more slowly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sales didn't change at all, but receivables grew. That's a collections problem, and it would be invisible if you only looked at revenue.&lt;/p&gt;
&lt;h2&gt;
  
  
  Connecting DSO to the cash conversion cycle
&lt;/h2&gt;

&lt;p&gt;DSO is one third of a bigger metric, the &lt;strong&gt;cash conversion cycle (CCC)&lt;/strong&gt;, which measures how long cash is tied up between paying suppliers and collecting from customers:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CCC = DIO + DSO - DPO
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DIO&lt;/strong&gt; (days inventory outstanding): how long inventory sits before it's sold&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DSO&lt;/strong&gt;: how long customers take to pay&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DPO&lt;/strong&gt; (days payables outstanding): how long the company takes to pay its own suppliers
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;cashConversionCycle&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;dio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dso&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dpo&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;dio&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;dso&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;dpo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A company with 45 days of inventory, a DSO of 36.5, and 30 days of payables has a CCC of &lt;code&gt;45 + 36.5 - 30 = 51.5&lt;/code&gt; days. That's how long, on average, a dollar is tied up in operations before coming back as cash. Shorten DSO by ten days and you free up ten days of cash, without raising a single price or making a single sale. That's why finance teams care so much about this number.&lt;/p&gt;
&lt;h2&gt;
  
  
  A minimal UI
&lt;/h2&gt;

&lt;p&gt;Here's a small page that wires the module to a form. It's vanilla JS on purpose so the structure is easy to see.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"ar-form"&lt;/span&gt; &lt;span class="na"&gt;novalidate&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&amp;gt;&lt;/span&gt;Period
    &lt;span class="nt"&gt;&amp;lt;select&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"period"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"annual"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Annual&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"quarterly"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Quarterly&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"monthly"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Monthly&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/select&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&amp;gt;&lt;/span&gt;Net credit sales &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"sales"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"number"&lt;/span&gt; &lt;span class="na"&gt;min=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;step=&lt;/span&gt;&lt;span class="s"&gt;"0.01"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&amp;gt;&lt;/span&gt;Beginning AR &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"begin"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"number"&lt;/span&gt; &lt;span class="na"&gt;min=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;step=&lt;/span&gt;&lt;span class="s"&gt;"0.01"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&amp;gt;&lt;/span&gt;Ending AR &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"end"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"number"&lt;/span&gt; &lt;span class="na"&gt;min=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;step=&lt;/span&gt;&lt;span class="s"&gt;"0.01"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Calculate&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"out"&lt;/span&gt; &lt;span class="na"&gt;aria-live=&lt;/span&gt;&lt;span class="s"&gt;"polite"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Turnover: — | DSO: —&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;arMetrics&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./ar.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;—&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#ar-form&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;submit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#out&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;arMetrics&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;netCreditSales&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sales&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;beginningAr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;begin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;endingAr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;end&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;period&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;period&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="s2"&gt;`Turnover: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;turnover&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt; per &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;period&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt; period | `&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
        &lt;span class="s2"&gt;`DSO: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dso&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt; days | Avg AR: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;avgAr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLocaleString&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Some details worth keeping:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;aria-live="polite"&lt;/code&gt; so screen readers announce updated results.&lt;/li&gt;
&lt;li&gt;Labels wrap their inputs, so each field has an accessible name without extra markup.&lt;/li&gt;
&lt;li&gt;Results say "per quarterly period" or "per annual period" so the user isn't left guessing what the turnover number refers to. That ambiguity is exactly what causes the DSO bug in the first place.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to see how a fuller version feels, with currency options, period selection, an industry comparison, a side-by-side period comparison, and print or share options, the live tool is here:&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://takemyaccountingclassforme.com/tools/accounts-receivable-turnover-calculator" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Ftakemyaccountingclassforme.com%2Fimages%2Fog-accounting.jpg" height="400" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://takemyaccountingclassforme.com/tools/accounts-receivable-turnover-calculator" rel="noopener noreferrer" class="c-link"&gt;
            Accounts Receivable Turnover Calculator - Free AR Ratio Calculator | TakeMyAccountingClassForMe.com
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Free AR turnover calculator with days sales outstanding. Perfect for accounting students and financial analysis.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Ftakemyaccountingclassforme.com%2Fimages%2Ffavicon.png" width="128" height="128"&gt;
          takemyaccountingclassforme.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



&lt;h2&gt;
  
  
  Checklist before you ship a financial calculator
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Is the period explicit, and does DSO use days in &lt;em&gt;that&lt;/em&gt; period?&lt;/li&gt;
&lt;li&gt;[ ] Do zero and missing values show a dash instead of &lt;code&gt;0&lt;/code&gt; or &lt;code&gt;Infinity&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;[ ] Is it clear whether the input is credit sales or total revenue?&lt;/li&gt;
&lt;li&gt;[ ] Are negative and non-numeric inputs rejected with a readable message?&lt;/li&gt;
&lt;li&gt;[ ] Is the averaging method visible to the user, especially for seasonal businesses?&lt;/li&gt;
&lt;li&gt;[ ] Are benchmarks labeled as rough guides and kept as editable data?&lt;/li&gt;
&lt;li&gt;[ ] Do the tests encode hand-computed examples, including a non-annual period?&lt;/li&gt;
&lt;li&gt;[ ] Is the 360 versus 365 day-count choice configurable?&lt;/li&gt;
&lt;li&gt;[ ] Does the UI say that results are estimates for analysis, not audited figures?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;AR turnover and DSO are simple formulas wrapped around some surprisingly easy-to-miss assumptions. The ones that matter most:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Turnover is &lt;strong&gt;per period&lt;/strong&gt;. DSO must use &lt;strong&gt;days in the same period&lt;/strong&gt;, or it will be wrong by a constant factor and still look plausible.&lt;/li&gt;
&lt;li&gt;Annualize turnover before comparing across periods or against benchmarks.&lt;/li&gt;
&lt;li&gt;Return &lt;code&gt;null&lt;/code&gt; for undefined cases instead of &lt;code&gt;0&lt;/code&gt; or &lt;code&gt;Infinity&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The two-point average is an approximation. For seasonal businesses, use more data points or say clearly what you did.&lt;/li&gt;
&lt;li&gt;A ratio is only meaningful next to the company's own credit terms and trend.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've built something similar, I'd be curious what edge cases bit you. Did you handle partial payments, credit memos, or multi-currency receivables? Let me know in the comments.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>fintech</category>
    </item>
    <item>
      <title>Build a GPA Calculator in JavaScript (and Avoid the Floating-Point Bugs Everyone Hits)</title>
      <dc:creator>Alan Matthew</dc:creator>
      <pubDate>Sat, 17 Oct 2026 10:31:14 +0000</pubDate>
      <link>https://dev.to/alan-matthew/build-a-gpa-calculator-in-javascript-and-avoid-the-floating-point-bugs-everyone-hits-2c31</link>
      <guid>https://dev.to/alan-matthew/build-a-gpa-calculator-in-javascript-and-avoid-the-floating-point-bugs-everyone-hits-2c31</guid>
      <description>&lt;p&gt;A GPA calculator looks like a ten-minute project. You multiply, you add, you divide, and you're done.&lt;/p&gt;

&lt;p&gt;Then you test it against a real transcript and the number is off by 0.01. Or &lt;code&gt;3.3 * 3&lt;/code&gt; gives you &lt;code&gt;9.899999999999999&lt;/code&gt;. Or a student enters a course with zero credits and your page shows &lt;code&gt;NaN&lt;/code&gt;. Or someone asks, "What do I need next semester to reach a 3.5?" and you realize you have no idea how to answer that.&lt;/p&gt;

&lt;p&gt;This post walks through the whole thing properly: the math, a small and well-tested JavaScript implementation, the floating-point trap, input validation, and a target-GPA planner. By the end you'll have a module you can drop into any front end.&lt;/p&gt;

&lt;p&gt;If you want a live reference to compare your output against while you build, here's a working calculator:&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://takemyaccountingclassforme.com/tools/gpa-calculator" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Ftakemyaccountingclassforme.com%2Fimages%2Fog-accounting.jpg" height="400" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://takemyaccountingclassforme.com/tools/gpa-calculator" rel="noopener noreferrer" class="c-link"&gt;
            College GPA Calculator - Calculate Semester &amp;amp; Cumulative GPA (4.0 Scale)
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Calculate your semester and cumulative GPA instantly. Designed for accounting majors with tips to boost your grades.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Ftakemyaccountingclassforme.com%2Fimages%2Ffavicon.png" width="128" height="128"&gt;
          takemyaccountingclassforme.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Let's get into it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The math in one paragraph&lt;/li&gt;
&lt;li&gt;Modeling the data&lt;/li&gt;
&lt;li&gt;The naive implementation (and why it's subtly wrong)&lt;/li&gt;
&lt;li&gt;Fixing floating-point error with scaled integers&lt;/li&gt;
&lt;li&gt;Cumulative GPA&lt;/li&gt;
&lt;li&gt;A target-GPA planner&lt;/li&gt;
&lt;li&gt;Validation and edge cases&lt;/li&gt;
&lt;li&gt;Writing tests&lt;/li&gt;
&lt;li&gt;Wiring it up to a simple UI&lt;/li&gt;
&lt;li&gt;Things real transcripts will throw at you&lt;/li&gt;
&lt;li&gt;Wrap-up&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The math in one paragraph
&lt;/h2&gt;

&lt;p&gt;GPA is a &lt;strong&gt;credit-weighted average&lt;/strong&gt;. Each letter grade maps to grade points. You multiply grade points by the course's credit hours to get &lt;strong&gt;quality points&lt;/strong&gt;, add up quality points across courses, and divide by total credit hours:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GPA = Σ (grade points × credits) ÷ Σ credits
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That's the whole formula. A 4-credit course matters more than a 1-credit course because it contributes more quality points &lt;em&gt;and&lt;/em&gt; more credits to the denominator.&lt;/p&gt;

&lt;p&gt;The standard unweighted 4.0 college scale with plus/minus grades looks like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Grade&lt;/th&gt;
&lt;th&gt;Points&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;4.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A-&lt;/td&gt;
&lt;td&gt;3.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B+&lt;/td&gt;
&lt;td&gt;3.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;3.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B-&lt;/td&gt;
&lt;td&gt;2.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C+&lt;/td&gt;
&lt;td&gt;2.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C-&lt;/td&gt;
&lt;td&gt;1.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;F&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Schools differ. Some cap A+ at 4.0, some give 4.3, some have no plus/minus grades at all. We'll make the scale a configurable object so you never hard-code it into your logic.&lt;/p&gt;
&lt;h2&gt;
  
  
  Modeling the data
&lt;/h2&gt;

&lt;p&gt;Keep the data model boring. A course is just credits and a grade:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;course&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Intermediate Accounting I&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;credits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;grade&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A semester is an array of those. Cumulative history is two numbers: previous GPA and previous credits.&lt;/p&gt;

&lt;p&gt;Resist the urge to store the computed GPA on each course. Derive everything from the raw inputs, and you'll never have stale values to debug.&lt;/p&gt;
&lt;h2&gt;
  
  
  The naive implementation (and why it's subtly wrong)
&lt;/h2&gt;

&lt;p&gt;Here's the version most of us write first:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GRADE_POINTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;4.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;3.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;B+&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;3.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;B&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;B-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;2.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;C+&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;2.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;C&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;C-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;D&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;F&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;naiveGpa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;courses&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;quality&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;credits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;courses&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;quality&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;GRADE_POINTS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;grade&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;credits&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;credits&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;credits&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;quality&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;credits&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It looks right, and for a lot of inputs it is. But there are three problems hiding in it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem 1: floating-point error.&lt;/strong&gt; JavaScript numbers are IEEE 754 doubles. Decimal values like &lt;code&gt;3.3&lt;/code&gt; and &lt;code&gt;3.7&lt;/code&gt; can't be represented exactly in binary, so arithmetic on them accumulates tiny errors. Try this in your console:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;3.3&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// 9.899999999999999&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;3.7&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// 11.100000000000001&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;For a single GPA the error is far too small to show up in a two-decimal display. But it matters in two situations: when you compare results (&lt;code&gt;gpa &amp;gt;= 3.5&lt;/code&gt; when the true value is exactly 3.5), and when you round. A value that should be &lt;code&gt;3.495&lt;/code&gt; can end up as &lt;code&gt;3.4949999999999997&lt;/code&gt; and round the wrong way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem 2: no validation.&lt;/strong&gt; If &lt;code&gt;credits&lt;/code&gt; is &lt;code&gt;0&lt;/code&gt; or a string like &lt;code&gt;"3"&lt;/code&gt; or &lt;code&gt;undefined&lt;/code&gt;, you get &lt;code&gt;NaN&lt;/code&gt;, &lt;code&gt;Infinity&lt;/code&gt;, or silent string concatenation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem 3: unknown grades.&lt;/strong&gt; &lt;code&gt;GRADE_POINTS["A+"]&lt;/code&gt; is &lt;code&gt;undefined&lt;/code&gt;, and &lt;code&gt;undefined * 3&lt;/code&gt; is &lt;code&gt;NaN&lt;/code&gt;, which then poisons the whole sum.&lt;/p&gt;

&lt;p&gt;Let's fix all three.&lt;/p&gt;
&lt;h2&gt;
  
  
  Fixing floating-point error with scaled integers
&lt;/h2&gt;

&lt;p&gt;The cleanest fix is to avoid decimals in the arithmetic. Store grade points as integers in tenths (&lt;code&gt;37&lt;/code&gt; instead of &lt;code&gt;3.7&lt;/code&gt;), and convert credits to tenths too so half-credit courses work. Multiply integers, add integers, and divide once at the very end.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Grade points in tenths, so 3.7 is stored as 37.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GRADE_POINTS_TENTHS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;37&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;B+&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;B&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;B-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;C+&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;C&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;C-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;D&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;F&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Sum a semester into integer totals.
 * quality is scaled by 100 (grade tenths × credit tenths)
 * credits is scaled by 10 (credit tenths)
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;semesterTotals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;courses&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;GRADE_POINTS_TENTHS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;quality&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;credits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;course&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;courses&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;course&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;grade&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gp&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Unknown grade: "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;course&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;grade&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;creditTenths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;course&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;credits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;creditTenths&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;creditTenths&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Invalid credits for "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;course&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;course&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;grade&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;quality&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;gp&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;creditTenths&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;credits&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;creditTenths&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;credits&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/** Convert scaled totals back to a GPA. Returns null if there are no credits. */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;gpaFromTotals&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;credits&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;credits&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// quality = 100 × true quality points, credits = 10 × true credits&lt;/span&gt;
  &lt;span class="c1"&gt;// quality / credits = 10 × GPA, so divide by 10 once more.&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;quality&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;credits&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Why the scaling works: if a course is 3 credits with an A-, the integer math is &lt;code&gt;37 * 30 = 1110&lt;/code&gt;. The true quality points are &lt;code&gt;3.7 * 3 = 11.1&lt;/code&gt;, so &lt;code&gt;1110&lt;/code&gt; is exactly &lt;code&gt;11.1 × 100&lt;/code&gt;. No rounding error, ever, until the single division at the end.&lt;/p&gt;

&lt;p&gt;Add a small formatter for display:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;formatGpa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gpa&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;digits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;gpa&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;—&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;gpa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;digits&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A word of caution about &lt;code&gt;toFixed&lt;/code&gt;: it formats the underlying binary value, so &lt;code&gt;(1.005).toFixed(2)&lt;/code&gt; returns &lt;code&gt;"1.00"&lt;/code&gt;, not &lt;code&gt;"1.01"&lt;/code&gt;. For GPA this rarely bites because our final value comes from one integer division, but if exact "round half up" behavior matters to you, round on the scaled integers before dividing, or use a decimal library.&lt;/p&gt;
&lt;h2&gt;
  
  
  Cumulative GPA
&lt;/h2&gt;

&lt;p&gt;Cumulative GPA is the same weighted average, just with your history included:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cumulative = (prevGPA × prevCredits + semesterQualityPoints) ÷ (prevCredits + semesterCredits)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;You reconstruct previous quality points from the previous GPA and credits, then add the new semester. In code, using the same scaled units:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;cumulativeGpa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prevGpa&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prevCredits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;semester&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prevCreditTenths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prevCredits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prevQualityScaled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prevGpa&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prevCredits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;gpaFromTotals&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prevQualityScaled&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;semester&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;credits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prevCreditTenths&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;semester&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;credits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Here's a worked example. A student has a 3.42 GPA over 60 credits. This semester they take 15 credits and earn 51.0 quality points.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Previous quality points: &lt;code&gt;3.42 × 60 = 205.2&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;New total: &lt;code&gt;205.2 + 51.0 = 256.2&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;New total credits: &lt;code&gt;60 + 15 = 75&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Cumulative GPA: &lt;code&gt;256.2 ÷ 75 = 3.416&lt;/code&gt;, which displays as &lt;code&gt;3.42&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice how little it moved. With 60 credits already banked, one semester at roughly the same performance level barely shifts the average. The more credits you have, the more "inertia" the cumulative GPA has. That's exactly the kind of intuition worth surfacing in a UI.&lt;/p&gt;
&lt;h2&gt;
  
  
  A target-GPA planner
&lt;/h2&gt;

&lt;p&gt;This is the feature that turns a calculator into a planning tool. Instead of "what's my GPA?", the question becomes "what do I need this semester to reach X?"&lt;/p&gt;

&lt;p&gt;Rearranging the cumulative formula to solve for the semester GPA:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;neededSemesterGpa = (target × (prevCredits + semCredits) − prevGPA × prevCredits) ÷ semCredits
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;requiredSemesterGpa&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prevGpa&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prevCredits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;semesterCredits&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prevCredits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;semesterCredits&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;needed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prevGpa&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prevCredits&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;
    &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;semesterCredits&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;needed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;reachable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;needed&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mf"&gt;4.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;alreadyMet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;needed&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Example: current 3.42 over 60 credits, taking 15 more, target 3.50.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;requiredSemesterGpa&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;3.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;prevGpa&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;3.42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;prevCredits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;semesterCredits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// needed ≈ 3.82, reachable: true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Check the math: &lt;code&gt;(3.5 × 75 − 3.42 × 60) ÷ 15 = (262.5 − 205.2) ÷ 15 = 3.82&lt;/code&gt;. So the student needs about a 3.82 this term, which is demanding but possible.&lt;/p&gt;

&lt;p&gt;Now try a target of 3.8 with the same inputs: &lt;code&gt;(3.8 × 75 − 205.2) ÷ 15 = 5.32&lt;/code&gt;. That's above 4.0, so &lt;code&gt;reachable&lt;/code&gt; is &lt;code&gt;false&lt;/code&gt;. The honest message to show is "not reachable in one semester; here's how many semesters it would take." A planner that quietly tells someone to earn a 5.32 is worse than no planner.&lt;/p&gt;

&lt;p&gt;You can extend this to compute the minimum number of semesters by looping: assume a perfect 4.0 each term and count until the cumulative GPA crosses the target.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;semestersToTarget&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prevGpa&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prevCredits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;creditsPerSemester&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxSemesters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;quality&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prevGpa&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prevCredits&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;credits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prevCredits&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;maxSemesters&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;quality&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mf"&gt;4.0&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;creditsPerSemester&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;credits&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;creditsPerSemester&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;quality&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;credits&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// not reachable within maxSemesters&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Validation and edge cases
&lt;/h2&gt;

&lt;p&gt;Real users type real garbage. Decide up front what your calculator does in each case instead of discovering it in production.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Empty course row&lt;/td&gt;
&lt;td&gt;Ignore it, don't throw&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Credits of &lt;code&gt;0&lt;/code&gt; or negative&lt;/td&gt;
&lt;td&gt;Reject with a clear message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Credits as a string (&lt;code&gt;"3"&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Coerce with &lt;code&gt;Number()&lt;/code&gt;, then validate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Credits like &lt;code&gt;1.5&lt;/code&gt; or &lt;code&gt;0.5&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Allow (this is why we scale credits by 10)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grade not in the scale&lt;/td&gt;
&lt;td&gt;Reject, don't produce &lt;code&gt;NaN&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No valid courses at all&lt;/td&gt;
&lt;td&gt;Return &lt;code&gt;null&lt;/code&gt; and show a dash, not &lt;code&gt;0.00&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Previous GPA above 4.0&lt;/td&gt;
&lt;td&gt;Warn: the user may be on a different scale&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Negative previous credits&lt;/td&gt;
&lt;td&gt;Reject&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The "return &lt;code&gt;null&lt;/code&gt;, not &lt;code&gt;0&lt;/code&gt;" rule is worth underlining. A GPA of &lt;code&gt;0.00&lt;/code&gt; is a real, meaningful (and alarming) result. Showing it when the user simply hasn't entered anything yet is misleading. Use &lt;code&gt;null&lt;/code&gt; internally and a placeholder like &lt;code&gt;—&lt;/code&gt; in the UI.&lt;/p&gt;

&lt;p&gt;A small helper keeps the empty-row case tidy:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;cleanRows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;grade&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;credits&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Writing tests
&lt;/h2&gt;

&lt;p&gt;A calculator is a perfect candidate for tests, because every case has a known right answer you can check by hand. Node ships with a built-in test runner, so you don't need any dependencies:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// gpa.test.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;assert&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:assert/strict&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;semesterTotals&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;gpaFromTotals&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;cumulativeGpa&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;formatGpa&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./gpa.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;typical semester&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;courses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;credits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;grade&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;credits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;grade&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;B+&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;credits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;grade&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;B&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;credits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;grade&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;credits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;grade&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;totals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;semesterTotals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;courses&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// 11.1 + 9.9 + 9.0 + 12.0 + 4.0 = 46.0 quality points over 13 credits&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;formatGpa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;gpaFromTotals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;totals&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;3.54&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;floating point does not leak into results&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;totals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;semesterTotals&lt;/span&gt;&lt;span class="p"&gt;([{&lt;/span&gt; &lt;span class="na"&gt;credits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;grade&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;B+&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}]);&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;totals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;990&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// exactly 9.9 × 100&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;half-credit courses are supported&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;totals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;semesterTotals&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;credits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;grade&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;credits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;grade&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;B&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;totals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;credits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cumulative GPA&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;semester&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;semesterTotals&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;credits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;grade&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;credits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;grade&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;credits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;grade&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;B+&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;credits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;grade&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;B&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;credits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;grade&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;B-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt; &lt;span class="c1"&gt;// 15 credits, 51.0 quality points&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;formatGpa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;cumulativeGpa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;3.42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;semester&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;3.42&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown grade throws&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;throws&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;semesterTotals&lt;/span&gt;&lt;span class="p"&gt;([{&lt;/span&gt; &lt;span class="na"&gt;credits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;grade&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A+&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}]),&lt;/span&gt; &lt;span class="sr"&gt;/Unknown grade/&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zero credits throws&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;throws&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;semesterTotals&lt;/span&gt;&lt;span class="p"&gt;([{&lt;/span&gt; &lt;span class="na"&gt;credits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;grade&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}]),&lt;/span&gt; &lt;span class="sr"&gt;/Invalid credits/&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no courses returns null, not zero&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;gpaFromTotals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;semesterTotals&lt;/span&gt;&lt;span class="p"&gt;([])),&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Run it with:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="nt"&gt;--test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Two of these tests are worth copying into any numeric project. The "floating point does not leak" test pins down the exact integer result so a future refactor can't quietly reintroduce decimals. The "returns null, not zero" test locks in the empty-state behavior.&lt;/p&gt;
&lt;h2&gt;
  
  
  Wiring it up to a simple UI
&lt;/h2&gt;

&lt;p&gt;Here's a minimal vanilla HTML and JS page. It's deliberately plain so you can see the structure without a framework in the way.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"gpa-form"&lt;/span&gt; &lt;span class="na"&gt;novalidate&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;table&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;thead&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&amp;lt;th&amp;gt;&lt;/span&gt;Course&lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&amp;lt;th&amp;gt;&lt;/span&gt;Credits&lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&amp;lt;th&amp;gt;&lt;/span&gt;Grade&lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&amp;lt;th&amp;gt;&amp;lt;/th&amp;gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/thead&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;tbody&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"rows"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/tbody&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/table&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"add"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Add course&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Calculate GPA&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"result"&lt;/span&gt; &lt;span class="na"&gt;aria-live=&lt;/span&gt;&lt;span class="s"&gt;"polite"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Semester GPA: —&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;semesterTotals&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;gpaFromTotals&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;formatGpa&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./gpa.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;grades&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;B+&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;B&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;B-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;C+&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;C&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;C-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;D&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;F&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#rows&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;addRow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tr&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
      &amp;lt;td&amp;gt;&amp;lt;input name="name" aria-label="Course name"&amp;gt;&amp;lt;/td&amp;gt;
      &amp;lt;td&amp;gt;&amp;lt;input name="credits" type="number" min="0.5" step="0.5" aria-label="Credits"&amp;gt;&amp;lt;/td&amp;gt;
      &amp;lt;td&amp;gt;
        &amp;lt;select name="grade" aria-label="Grade"&amp;gt;
          &amp;lt;option value=""&amp;gt;Select&amp;lt;/option&amp;gt;
          &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;grades&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;option&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/option&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;
        &amp;lt;/select&amp;gt;
      &amp;lt;/td&amp;gt;
      &amp;lt;td&amp;gt;&amp;lt;button type="button" class="remove" aria-label="Remove course"&amp;gt;✕&amp;lt;/button&amp;gt;&amp;lt;/td&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;remove&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;closest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tr&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#add&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;addRow&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;addRow&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#gpa-form&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;submit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tr&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tr&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[name="name"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;credits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[name="credits"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;grade&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[name="grade"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}))&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;grade&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;credits&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#result&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gpa&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;gpaFromTotals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;semesterTotals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`Semester GPA: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;formatGpa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gpa&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A few small UX decisions in there are worth copying:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;aria-live="polite"&lt;/code&gt;&lt;/strong&gt; on the result so screen readers announce the new GPA when it changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;aria-label&lt;/code&gt;&lt;/strong&gt; on every input, since table cells don't give inputs a visible label.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;step="0.5"&lt;/code&gt;&lt;/strong&gt; on credits so half-credit courses are allowed but nonsense like &lt;code&gt;2.37&lt;/code&gt; is discouraged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;novalidate&lt;/code&gt; plus our own error handling&lt;/strong&gt;, so you control the message wording instead of relying on browser defaults.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filtering blank rows&lt;/strong&gt; instead of erroring, because four empty starter rows are the normal state of the form.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to see how a polished version of this feels, with add and remove rows, a reset button, a cumulative section, and print or save options, open the live tool from the embed above and compare it against what you've built.&lt;/p&gt;
&lt;h2&gt;
  
  
  Things real transcripts will throw at you
&lt;/h2&gt;

&lt;p&gt;The formula is easy. The real world is messy. Before you ship a calculator to actual students, think through these cases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repeated courses.&lt;/strong&gt; Schools handle retakes in three main ways: the new grade replaces the old one, both grades are averaged, or both count as separate attempts. Your data model should let a course carry a flag such as &lt;code&gt;replacedBy&lt;/code&gt; or &lt;code&gt;excluded&lt;/code&gt;, rather than assuming one policy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pass/fail and audit courses.&lt;/strong&gt; These usually carry credits but no grade points and don't enter the GPA at all. Model them as &lt;code&gt;countsTowardGpa: false&lt;/code&gt; instead of mapping "P" to a number.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Withdrawals.&lt;/strong&gt; A "W" generally has no GPA impact. Same flag, same treatment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transfer credits.&lt;/strong&gt; Transfer credits often count toward degree completion but not toward the institutional GPA. If you're accepting "previous credits" as an input, say clearly which credits the user should include.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A+ and other scale variations.&lt;/strong&gt; Some institutions treat A+ as 4.0, others as 4.3. A configurable scale object, like the one we used, handles this without touching your logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weighted GPAs.&lt;/strong&gt; High schools commonly weight AP or Honors courses on a 5.0 scale. That's a different calculation and a different scale. If you support both, keep them as separate modes so the labels, scales, and validation don't get tangled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rounding policy.&lt;/strong&gt; Some registrars truncate, some round, and some show three decimals. Make &lt;code&gt;digits&lt;/code&gt; a parameter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's an estimate, not the record.&lt;/strong&gt; However careful your code is, the official GPA is whatever the registrar says. Say so in the UI. A small "for planning purposes" note builds more trust than pretending to be authoritative.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where to go from here
&lt;/h2&gt;

&lt;p&gt;Once the core module is solid, there are some natural extensions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Persist entries&lt;/strong&gt; in &lt;code&gt;localStorage&lt;/code&gt; so students don't retype their courses every visit. Wrap reads and writes in &lt;code&gt;try/catch&lt;/code&gt;, since storage can be blocked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a "what-if" mode&lt;/strong&gt; where each grade is a dropdown and the GPA updates live as you change it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visualize the impact&lt;/strong&gt; of each course by showing its share of total quality points. It makes the credit-weighting idea concrete.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Export results&lt;/strong&gt; as a text file or a printable view.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Support multiple scales&lt;/strong&gt; (4.0, 4.3, percentage-based) behind a single interface.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a chart&lt;/strong&gt; of semester GPA over time, which is a very motivating view for students.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you build any of these, I'd love to hear how you structured it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;To recap the important parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GPA is a &lt;strong&gt;credit-weighted average&lt;/strong&gt;: quality points divided by credits.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;scaled integers&lt;/strong&gt; for the arithmetic and divide once at the end, so floating-point error never reaches your results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate inputs&lt;/strong&gt; and return &lt;code&gt;null&lt;/code&gt; instead of &lt;code&gt;0&lt;/code&gt; when there's nothing to calculate.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;target-GPA planner&lt;/strong&gt; is more useful than a plain calculator, as long as it's honest when a target isn't reachable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write tests&lt;/strong&gt; with hand-computed expectations. A calculator is the easiest thing in the world to test well.&lt;/li&gt;
&lt;li&gt;Real transcripts have &lt;strong&gt;retakes, pass/fail, withdrawals, and transfer credits&lt;/strong&gt;, so design your data model with flags rather than baking in one school's policy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you'd like to sanity-check your implementation against a working version, here's the live calculator again:&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://takemyaccountingclassforme.com/tools/gpa-calculator" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Ftakemyaccountingclassforme.com%2Fimages%2Fog-accounting.jpg" height="400" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://takemyaccountingclassforme.com/tools/gpa-calculator" rel="noopener noreferrer" class="c-link"&gt;
            College GPA Calculator - Calculate Semester &amp;amp; Cumulative GPA (4.0 Scale)
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Calculate your semester and cumulative GPA instantly. Designed for accounting majors with tips to boost your grades.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Ftakemyaccountingclassforme.com%2Fimages%2Ffavicon.png" width="128" height="128"&gt;
          takemyaccountingclassforme.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;Questions, edge cases I missed, or a different approach to the rounding problem? Drop them in the comments.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Truth Tables for Developers: Build One in Python (and Verify Boolean Logic Fast)</title>
      <dc:creator>Alan Matthew</dc:creator>
      <pubDate>Fri, 16 Oct 2026 11:49:46 +0000</pubDate>
      <link>https://dev.to/alan-matthew/truth-tables-for-developers-build-one-in-python-and-verify-boolean-logic-fast-pc</link>
      <guid>https://dev.to/alan-matthew/truth-tables-for-developers-build-one-in-python-and-verify-boolean-logic-fast-pc</guid>
      <description>&lt;p&gt;Boolean logic shows up everywhere: &lt;code&gt;if&lt;/code&gt; conditions, access-control rules, query filters, circuit design, SAT solvers. Truth tables are the simplest way to check that an expression does what you think it does.&lt;/p&gt;

&lt;p&gt;This post covers the core idea, the one operator that causes most mistakes, and a small Python script you can reuse.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a truth table is
&lt;/h2&gt;

&lt;p&gt;A truth table lists every possible True/False assignment for your variables and the result of your expression for each one. With &lt;code&gt;n&lt;/code&gt; variables you get &lt;code&gt;2^n&lt;/code&gt; rows, so 3 variables is 8 rows and 8 variables is 256. That growth is why you stop doing this by hand fairly quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The operators
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operator&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;False when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;A AND B&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;both must be true&lt;/td&gt;
&lt;td&gt;either is false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;A OR B&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;at least one true (inclusive)&lt;/td&gt;
&lt;td&gt;both false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NOT A&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;inverts&lt;/td&gt;
&lt;td&gt;A is true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;A XOR B&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;exactly one true&lt;/td&gt;
&lt;td&gt;both the same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;A -&amp;gt; B&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;material conditional&lt;/td&gt;
&lt;td&gt;A true &lt;strong&gt;and&lt;/strong&gt; B false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;A &amp;lt;-&amp;gt; B&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;biconditional&lt;/td&gt;
&lt;td&gt;values differ&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The one that trips everyone: implication
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;A -&amp;gt; B&lt;/code&gt; is false in exactly one case: A is true and B is false. If A is false, the whole thing is true regardless of B (vacuous truth).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;A&lt;/th&gt;
&lt;th&gt;B&lt;/th&gt;
&lt;th&gt;A → B&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;F&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;F&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;F&lt;/td&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;F&lt;/td&gt;
&lt;td&gt;F&lt;/td&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is not a quirk. It's what makes the contrapositive valid: &lt;code&gt;A -&amp;gt; B&lt;/code&gt; is equivalent to &lt;code&gt;NOT B -&amp;gt; NOT A&lt;/code&gt;. It's also why &lt;code&gt;A -&amp;gt; B&lt;/code&gt; is equivalent to &lt;code&gt;(NOT A) OR B&lt;/code&gt;, which is how you'd write it in most programming languages.&lt;/p&gt;

&lt;h2&gt;
  
  
  A truth table generator in Python
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;itertools&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;truth_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;product&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;repeat&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# fine for your own input, never for untrusted strings
&lt;/span&gt;        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="nf"&gt;truth_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(not a) or b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;a b | (not a) or b
1 1 | 1
1 0 | 0
0 1 | 1
0 0 | 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the implication table from above. Python has no implication operator, so &lt;code&gt;(not a) or b&lt;/code&gt; stands in for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking logical equivalence
&lt;/h2&gt;

&lt;p&gt;Two expressions are logically equivalent if they produce the same output for every assignment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;equivalent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expr1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expr2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;product&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;repeat&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expr1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nf"&gt;eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expr2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

&lt;span class="c1"&gt;# De Morgan's law
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;equivalent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not (a and b)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(not a) or (not b)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;  &lt;span class="c1"&gt;# True
&lt;/span&gt;
&lt;span class="c1"&gt;# Common mistake: not (a and b) is NOT the same as (not a) and (not b)
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;equivalent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not (a and b)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(not a) and (not b)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;  &lt;span class="c1"&gt;# False
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second check is a good test of whether you've internalized De Morgan's laws. Refactoring a condition by "distributing the not" and getting it wrong is a classic source of bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Identities worth knowing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;De Morgan:&lt;/strong&gt; &lt;code&gt;NOT (P AND Q) ≡ (NOT P) OR (NOT Q)&lt;/code&gt;, and &lt;code&gt;NOT (P OR Q) ≡ (NOT P) AND (NOT Q)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Material implication:&lt;/strong&gt; &lt;code&gt;P -&amp;gt; Q ≡ (NOT P) OR Q&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contrapositive:&lt;/strong&gt; &lt;code&gt;P -&amp;gt; Q ≡ NOT Q -&amp;gt; NOT P&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Double negation:&lt;/strong&gt; &lt;code&gt;NOT NOT P ≡ P&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Absorption:&lt;/strong&gt; &lt;code&gt;P OR (P AND Q) ≡ P&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Classifying expressions
&lt;/h2&gt;

&lt;p&gt;Once you can enumerate every row, you can classify any expression:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tautology:&lt;/strong&gt; true for every assignment (&lt;code&gt;P OR NOT P&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contradiction:&lt;/strong&gt; false for every assignment (&lt;code&gt;P AND NOT P&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contingency:&lt;/strong&gt; true for some, false for others&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Spotting a tautology or contradiction inside a real condition usually means dead code or a logic bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you'd rather not write the script
&lt;/h2&gt;

&lt;p&gt;I built a free browser-based generator that does this with a proper parser instead of &lt;code&gt;eval&lt;/code&gt;. It supports up to 8 variables, shows intermediate sub-expression columns, flags tautologies and contradictions, has an equivalence checker, and exports CSV. It runs entirely client-side, so nothing is sent to a server:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://paysomeonetotakeonlineclass.us/tools/truth-table-generator" rel="noopener noreferrer"&gt;Truth Table Generator&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: this tool is hosted on my own site.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you spot a precedence or parsing edge case, let me know in the comments. Operator precedence in the tool is NOT &amp;gt; AND &amp;gt; XOR &amp;gt; OR &amp;gt; IMPLIES &amp;gt; EQUIV.&lt;/p&gt;

</description>
      <category>computerscience</category>
      <category>beginners</category>
      <category>logic</category>
      <category>python</category>
    </item>
    <item>
      <title>How to Pass an Online Class When You're Behind, Overwhelmed, or Working Full-Time</title>
      <dc:creator>Alan Matthew</dc:creator>
      <pubDate>Wed, 14 Oct 2026 11:19:43 +0000</pubDate>
      <link>https://dev.to/alan-matthew/how-to-pass-an-online-class-when-youre-behind-overwhelmed-or-working-full-time-3aek</link>
      <guid>https://dev.to/alan-matthew/how-to-pass-an-online-class-when-youre-behind-overwhelmed-or-working-full-time-3aek</guid>
      <description>&lt;p&gt;Online classes feel flexible until you fall behind. Then the flexibility turns into a pile of unread modules, missed deadlines, and a discussion board you're afraid to open. If you're a self-taught dev finishing a degree, a bootcamp student, or a working professional taking a course on the side, this guide is for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why online classes are harder than they look
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No built-in structure.&lt;/strong&gt; There's no 9 a.m. lecture forcing you to show up. Every deadline depends on you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silence hides problems.&lt;/strong&gt; In a classroom, a confused face gets noticed. Online, you can drift for three weeks before anyone knows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything is text.&lt;/strong&gt; Instructions, feedback, and announcements are easy to skim and miss.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Isolation.&lt;/strong&gt; Without classmates around, motivation drops fast.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Falling behind is a design problem as much as a discipline problem. The fix is adding structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Triage in the first 30 minutes
&lt;/h2&gt;

&lt;p&gt;If you're already behind, don't start doing work yet. Spend half an hour building a picture:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the &lt;strong&gt;syllabus&lt;/strong&gt; and list every graded item with its due date and weight.&lt;/li&gt;
&lt;li&gt;Mark what's &lt;strong&gt;submitted, late-but-still-accepted, and missed entirely&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Calculate your current grade and the grade you need to pass.&lt;/li&gt;
&lt;li&gt;Identify the &lt;strong&gt;highest-weight items&lt;/strong&gt; remaining. Those get your best hours.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Often the situation is better than it feels. A missed 2% quiz matters far less than an unwritten 30% final project.&lt;/p&gt;

&lt;p&gt;Think of it as a backlog grooming session: sort by impact, not by guilt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Talk to your instructor early
&lt;/h2&gt;

&lt;p&gt;Most students wait too long to email. Instructors can grant extensions, accept late work, drop a lowest grade, or point you to resources, but only if you ask before it's a crisis.&lt;/p&gt;

&lt;p&gt;A short, honest email works:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Subject:&lt;/strong&gt; Request for guidance, [Course Name], [Your Name]&lt;/p&gt;

&lt;p&gt;Hi Professor [Name], I've fallen behind in [course] because [one-sentence reason, e.g. a work schedule change]. I've reviewed the syllabus and want to catch up on [specific items]. Would it be possible to [extension / late submission / meeting] for [assignment]? I can send my plan for the next two weeks if that helps.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Be specific, take responsibility without over-apologising, and offer a plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Build a weekly system
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Block time like a job.&lt;/strong&gt; Put 2-3 recurring study blocks on your calendar and treat them as appointments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use a simple weekly rhythm:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Day&lt;/th&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Monday&lt;/td&gt;
&lt;td&gt;Skim all of the week's modules and list deadlines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Midweek&lt;/td&gt;
&lt;td&gt;Do the main readings and graded work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Thursday&lt;/td&gt;
&lt;td&gt;Draft assignments and post early discussion contributions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Friday/Saturday&lt;/td&gt;
&lt;td&gt;Revise, submit, and preview next week&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Work backward from deadlines.&lt;/strong&gt; A paper due Sunday needs an outline by Wednesday and a draft by Friday.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Submit early when you can.&lt;/strong&gt; Platform glitches, internet outages, and upload errors happen on deadline day. Buffer time prevents disasters, the same way it does for a release.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Study techniques that work online
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Active recall:&lt;/strong&gt; after a lecture, close the tab and write down what you remember. Then check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spaced repetition:&lt;/strong&gt; review material at increasing intervals using flashcards or an app like Anki.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Teach it back:&lt;/strong&gt; explain the concept aloud as if to a friend (or a rubber duck). If you stumble, you've found a gap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Practice problems first&lt;/strong&gt; for maths, statistics, accounting, and finance. Reading solutions feels like learning but isn't. Attempt before you look.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chunk your sessions:&lt;/strong&gt; 25-50 minutes of focus, then a short break.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 5: Writing assignments and discussion posts
&lt;/h2&gt;

&lt;p&gt;Discussion boards and short essays eat time. Make them manageable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Answer the prompt directly in your first sentence.&lt;/li&gt;
&lt;li&gt;Use a simple structure: claim, evidence, explanation, link to course material.&lt;/li&gt;
&lt;li&gt;Cite the readings. Instructors notice.&lt;/li&gt;
&lt;li&gt;For replies to classmates, add something new: a counter-example, a question, a connection to another reading.&lt;/li&gt;
&lt;li&gt;Keep a running notes document for the course so you aren't starting from scratch every week.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 6: Use the support you've probably already paid for
&lt;/h2&gt;

&lt;p&gt;Most students don't realise their tuition includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tutoring centres&lt;/strong&gt;, often virtual, with maths, statistics, writing, and accounting specialists&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writing centres&lt;/strong&gt; that review drafts and teach structure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Library research help&lt;/strong&gt; and subject librarians&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accessibility services&lt;/strong&gt;, if you need accommodations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Academic advisors&lt;/strong&gt; who can discuss withdrawing, incompletes, or course load&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Counselling and mental health services&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These work well because they teach you rather than finish tasks for you, which is what you need on the exam.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Protect your integrity and your degree
&lt;/h2&gt;

&lt;p&gt;When you're desperate, shortcuts look tempting: someone logging in to do your quizzes, writing your discussion posts, or "taking" the course for you. Don't. Beyond the ethics, the practical risks are serious:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Platforms track IP addresses, login patterns, writing style, and proctoring data.&lt;/li&gt;
&lt;li&gt;Penalties can include a failing grade, suspension, expulsion, or a permanent note on your record.&lt;/li&gt;
&lt;li&gt;You may still face a proctored exam you can't pass, since the knowledge isn't yours.&lt;/li&gt;
&lt;li&gt;Anyone doing your work holds your credentials and could use them against you.&lt;/li&gt;
&lt;li&gt;Promises like "guaranteed A" can't be honoured by an honest provider.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Legitimate help improves your own work: tutoring, feedback on drafts, and explanations of concepts. Doing the graded work for you is not legitimate help.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8: If you really can't recover the semester
&lt;/h2&gt;

&lt;p&gt;Sometimes stepping back is the right call, and doing it strategically beats failing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Withdraw before the deadline&lt;/strong&gt; to avoid a failing grade (check refund and financial aid implications first).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Request an incomplete&lt;/strong&gt; if you have documented circumstances.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retake the course&lt;/strong&gt; with a lighter load.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Switch format&lt;/strong&gt;, such as an eight-week course or a hybrid class.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Talk to your advisor and financial aid office before deciding. A withdrawal is recoverable, but misconduct often isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Syllabus reviewed, deadlines listed&lt;/li&gt;
&lt;li&gt;[ ] Current grade and target grade calculated&lt;/li&gt;
&lt;li&gt;[ ] Instructor emailed with a specific plan&lt;/li&gt;
&lt;li&gt;[ ] Study blocks scheduled&lt;/li&gt;
&lt;li&gt;[ ] Support services contacted&lt;/li&gt;
&lt;li&gt;[ ] Work submitted in your own words, with help declared if required&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;Most students who struggle in &lt;a href="https://paidonlineclasshelp.com/" rel="noopener noreferrer"&gt;online classes&lt;/a&gt; don't lack ability. They lack structure, early communication, and the right support. Fix those and the course becomes manageable.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What's worked for you when you've fallen behind in a course? Share it in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>learning</category>
      <category>career</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Structuring a Service Page for Answer Engines: 5 Patterns That Work</title>
      <dc:creator>Alan Matthew</dc:creator>
      <pubDate>Tue, 13 Oct 2026 12:43:47 +0000</pubDate>
      <link>https://dev.to/alan-matthew/structuring-a-service-page-for-answer-engines-5-patterns-that-work-9cd</link>
      <guid>https://dev.to/alan-matthew/structuring-a-service-page-for-answer-engines-5-patterns-that-work-9cd</guid>
      <description>&lt;p&gt;Search is shifting from "ten blue links" to answers generated by LLM-backed engines. If you build marketing sites, the question isn't just "will this rank?" but "will this get quoted?" Here are five structural patterns I keep seeing on pages that get extracted cleanly.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Question headings, answer-first paragraphs
&lt;/h2&gt;

&lt;p&gt;Make each H2 a real question, then put a self-contained 40-60 word answer directly under it, before any nuance.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;How much does X cost in Doha?&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"direct-answer"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Basic plans start at $12 per unit, standard plans at $38, and
  premium plans at $58. Every quote is fixed in writing before payment.
&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Extractors favor passages that make sense out of context. Don't write "as mentioned above" in these blocks.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Make the FAQ mirror the headings
&lt;/h2&gt;

&lt;p&gt;If your H2s are questions, your FAQ should reuse the same wording with the same answers. It reinforces one canonical answer per question instead of five slightly different ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Use real tables for comparisons
&lt;/h2&gt;

&lt;p&gt;Tables are easy for machines to parse and for users to scan. Keep them to a few columns with consistent units.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;table&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;caption&amp;gt;&lt;/span&gt;Citation styles by discipline&lt;span class="nt"&gt;&amp;lt;/caption&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;thead&amp;gt;&amp;lt;tr&amp;gt;&amp;lt;th&amp;gt;&lt;/span&gt;Style&lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&amp;lt;th&amp;gt;&lt;/span&gt;Typical fields&lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&amp;lt;th&amp;gt;&lt;/span&gt;In-text format&lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&amp;lt;/tr&amp;gt;&amp;lt;/thead&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;tbody&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;&lt;/span&gt;APA 7&lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;&lt;/span&gt;Psychology, education&lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;&lt;/span&gt;(Author, 2024)&lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;&lt;/span&gt;IEEE&lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;&lt;/span&gt;Engineering, CS&lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;&lt;/span&gt;[1]&lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/tbody&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/table&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Declare location and language explicitly
&lt;/h2&gt;

&lt;p&gt;For local businesses, say where you operate in the markup as well as the copy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"geo.region"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"QA"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"geo.placename"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"Doha, Qatar"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"ICBM"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"25.2854, 51.5310"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;property=&lt;/span&gt;&lt;span class="s"&gt;"og:locale"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"en_QA"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"canonical"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://example.com/"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Be realistic about these. Google has said it doesn't use &lt;code&gt;geo.*&lt;/code&gt; meta tags for ranking, so they're a low-cost hint for other consumers, not a lever. Your real local signals are your Business Profile, consistent NAP data, and &lt;code&gt;LocalBusiness&lt;/code&gt; structured data.&lt;/p&gt;

&lt;p&gt;If you serve bilingual audiences, add &lt;code&gt;hreflang&lt;/code&gt; and a proper Arabic (&lt;code&gt;dir="rtl"&lt;/code&gt;) version rather than a single translated line.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Control your snippets
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"robots"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"index, follow, max-snippet:-1, max-image-preview:large"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lets engines show longer excerpts. Decide deliberately whether you want that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What not to do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Don't stuff Q&amp;amp;A formatting onto thin content. Structure amplifies substance but can't replace it.&lt;/li&gt;
&lt;li&gt;Don't claim things in copy you can't back up. Engines and users both cross-check.&lt;/li&gt;
&lt;li&gt;Do validate JSON-LD with the Rich Results Test. Structured data is the part most often broken.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] H2s are questions; each is followed by an answer-first paragraph&lt;/li&gt;
&lt;li&gt;[ ] FAQ wording matches headings&lt;/li&gt;
&lt;li&gt;[ ] Comparisons are real &lt;code&gt;&amp;lt;table&amp;gt;&lt;/code&gt;s&lt;/li&gt;
&lt;li&gt;[ ] Canonical, locale, and hreflang are correct&lt;/li&gt;
&lt;li&gt;[ ] JSON-LD validates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://dissertationwritingservices.qa/" rel="noopener noreferrer"&gt;Dissertation Writing Help Tool&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What patterns have you seen get pages cited by AI answers? Curious what's worked for you.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>ai</category>
      <category>html</category>
    </item>
  </channel>
</rss>
