无需数学 (2023)
Math Not Required (2023)

原始链接: https://programmersstone.blog/posts/math-not-required/

## 数学对程序员来说是必需的吗?信用卡示例 本文探讨了强大的数学基础是否是编程的*必需*条件,认为它并非绝对必要,但肯定很有价值。它通过编写信用卡还款模型来证明这一点。 该示例展示了如何使用编程来模拟现实世界的金融场景——特别是复利对信用卡债务的巨大影响。代码计算每日利息、最低还款额和还款时间表。虽然所使用的公式(例如将年利率转换为日利率)*可以*在网上找到,但核心逻辑更多地依赖于数据结构和算法实现,而不是高级数学概念。 该示例生动地说明了小的改变——例如增加最低还款额或增加少量超额还款——如何显着减少债务和还款时间。这突出了编程的力量,即使没有深厚的数学专业知识,也能提供见解。 最终,文章建议,虽然复习基础代数和几何是有益的,但程序员可以在没有广泛数学知识的情况下有效地工作,并且编程本身可以成为理解和应对数学复杂情况的工具。

相关文章

原文

I want to be very clear: this article is examining whether or not math is essential background knowledge for programmers. It is not arguing that math is not important or valuable.

I believe that most people can benefit from learning some math! You are absolutely affected by math in your day-to-day life, whether you understand it or not.

For example, many adults have a credit card. Credit cards typically come with an Annual Percentage Rate (APR), which is pretty confusing since most cards calculate daily compound interest. Compound interest could be considered the eighth wonder of the modern world and, when you compound it daily, it's simply terrifying. It's vital to understand at least the magnitude of how this grows if you carry a significant balance.

However, if you don't know this, programming might be able to save you! Let's build a model for credit cards:

  defmodule CreditCard do
    defstruct balance: 7_279.0,
              apr: 24.45,
              daily_periodic_rate: nil,
              minimum_percent: 0.02,
              minimum_floor: 27.5,
              last_due: nil

    def new(fields \\ []) do
      fields
      |> Keyword.put_new_lazy(:last_due, &Date.utc_today/0)
      |> then(&struct!(__MODULE__, &1))
      |> then(fn card ->
        %__MODULE__{card | daily_periodic_rate: card.apr / 100 / 365}
      end)
    end

    def payment_schedule(card, overpayment \\ 0) do
      Stream.unfold(card, fn
        %__MODULE__{balance: 0.0} ->
          nil

        card ->
          payment =
            CreditCard.Payment.new(card)
            |> CreditCard.Payment.advance_due_date()
            |> CreditCard.Payment.accrue_interest()
            |> CreditCard.Payment.apply_payment(overpayment)

          {payment, payment.card_after}
      end)
    end
  end

This code above is mostly just a data structure. The defaults you see came from Google:

The CreditCard.payment_schedule/2 function provides a public interface for repeatedly making payments, until the balance reaches zero. A payment involves a few steps, but the keys bits are that interest is accrued for days past and then a payment is applied to the new total. A minimum payment is always made, but we can choose to pass an overpayment amount.

The trickiest line in the code above is probably where I convert the APR to a daily_periodic_rate. Is this using math knowledge? I don't think it has to be, since the formulas needed are easily Googled. (Yes, I've made some simplifying assumptions: assume the full balance is already subject to interest, ignore details like cash advances to assume the whole balance is under the same APR, and assume no new debt will be added.)

