乍听之下,不无道理;仔细揣摩,胡说八道

0%

直观地了解自己每天在吃上面的开销

众所周知,我用Emacsledger-mode来记账(参见以前的文章《程序员的记账工具——ledger与ledger-mode》)。作为一个出色的命令行报表工具,ledger的命令balanceregister足以涵盖大部分的使用场景:

  • balance可以生成所有帐号的余额的报表,用于每天与各个账户中的真实余额进行比较;
  • register可以生成给定帐号的交易明细,用于在余额不一致时与真实账户的流水一条条核对;

美中不足的是,ledger的报表不够直观,因为它们是冷冰冰的文字信息,而不是振奋人心的统计图形。好在,正如ledger不存储数据,而只是一份份.ledger文件中的交易记录的搬运工一样,gnuplot也是这样的工具——它不存储数据,它只负责将存储在文本文件的数据以图形的形态呈现出来。

如何运用gnuplot

gnuplot是很容易使用的。以最简单的情况为例,首先将如下内容保存到文件/tmp/data.csv

1
2
3
-1 -1
0 0
1 1

然后在命令行中启动gnuplot,进入它的 REPL 中,并执行如下命令

1
plot "/tmp/data.csv"

即可得到这三组数据的展示

三组数据分别是坐标为(-1, -1)(0, 0),以及(1, 1)的点。

因此要让gnuplot绘制开销的图形,首先就是从账本中提取出要绘制的数据,再决定如何用gnuplot绘制即可。

ledger提取开销记录

尽管ledger的子命令register可以打印出给定帐号的交易明细,但此处更适合使用csv子命令。例如,下列的命令可以将最早的10条、吃的方面的支出记录,都以 CSV 格式打印出来

1
2
3
4
5
6
7
8
9
10
11
➜  Accounting ledger --anon --head 10 -f 2021.ledger csv 'Expense:Food'
"2019/09/10","","32034acc","efe2a5b9:c720f278:58a3cd91:0dc07b7b","A","20","",""
"2019/09/11","","a61b6164","5d45e249:fe84ca06:778d1855:daf61ede","A","5","",""
"2019/09/11","","674ec19f","5d018df1:ebf020db:29d43aba:d0c84127","A","15","",""
"2019/09/11","","e55ff018","370ca545:7d3aa2d0:86f5f330:1379261b","A","20","",""
"2019/09/12","","f6aa675c","08315491:4c8f1ee7:5eeaddf3:f879914e","A","10.5","",""
"2019/09/12","","139b790f","a137e4ee:9bc8ee49:7d7ccd8b:472d6007","A","23.9","",""
"2019/09/12","","b24b716d","de348971:5364622c:b2144d94:01e74ff3","A","148","",""
"2019/09/13","","e7c066fa","b418a3b2:a3e21e87:a32ee8ac:8716a847","A","3","",""
"2019/09/13","","9eb044fe","702a13e9:3de7f1bd:9b20a278:1d20668d","A","24","",""
"2019/09/13","","ba301270","d2b7eeb3:381f9473:54f86a33:391a8662","A","36","",""

--anon选项可以将交易明细中的敏感信息(如收款方、帐号)等匿名处理。

尽管ledger打印出的内容有很多列,但只有第一列的日期,以及第六列的金额是我所需要的。同时,由于一天中可能会有多次吃的方面的开销,因此同一天的交易也会有多笔,在绘图之前,需要将同一天之中的开销累加起来,只留下一个数字。这两个需求,都可以用csvsql来满足。

csvsql聚合数据

以前文中的10条记录为例,用如下的命令可以将它们按天聚合在一起

1
ledger --anon --head 10 -f 2021.ledger csv 'Expense:Food' | csvsql -H --query 'SELECT `a`, SUM(`f`) FROM `expense` GROUP BY `a` ORDER BY `a` ASC' --tables 'expense'

其中:

  • 选项-Hcsvsql知道从管道中输入的数据没有标题行。后续处理时,csvsql会默认使用abc等作为列名;
  • 选项--query用于提交要执行的 SQL 语句;
  • 选项--tables用于指定表的名字,这样在--query中才能用 SQL 对其进行处理;

结果如下

1
2
3
4
5
6
➜  Accounting ledger --anon --head 10 -f 2021.ledger csv 'Expense:Food' | csvsql -H --query 'SELECT `a`, SUM(`f`) FROM `expense` GROUP BY `a` ORDER BY `a` ASC' --tables 'expense'
a,SUM(`f`)
2019-09-10,20
2019-09-11,40
2019-09-12,182.4
2019-09-13,63

gnuplot读取数据并绘图

用重定向将csvsql的输出结果保存到文件/tmp/data.csv中,然后就可以用gnuplot将它们画出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
➜  Accounting ledger --anon --head 10 -f 2021.ledger csv 'Expense:Food' | csvsql -H --query 'SELECT `a`, SUM(`f`) FROM `expense` GROUP BY `a` ORDER BY `a` ASC' --tables 'expense' | tail -n '+2' > /tmp/data.csv
➜ Accounting cat /tmp/plot_expense.gplot
set format x '%y-%m-%d'
set style data boxes
set terminal png font '/System/Library/Fonts/Hiragino Sans GB.ttc'
set title '吃的开销'
set output '/tmp/xyz.png'
set timefmt '%Y-%m-%d'
set xdata time
set xlabel '日期'
set xrange ['2019-09-10':'2019-09-13']
set ylabel '金额(¥)'
set yrange [0:200]
set datafile separator comma
plot '/tmp/data.csv' using 1:2
➜ Accounting gnuplot /tmp/plot_expense.gplot

