#912 贷款计算公式的推导

2023-10-17

等额本息

设:

  • 总金额 A
  • 月利率 R
  • 贷款期限 N
  • 每月还款 X

那么:

第 1 期剩余金额:$Q_1 = A(1+R)-X$
第 2 期剩余金额:$Q_2 = Q_1(1+R)-X$
。。。
第 n 期剩余金额:$Q_n = Q_{n-1}(1+R)-X$

推导一下:

$$
\begin{aligned}
Q_2 &= Q_1(1+R)-X \
&= (A(1+R)-X)(1+R)-X \
&= A(1+R)^2-X(1+R)-X \
&= A(1+R)^2-X(1+(1+R))
\end{aligned}
$$

$$
\begin{aligned}
Q_3 &= Q_2(1+R)-X \
&= (A(1+R)^2-X(1+(1+R)))(1+R)-X \
&= A(1+R)^3-X(1+(1+R))(1+R)-X \
&= A(1+R)^3-X(1+R)^2-X(1+R)-X \
&= A(1+R)^3-X(1+(1+R)+(1+R)^2)
\end{aligned}
$$

$$
\begin{aligned}
Q_k &= A(1+R)^k-X(1+(1+R)+ \cdots +(1+R)^{k-1})
\end{aligned}
$$

根据等比数列公式 $ S_n=\frac {a_1(1-q^n)} {1-q} $(q 不等于 1):

$$
\begin{aligned}
1+(1+R)+ \cdots +(1+R)^{k-1} &= \frac {1 \times (1-(1+R)^k)} {1-(1+R)} \
&= \frac {1-(1+R)^k} {1-(1+R)}
\end{aligned}
$$

代入上面的推导方程中:

$$
\begin{aligned}
Q_k &= A(1+R)^k-X \cdot (1+(1+R)+ \cdots +(1+R)^{k-1}) \
&= A(1+R)^k - X \cdot \frac {1-(1+R)^k} {1-(1+R)}
\end{aligned}
$$

k = N 的时候:

$$
\begin{aligned}
Q_n &= A(1+R)^N - X \cdot \frac {1-(1+R)^N} {1-(1+R)} = 0 \
A(1+R)^N &= X \cdot \frac {1-(1+R)^N} {1-(1+R)} \
X &= \frac {A(1+R)^N} {\frac {1-(1+R)^N} {1-(1+R)}} \
&= \frac {A(1+R)^N} {\frac {(1+R)^N-1} {R}} \
&= \frac {AR(1+R)^N} {(1+R)^N-1}
\end{aligned}
$$

也就是:

$$
\begin{aligned}
每期还款金额 = 贷款金额 \times 月利率 \times \frac {(1+月利率)^{还款期数}} {(1+月利率)^{还款期数}-1}
\end{aligned}
$$

等额本金

设:

  • 总金额 A
  • 月利率 R
  • 贷款期限 N

每月还款计算:

$$
\begin{aligned}
X_1 &= {\frac A N} + A \cdot R \
X_2 &= {\frac A N} + (A - ({\frac A N})) \cdot R \
X_3 &= {\frac A N} + (A - ({\frac A N}) \cdot 2) \cdot R \
X_N &= {\frac A N} + (A - ({\frac A N}) \cdot (N - 1)) \cdot R
\end{aligned}
$$

也可以视作:

$$
\begin{aligned}
X_1 &= {\frac A N} + {\frac N N} \cdot A \cdot R \
X_2 &= {\frac A N} + {\frac {N - 1} N} \cdot A \cdot R \
X_3 &= {\frac A N} + {\frac {N - 2} N} \cdot A \cdot R \
X_{N-1} &= {\frac A N} + {\frac 2 N} \cdot A \cdot R \
X_N &= {\frac A N} + {\frac 1 N} \cdot A \cdot R
\end{aligned}
$$

总还款金额:

$$
\begin{aligned}
T &= A + A \cdot R \cdot {\frac {(1 + 2 + \cdots + N)} N} & 总还款金额\
&= A + A \cdot R \cdot ({\frac {N + 1} {2}}) \
I &= A \cdot R \cdot ({\frac {N + 1} {2}}) & 总利息 \
\end{aligned}
$$

#911 WSL git 太慢

2023-10-15