Let's look at the payments:

  defmodule CreditCard.Payment do
    defstruct ~w[card_before card_after days interest minimum paid]a

    def new(card), do: %__MODULE__{card_before: card, card_after: card}

    def advance_due_date(payment) do
      # try to jump a month forward
      last_due = payment.card_before.last_due
      next_due = Date.add(last_due, Date.days_in_month(last_due))

      # correct if we overshot a shorter month (not possible in November or
      # December, since both December and January have 31 days)
      next_due =
        if next_due.month == last_due.month + 2 do
          Date.add(next_due, -next_due.day)
        else
          next_due
        end

      days = Date.diff(next_due, last_due)

      %__MODULE__{
        payment
        | card_after: %CreditCard{payment.card_after | last_due: next_due},
          days: days
      }
    end

    def accrue_interest(payment) do
      card = payment.card_after
      interest = card.balance * card.daily_periodic_rate * payment.days
      new_balance = card.balance + interest

      %__MODULE__{
        payment
        | card_after: %CreditCard{payment.card_after | balance: new_balance},
          interest: interest
      }
    end

    def apply_payment(payment, overpayment \\ 0) do
      card = payment.card_after

      minimum =
        card.balance
        |> min(card.minimum_floor)
        |> max(card.balance * card.minimum_percent)

      paid = min(card.balance, minimum + overpayment)
      new_balance = card.balance - paid

      %__MODULE__{
        payment
        | card_after: %CreditCard{payment.card_after | balance: new_balance},
          minimum: minimum,
          paid: paid
      }
    end
  end

Another data structure and three public functions as the interface for manipulating it. The longest function finds the next payment date, because calendaring might be the only subject more complex credit cards. The interest calculation is right out of the formulas page I linked to above. Finally the actual payment function has to sort out whether we're paying the minimum percentage, using the minimum floor, or just paying the full remaining balance, depending on how much is left on the card.

That's enough for us to see how long it'll take to pay off an average debt if we stick to minimum payments:

  payments =
    CreditCard.new()
    |> CreditCard.payment_schedule()
    |> Enum.map(& &1.paid)

  %{
    months: length(payments),
    total_paid: payments |> Enum.sum() |> Float.round(2)
  }
  |> IO.inspect()
%{months: 87414, total_paid: 6257806.8}

Over 7,000 years and $6,000,000? You would be forgiven for assuming that I have a bug! But do I?

Let's examine the first five payments:

  CreditCard.new()
  |> CreditCard.payment_schedule()
  |> Enum.take(5)
  |> IO.inspect()
[
  %CreditCard.Payment{
    days: 31,
    interest: 151.1539191780822,
    card_after: %CreditCard{
      balance: 7281.550840794521,
      apr: 24.45,
      daily_periodic_rate: 6.698630136986302e-4,
      minimum_percent: 0.02,
      minimum_floor: 27.5,
      last_due: ~D[2023-11-16]
    },
    card_before: %CreditCard{
      balance: 7279.0,
      apr: 24.45,
      daily_periodic_rate: 6.698630136986302e-4,
      minimum_percent: 0.02,
      minimum_floor: 27.5,
      last_due: ~D[2023-10-16]
    },
    minimum: 148.60307838356164,
    paid: 148.60307838356164
  },
  %CreditCard.Payment{
    days: 30,
    interest: 146.32924771843236,
    card_after: %CreditCard{
      balance: 7279.322486742694,
      apr: 24.45,
      daily_periodic_rate: 6.698630136986302e-4,
      minimum_percent: 0.02,
      minimum_floor: 27.5,
      last_due: ~D[2023-12-16]
    },
    card_before: %CreditCard{
      balance: 7281.550840794521,
      apr: 24.45,
      daily_periodic_rate: 6.698630136986302e-4,
      minimum_percent: 0.02,
      minimum_floor: 27.5,
      last_due: ~D[2023-11-16]
    },
    minimum: 148.55760177025905,
    paid: 148.55760177025905
  },
  %CreditCard.Payment{
    days: 31,
    interest: 151.1606158582637,
    card_after: %CreditCard{
      balance: 7281.873440548939,
      apr: 24.45,
      daily_periodic_rate: 6.698630136986302e-4,
      minimum_percent: 0.02,
      minimum_floor: 27.5,
      last_due: ~D[2024-01-16]
    },
    card_before: %CreditCard{
      balance: 7279.322486742694,
      apr: 24.45,
      daily_periodic_rate: 6.698630136986302e-4,
      minimum_percent: 0.02,
      minimum_floor: 27.5,
      last_due: ~D[2023-12-16]
    },
    minimum: 148.60966205201916,
    paid: 148.60966205201916
  },
  %CreditCard.Payment{
    days: 31,
    interest: 151.21358833600186,
    card_after: %CreditCard{
      balance: 7284.425288307241,
      apr: 24.45,
      daily_periodic_rate: 6.698630136986302e-4,
      minimum_percent: 0.02,
      minimum_floor: 27.5,
      last_due: ~D[2024-02-16]
    },
    card_before: %CreditCard{
      balance: 7281.873440548939,
      apr: 24.45,
      daily_periodic_rate: 6.698630136986302e-4,
      minimum_percent: 0.02,
      minimum_floor: 27.5,
      last_due: ~D[2024-01-16]
    },
    minimum: 148.6617405776988,
    paid: 148.6617405776988
  },
  %CreditCard.Payment{
    days: 29,
    interest: 141.50744522395203,
    card_after: %CreditCard{
      balance: 7277.41407886057,
      apr: 24.45,
      daily_periodic_rate: 6.698630136986302e-4,
      minimum_percent: 0.02,
      minimum_floor: 27.5,
      last_due: ~D[2024-03-16]
    },
    card_before: %CreditCard{
      balance: 7284.425288307241,
      apr: 24.45,
      daily_periodic_rate: 6.698630136986302e-4,
      minimum_percent: 0.02,
      minimum_floor: 27.5,
      last_due: ~D[2024-02-16]
    },
    minimum: 148.51865467062387,
    paid: 148.51865467062387
  }
]