生成的图片文件/tmp/xyz.png如下

在脚本文件/tmp/plot_expense.gplot中用到的命令都可以通过gnuplot在线手册查阅到:

  • set format命令用于设置坐标轴的刻度的格式。set format x "%y-%m-%d"意味着设置 X 轴的刻度为形如19-09-10的格式;
  • set style data命令设置数据的绘制风格。set style data box表示采用空心柱状图;
  • set terminal命令用于告诉gnuplot该生成什么样的输出。set terminal png font '/System/Library/Fonts/Hiragino Sans GB.ttc'表示输出结果为 PNG 格式的图片,并且采用给定的字体;
  • set title命令控制输出结果顶部中间位置的标题文案;
  • set output命令用于将原本输出到屏幕上的内容重定向到文件中;
  • set timefmt命令用于指定输入的日期时间数据的格式。set timefmt '%Y-%m-%d'意味着输入的日期时间数据的为形如2019-09-10的格式;
  • set xdata命令控制gnuplot如何理解属于 X 轴的数据。set xdata time表示 X 轴上的均为时间型数据;
  • set xlabel命令控制 X 轴的含义的文案。set ylabel与其类似,只是作用在 Y 轴上;
  • set xrange命令控制gnuplot所绘制的图形中 X 轴上的展示范围;
  • set datafile separator命令控制gnuplot读取数据文件时各列间的分隔符,comma表示分隔符为逗号。

想要按周统计怎么办

假设我要查看的是2021年每一周在吃的方面的总开支,那么需要在csvsql中将数据按所处的是第几周进行聚合

1
2
3
4
5
6
7
8
9
10
11
12
➜  Accounting ledger -b '2021-01-01' -f 2021.ledger csv 'Expense:Food' | csvsql -H --query 'SELECT strftime("%W", `a`) AS `week`, SUM(`f`) FROM `expense` GROUP BY `week` ORDER BY `a` ASC' --tables 'expense' | tail -n '+2' > /tmp/expense_dow.csv
➜ Accounting head /tmp/expense_dow.csv
00,633.6
01,437.3
02,337.5
03,428.4
04,191.5
05,330.4
06,154.6
07,621.4
08,485.6
09,375.73

同时也需要调整gnuplot的脚本

1
2
3
4
5
6
7
8
9
set terminal png font '/System/Library/Fonts/Hiragino Sans GB.ttc'
set title '吃的开销'
set output '/tmp/xyz2.png'
set xlabel '第几周'
set xrange [0:54]
set ylabel '金额(¥)'
set yrange [0:1000]
set datafile separator comma
plot '/tmp/expense_dow.csv' using 1:2 with lines

结果如下

想要同时查看两年的图形怎么办

gnuplot支持同时绘制多条曲线,只要使用数据文件中不同的列作为纵坐标即可。假设我要对比的是2020年和2021年,那么先分别统计两年的开支到不同的文件中

1
2
➜  Accounting ledger -b '2020-01-01' -e '2021-01-01' -f 2021.ledger csv 'Expense:Food' | csvsql -H --query 'SELECT strftime("%W", `a`) AS `week`, SUM(`f`) FROM `expense` GROUP BY `week` ORDER BY `a` ASC' --tables 'expense' | tail -n '+2' > /tmp/expense_2020.csv
➜ Accounting ledger -b '2021-01-01' -f 2021.ledger csv 'Expense:Food' | csvsql -H --query 'SELECT strftime("%W", `a`) AS `week`, SUM(`f`) FROM `expense` GROUP BY `week` ORDER BY `a` ASC' --tables 'expense' | tail -n '+2' > /tmp/expense_2021.csv

再将处于同一周的数据合并在一起

1
➜  Accounting csvjoin -H -c a /tmp/expense_2020.csv /tmp/expense_2021.csv | tail -n '+2' > /tmp/expense_2years.csv

最后,再让gnuplot一次性绘制两条折线

1
2
3
4
5
6
7
8
9
set terminal png font '/System/Library/Fonts/Hiragino Sans GB.ttc'
set title '吃的开销'
set output '/tmp/xyz2years.png'
set xlabel '第几周'
set xrange [0:54]
set ylabel '金额(¥)'
set yrange [0:1000]
set datafile separator comma
plot '/tmp/expense_2years.csv' using 1:2 with lines title "2020", '/tmp/expense_2years.csv' using 1:3 with lines title "2021"

结果如下

后记

其实仍然是非常不直观的,因为最终生成的是一张静态的图片,并不能做到将鼠标挪到曲线上时就给出所在位置的纵坐标的效果。

Liutos wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!
你的一点心意,我的十分动力。