使用 WSL 的时候 git 用得简直令人难受,每次切换环境(Windows 和 WSL),都需要 Refresh Index,一次就得二十几秒。

最后只好搞一个别名:

# 5.15.90.1-microsoft-standard-WSL2
if [[ "`uname -r`" == *WSL2 ]]; then
    SHMODE=wsl
fi

if [[ $SHMODE == "wsl" ]]; then
    alias git=git.exe
fi

这是我的本地环境中截出来得一点,其实还做了一些其他得适配,总算能够接受了。

原因

暂时没有读到相关资料。
WSL 已经出来很久了,可是这些性能问题微软还是没有解决,可能是他们不想解决。

参考资料与拓展阅读

#910 Python Popen 卡住

2023-09-27

程序中有些时候无法避免需要执行系统命令完成一些任务。
比如,我们系统有一个内部的小功能以来 rsync 来做文件同步。

最近发现如果数据量稍微大一点,rsync 就会卡住,不知道为什么。

经过排查之后,发现是 Popen 模块的使用错误导致。

准备环境

mkdir /tmp/aa /tmp/bb
# Create 10K ~ 1000k Files
for i in {00001..10000}; do
    file_size=$((1 + $RANDOM % 100))
    dd if=/dev/urandom of=/tmp/aa/file$i.txt bs=10K count=$file_size
done
# du -sh /tmp/aa/
# 4.9G    /tmp/aa/

代码

from subprocess import PIPE, STDOUT, Popen

src_dir = '/tmp/aa/'
tgt_dir = '/tmp/bb/'

# --remove-source-files
command = 'rsync -av %s %s' % (src_dir, tgt_dir)
p = Popen(command, stdin=PIPE, stdout=PIPE, stderr=STDOUT, shell=True)
p.wait()
if p.returncode == 0:
    LOG.info('rsync success')
else:
    LOG.warning('rsync error %d', p.returncode)

数据传输卡在 file0670.txt 了,总传输数据 2.3G。

排查

经过排查之后,确认是我们的编码问题。
代码捕获了标准输出和标准错误,但是我们没有去读这个数据,最后把管道缓冲区占满了,程序就无法继续运行。

Popen 初始化有一个参数 pipesize,如果设置了,则会调用 fcntl.fcntl(p2cwrite, fcntl.F_SETPIPE_SZ, self.pipesize) 设置缓冲区大小。
在 man fcntl 中了解到:

Changing the capacity of a pipe
    F_SETPIPE_SZ (int; since Linux 2.6.35)
            Change  the capacity of the pipe referred to by fd to be at least arg bytes.  An unprivileged process can adjust the pipe capacity to any value between the system page size and the limit defined in /proc/sys/fs/pipe-max-size (see proc(5)).  Attempts to set the pipe capacity below the page size are silently rounded up to the page  size.   Attempts  by  an  unprivileged  process  to  set  the  pipe  capacity  above  the  limit  in /proc/sys/fs/pipe-max-size yield the error EPERM; a privileged process (CAP_SYS_RESOURCE) can override the limit.

            When  allocating  the  buffer for the pipe, the kernel may use a capacity larger than arg, if that is convenient for the implementation.  (In the current implementation, the allocation is the next higher power-of-two page-size multiple of the requested size.)  The actual capacity (in bytes) that is set is returned as the function result.

            Attempting to set the pipe capacity smaller than the amount of buffer space currently used to store data produces the error EBUSY.

            Note that because of the way the pages of the pipe buffer are employed when data is written to the pipe, the number of bytes that can be written may be less than the nominal size, depending on the size of the writes.

    F_GETPIPE_SZ (void; since Linux 2.6.35)
            Return (as the function result) the capacity of the pipe referred to by fd.

又在 man 7 pipe | grep size -C10 中了解到:

