直觀地瞭解自己每天在吃上面的開銷

Liutos發表於2021-10-07

眾所周知,我用Emacsledger-mode來記賬(參見以前的文章《程式設計師的記賬工具——ledger與ledger-mode》)。作為一個出色的命令列報表工具,ledger的命令balanceregister足以涵蓋大部分的使用場景:

  • balance可以生成所有帳號的餘額的報表,用於每天與各個賬戶中的真實餘額進行比較;
  • register可以生成給定帳號的交易明細,用於在餘額不一致時與真實賬戶的流水一條條核對;

美中不足的是,ledger的報表不夠直觀,因為它們是冷冰冰的文字資訊,而不是振奮人心的統計圖形。好在,正如ledger不儲存資料,而只是一份份.ledger檔案中的交易記錄的搬運工一樣,gnuplot也是這樣的工具——它不儲存資料,它只負責將儲存在文字檔案的資料以圖形的形態呈現出來。

如何運用gnuplot

gnuplot是很容易使用的。以最簡單的情況為例,首先將如下內容儲存到檔案/tmp/data.csv

-1 -1
0 0
1 1

然後在命令列中啟動gnuplot,進入它的 REPL 中,並執行如下命令

plot "/tmp/data.csv"

即可得到這三組資料的展示

三組資料分別是座標為(-1, -1)(0, 0),以及(1, 1)的點。

因此要讓gnuplot繪製開銷的圖形,首先就是從賬本中提取出要繪製的資料,再決定如何用gnuplot繪製即可。

ledger提取開銷記錄

儘管ledger的子命令register可以列印出給定帳號的交易明細,但此處更適合使用csv子命令。例如,下列的命令可以將最早的10條、吃的方面的支出記錄,都以 CSV 格式列印出來

➜  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條記錄為例,用如下的命令可以將它們按天聚合在一起

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 對其進行處理;

結果如下

➜  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將它們畫出來

➜  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中將資料按所處的是第幾周進行聚合

➜  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的指令碼

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年,那麼先分別統計兩年的開支到不同的檔案中

➜  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

再將處於同一周的資料合併在一起

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

最後,再讓gnuplot一次性繪製兩條折線

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"

結果如下

後記

其實仍然是非常不直觀的,因為最終生成的是一張靜態的圖片,並不能做到將滑鼠挪到曲線上時就給出所在位置的縱座標的效果。

閱讀原文

相關文章