If you look closely at the first payment, you can see that the interest is a little more than what we're required to pay. Our balance actually went up by a couple of bucks!

The second payment was better, because it happened in a month that's one day shorter. It cost us less interest. After making both payments we're about 32 cents above where we started. So what saves us from a forever climbing balance?

February. Check out that fifth payment. Unfortunately it landed in a leap year, so it still has one more day than normal. However, it's enough of a win to undo the damage of the previous four payments and leave us over a buck and a half to the good.

This cycle repeats for a very long time, with each new February gaining a tiny bit of ground. Eventually we reach a point where all payments are more than the interest and a payoff can be reached. Even that is slow going though.

When you're in this situation, little changes can have a huge impact. Look at the differences we see if we just up the minimum payment percentage to the higher end of average:

  payments =
    CreditCard.new(minimum_percent: 0.03)
    |> CreditCard.payment_schedule()
    |> Enum.map(& &1.paid)

  %{
    months: length(payments),
    total_paid: payments |> Enum.sum() |> Float.round(2)
  }
  |> IO.inspect()
%{months: 258, total_paid: 20606.42}

We're still paying for over 20 years and it'll cost nearly three times the amount charged, but this seems like a miracle compared to the previous outcome.

What if we tossed in an extra $10 per month?

  card = CreditCard.new(minimum_percent: 0.03)

  overpayments =
    card
    |> CreditCard.payment_schedule(10)
    |> Enum.map(& &1.paid)

  overpayments_total = overpayments |> Enum.sum() |> Float.round(2)

  minimums_total =
    card
    |> CreditCard.payment_schedule()
    |> Enum.map(& &1.paid)
    |> Enum.sum()

  %{
    months: length(overpayments),
    total_paid: overpayments_total,
    savings: Float.round(minimums_total - overpayments_total, 2)
  }
  |> IO.inspect()
%{months: 178, total_paid: 17481.32, savings: 3125.1}

That saves us over six years and $3,000 even with the increased outlay of cash each month.

Hopefully you get the idea. I think math holds a lot of value to humans in general, regardless of where we land on the programming issue. Just refreshing algebra and geometry can do a lot for you, if it's been a while. In a world where typical credit card debt turns into a life long reduction of income, this stuff matters!

联系我们 contact @ memedata.com