Pipe capacity
    A pipe has a limited capacity.  If the pipe is full, then a write(2) will block or fail, depending on whether the O_NONBLOCK flag is set (see below).  Different implementations have different limits for the  pipe  capacity.
    Applications should not rely on a particular capacity: an application should be designed so that a reading process consumes data as soon as it is available, so that a writing process does not remain blocked.

    In Linux versions before 2.6.11, the capacity of a pipe was the same as the system page size (e.g., 4096 bytes on i386).  Since Linux 2.6.11, the pipe capacity is 16 pages (i.e., 65,536 bytes in a system with a page size of 4096 bytes).  Since Linux 2.6.35, the default pipe capacity is 16 pages, but the capacity can be queried and set using the fcntl(2) F_GETPIPE_SZ and F_SETPIPE_SZ operations.  See fcntl(2) for more information.

    The following ioctl(2) operation, which can be applied to a file descriptor that refers to either end of a pipe, places a count of the number of unread bytes in the pipe in the int buffer pointed to by the final argument of the call:

        ioctl(fd, FIONREAD, &nbytes);

    The FIONREAD operation is not specified in any standard, but is provided on many implementations.

也就是说:

  1. 非特权进程可以调整管道缓冲区大小,范围是:页大小到 /proc/sys/fs/pipe-max-size
  2. 低于页大小,这会被当作是页大小
  3. 超过 pipe-max-size 则会报错 EPERM
  4. 特权进程不受限制
  5. 管道缓冲区大小
  6. 2.6.11 以前,系统页大小
  7. 2.6.11 之后,系统页大小 x 16
  8. 2.6.35 之后,可以通过 fcntl 来手动调整
  9. 如果管道缓冲区满了,则会写阻塞,除非程序设置了非阻塞运行模式(O_NONBLOCK

查看当前系统的页大小

getconf PAGE_SIZE
4096
getconf PAGESIZE
4096

验证

系统的缓冲区大小应该是 16 x 4K = 64K

import subprocess

cmd = ['python', '-c', 'print("a" * 1024 * 64, end=".")']
# cmd = ['python', '-c', 'import time; time.sleep(10);']
print(1)
p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
print(2)
# stdout, stderr = p.communicate()
# print(repr([stdout, stderr]))
print(3)
p.wait()

子进程执行 python 命令,输出 64KB 不会卡住,增加 1B 就会卡在 wait 那个地方。
解除 communicate 那一行的注释,程序能正常运行。

调整

子程序使用系统的 stdin/stdout/stderr

Popen(command, shell=True)

重定向到 DEVNULL

Popen(command, stdout=subprocess.DEVNULL, shell=True)

# python2 不支持 DEVNULL
devnull = os.open(os.devnull, os.O_RDWR)
Popen(command, stdout=devnull, shell=True)
devnull.close()

读子程序的 stdout

p = Popen(command, stdout=PIPE, stderr=STDOUT, shell=True)
# 程序阻塞着,不停从子进程标准输出读数据
p.communicate()

使用 run

result = subprocess.run(command, shell=True)
print(result.returncode)
print(result.stdout)
print(result.stderr)

#909 耕耘与收获

2023-09-15

今天的科技爱好者周刊上分享了美国风险投资家纳瓦尔在访谈中说的一些话:

"新人企业家的常见错误,就是认为结果是可预测的。如果我长期努力工作,就应该会得到某种成果。"

"实际上,你的成果是不可预测的。你工作多么努力并不重要,重要的是你在做什么、与谁一起工作、以及在哪里工作。 "

"你每天都会看到,那些赚最多钱的人,工作时间根本不长。而淘金者和商铺老板,努力工作一天,赚不到什么钱。 "

"重要的不在于你的努力程度,而在于仔细选择工作、人员和项目。"

"真正有效的工作方式,不是铁人三项或马拉松,比拼谁坚持的时间长,而是短跑,当机会来临的时候冲刺,平时注意健康和休息。"

"你要像狮子一样,看到猎物一跃而起,而不要牛一样,从早到晚劳作。"

给我们的启示:

现实世界是非线性的,也就是说耕耘不一定可以换来收获,我们需要摆脱线性的思维。

这期科技爱好者后面的另一个言论其实也有这样的意思,对开发者的针对性更强:

不要在疲劳的时候写代码。敬业和专业精神,更多地体现在你的纪律性,而不是体现在投入的时间。
-- Robert C. Martin(鲍勃大叔),《Clean Coder》的作者

#908 放弃大公司带来的收入

2023-09-05

来自《科技爱好者周刊(第 269 期):为什么英雄不使用炸药》的观点:

本文摘自 37signals 公司的《重来 3》(电子工业出版社,2020)一书。

绝大多数软件公司,比如我们,都难以抵抗大客户的诱惑。这是因为绝大多数企业软件是按人头收费的。
举例来说,你把软件卖给一家 7 人的小公司,每人收费 10 美元,那么这家公司每月为你带来 70 美元的收入。
但是,如果你卖给一家 120 人的公司,每月就有 1200 美元的收入。卖给 1200 人或 1.2 万人的公司,对收入的提升就更不用说了。
这就是大公司的诱惑,真的会让软件公司上瘾。

但是,我们从第一天起就决定,拒绝这种按人头收费的商业模式。这不是因为我们不喜欢钱,而是因为我们更加喜欢自由!
它的问题在于,它会让你依赖大客户,丢失对产品的控制力,决定了你要把时间花在谁身上。一旦采用这种模式,就不可能避开这些压力,唯一的办法就是放弃该模式。
所以,我们采用了截然相反的做法。今天,我们的软件价格是每月 99 美元,不限制人数。你的公司有 5 个人、50 个人、500 个人、还是 5000 个人,价格全都一样。你只需每月花费 99 美元,想付更多也不行。

乍一看,我们的做法简直毫无道理。为什么放着眼前的钱不拿呢,白白让最大的客户占便宜。要知道,就算让他们付 10 倍、甚至 100 倍的价格,对他们也不是大问题。
但是,我们有自己的理由:

  1. 既然没有哪个客户付得比较多,那也就没谁对功能、修改或例外情况有特别的发言权。
    这就让我们可以自由地开发自己想要的软件。当你心里没有恐惧,不担心服务不好少数几个超级大客户时,做正确的事就会容易得多。
  2. 我们希望自己的产品,为像我们一样的小企业服务。我们自己就是小企业,我们希望帮到他们。这是实实在在的机会,让我们发挥实实在在的影响力。
  3. 追求大合同必然会带来很多销售成本,比如客户关系、销售会议等等。我们非常厌恶这些事情,不愿陷进去。一旦你想从大客户那儿挣到大钱,这些东西就无可避免,不想碰它们就只有不做。

你可能会说,为什么不两种生意都做呢?一种业务模式面向小企业,同时另一组人专门服务大客户。问题是,我们不想成为一个拥有两套企业文化的公司。小企业销售和大企业销售,这是两种很不一样的业务,需要两种很不一样的员工。

放弃大公司带来的收益,换取更加专注于自己产品。这个观点让我受到震撼。

  • 如果专注于普通客户,企业的压力不会那么大。
  • 服务大客户的思路和服务普通客户的思路会造就不同的企业文化。

这个思路真的是挺好。

#906 一点工作心得

2023-08-15
  1. 戒躁,克制反驳别人的想法
    一则,别人与我看法相左的言辞中,一般也有他一点道理在
    二则,情绪受激之时说话总是容易偏颇,不如平心静气地思考思考
    三则,和为贵
  2. 沟通的时候多想想对方的立场
    由于身处的位置不同,看问题的角度不同,对同一件事情的看法有很大差异
  3. 三人行必有我师
    大家身上都有值得我学习的点,多学习别人的优点,也要学习别人看问题的角度
  4. 将压力分摊出去
    了解同事 + 信任同事
    注意:及时跟踪进度
  5. 合理安排工作
    分清轻重缓急,不要遗漏
  6. 适应快速切换
    应该我更喜欢专心地做一件事,但实际生活中,工作中,几乎不可能如此,总是不停地有各种事情插入
    需要学会在各个任务之间快速切换

#904 数学:小圆沿着大圆滚动

2023-07-22

阮一峰博客上看到这么一个数学题(原文是 maths.org 2014 年的一篇文章):

已知:小圆的半径为 $r$,大圆的半径为 $4r$
:小圆自身滚动了几周?

#903 转载:成年期的快与慢

2023-07-21

我们生活在这样一个社会:小孩子像成年人一样老成,而成年人像小孩子一样幼稚

现在的孩子们比以前更容易接触到成年人的世界,因此他们更早成人化。

从很小的年龄起,他们就在视频网站观看暴力战争,在社交网络上看到性感和暴露的照片和视频。

然而,当孩子们成年以后,他们往往无法实现经济独立,也没有机会承担足够的责任

结果,整个社会的文化就变得很幼稚,成年人感到无法做出承诺,即使承诺了也缺乏信心,对以后的生活感到难以把握。

他们的行事方式和处事态度,就像还在青少年时期。