Đi du học có “lời” không?

Chắc chắn đây là câu hỏi của rất nhiều người, và hôm nay xin phép dùng dữ liệu thu chi và ít vốn liếng data science của mình trong những năm qua để trả lời. Bài viết này sử dụng file data (.csv) của Money Lover , một ứng dụng tuyệt vời mà mình đã sử dụng từ khi sang Nhật để lưu giữ nhật ký thu nhập và chi tiêu hằng tháng. Các bạn sẽ biết cách vẽ một số biểu đồ stacked bar (cột chồng) để xem balance, cũng như income của mình ra sao và làm treemap (là biểu đồ cây hoặc biểu đồ nhiệt) để xem mình chi cái gì nhiều nhất. Chúng ta sẽ code bằng Python.

Dành cho bạn nào chưa biết thì Money Lover là app của người Việt, quản lý tài chính cá nhân cực kỳ xuất sắc và có giao diện đẹp. Ngoài ra, cái mình thích là app có thể xuất dữ liệu sang dạng .csv để người dùng tự xử lý nữa. Đây là app mà mình nghĩ ai cũng nên sở hữu.

Data

File csv nên xử lý quá đơn giản. Các bạn nào dùng app bằng tiếng Việt thì chú ý để encoding là utf-16 nhé, không thì file csv đọc ra sẽ không ổn. Mặc dù có các ví khác, nhưng vì thu nhập/ chi tiêu cá nhân của mình chỉ có 2 ví là “Bank” và “Cash” nên mình lọc dữ liệu của riêng 2 ví này. Để tránh phân mảnh thì các bạn nên đổi định dạng sang datetimesort theo datetime.

import pandas as pd 
import matplotlib.pyplot as plt 
import numpy as np
data = pd.DataFrame(pd.read_csv('path/file.csv', encoding='utf-16'))
#to datetime 
data['Date'] = pd.to_datetime(data['Date'])
data = data[data['Wallet'].isin(['Bank Japan','Cash Japan'])]
#sort by date
data_sorted = data.sort_values(by=['Date'], ascending=True).reset_index(drop=True)

Hành trang lên đường của mình ban đầu là 40 man nhé. So sánh tiền bây giờ với con số này là biết được ngay đi du học tốn kém như nào và có lời hay không.

Các library mà chúng ta sử dụng. Ngoài các library quen thuộc ra thì để vẽ treemap, chúng ta sẽ cần squarify.

import pandas as pd
import maplotlib.pyplot as plt
import numpy as np
import squarify #treemap

“Lời” hay không?

Chúng ta sẽ dùng python vẽ plot mấy biểu đồ đơn giản về thu nhập và chi phí để trả lời câu hỏi trên. Ở đây, chúng ta sẽ tạo ra 2 dataframe là income (thu nhập) với số tiền (Amount) dương và expense (chi tiêu) với số tiền âm.

# new dataframes
income = data_sorted[data_sorted['Amount'] > 0]
expense = data_sorted[data_sorted['Amount'] < 0]
# plot
plt.figure(figsize=(10,8))
width = 7
fontsize = 15
plt.bar(income['Date'], income['Amount'], color='cornflowerblue', width=width, label='income')
plt.bar(expense['Date'], expense['Amount'], color='tomato', width=width, label='expense')
plt.axhline(0, color='grey', lw=0.5)
plt.title('Income and expense', fontsize=18)
plt.xlabel('Date', fontsize=fontsize)
plt.ylabel('Amount (JPY)', fontsize=fontsize)
plt.xticks(rotation=45)
plt.legend(fontsize=fontsize)

Biểu đồ này đơn thuần biểu thị flows (dòng tiền) thôi. Các bạn sẽ thấy thu nhập của mình không ổn định lắm trong những năm đầu tiên (vì phải kiếm học bổng). Sau khi có rồi thì ổn định hơn hẳn, dòng tiền rất đều đặn 🙂

Nhược điểm của biểu đồ này là chúng ta chỉ thấy flows, chứ không thấy stock. Cái quan trọng là stock vì nó sẽ cho thấy flows add vào bao nhiêu và lấy đi bao nhiêu. Để làm điều này, chúng ta sẽ tính cumulative sum cho cả income và expense và plot giá trị đó, các phần còn lại gần như giữ nguyên.

# make cummulative sum 
income['cum_sum'] = income['Amount'].cumsum()
expense['cum_sum'] = expense['Amount'].cumsum()
# plot 
plt.bar(income['Date'], income['cum_sum'], color='cornflowerblue', width=width, label='income')
plt.bar(expense['Date'], expense['cum_sum'], color='tomato', width=width, label='expense')
# the rest is the same

Bây giờ bức tranh đã dễ hiểu hơn. Nhìn rất giống tài khoản ngân khố quốc gia (national account). Tuy nhiên, để biết được chi nhiều hơn hay ít hơn thu về thì chúng ta cần tính giá trị net.

Để cho dễ nhìn, chúng ta sẽ nhóm gộp Amount (tổng thu nhập – chi tiêu) theo tháng, rồi tính net dựa vào cumulative sum. Vì dữ liệu gốc bao gồm nhiều entries cùng 1 ngày, nên trước tiên chúng ta phải gộp theo ngày trước rồi reset index theo ngày. Như vậy, dữ liệu sẽ được phân loại theo ngày. Tiếp theo, trong format ngày đó thì lấy ra month và year, rồi tạo thêm 1 biến Month_Year, để sau đó gộp theo Month_Year. Dữ liệu thu về chính là tổng thu nhập – chi tiêu theo từng tháng một. Nhưng vì đây mới chỉ là flow thôi, nên chúng ta tính tiếp cumulative sum để ra stock.

# (1) sum data by day
data_daily = data_sorted.groupby(['Date'])['Amount'].sum().reset_index()
data_daily.set_index('Date', inplace=True)
# (2) sum data by month 
data_daily['Month'] = data_daily.index.month
data_daily['Year'] = data_daily.index.year
data_daily['Month_Year'] = data_daily['Month'].map(str) + '-' + data_daily['Year'].map(str)
data_daily['Month_Year'] = pd.to_datetime(data_daily['Month_Year'])
data_monthly = data_daily.groupby(['Month_Year'])['Amount'].sum().reset_index()
data_monthly.set_index('Month_Year', inplace=True)
# plot 
plt.figure(figsize=(10,8))
data_monthly['cum_sum'] = data_monthly['Amount'].cumsum()
plt.plot(data_monthly['cum_sum'], color='darkgreen')

Theo như số liệu cho thấy thì cuộc đời không phải lúc nào cũng màu hồng đâu. Có lúc rất nhiều xiền, nhưng cũng có lúc chạm đáy khủng hoảng (mùa xuân 2021). Điểm của mình không thật sự tốt ở kỳ thứ hai khi phải đi làm. Nhưng đến kỳ tiếp theo mình chấp nhận bỏ việc để làm khoá luận nên gần như không có chi tiêu. Nói vậy thôi, lúc đấy xác định tháng sau có học bổng rồi nên bỏ việc tự tin hơn hẳn.

Như vậy, câu hỏi của mọi người đã có lời giải đáp rồi. Nhìn chung mấy năm đầu sẽ hơi vất vả nếu không có học bổng, nhưng nếu học điểm tốt để kiếm về học bổng thì cuộc sống lại tươi đẹp ngay . Ở thời điểm của bài viết thì số tiền mình đang có chưa bằng số mang sang, nên câu trả lời có thể là “không lời“. Cũng đúng thôi, đi học để “lời” cái khác chứ không hẳn là tiền bạc mà 🙂

Không lời vẫn đi ! Chăm chỉ học thì họ sẽ không cho mình thiệt đâu.

Nguồn income từ đâu?

Income của mình chủ yếu đến từ 3 nguồn: đi làm thêm (gọi chung là salary, mặc dù từ này chưa được chính xác lắm), học bổng (scholarship) và học bổng theo lương (gọi là stipend, cái này nhiều khi phải đóng thuế). Một biểu đồ bar stacked (hoặc area) sẽ giúp chúng ta biết được tỷ trọng nguồn thu nhập theo thời gian.

Để làm điều đó, đầu tiên mình lấy dữ liệu của 3 cục income mình quan tâm (salary, scholarship và stipend). Sau đó, merge chúng lại và sort theo ngày (ngày nhận lương thì nhận giá trị dương, ngày nào không thì để 0). Tiếp theo, chúng ta tiếp tục merge lại theo tháng.

# sort out income sources 
data_sorted_date = data_sorted.set_index('Date')
y1 = data_sorted_date[data_sorted_date['Category'] == 'Salary']['Amount']
y2 = data_sorted_date[data_sorted_date['Category'] == 'Stipend']['Amount']
y3 = data_sorted_date[data_sorted_date['Category'] == 'Scholarship']['Amount']

# merge y1, y2, y3 dataset into one dataset by date
data_merge = pd.merge(y1, y2, how='outer', left_index=True, right_index=True)
data_merge = pd.merge(data_merge, y3, how='outer', left_index=True, right_index=True)
data_merge.fillna(0, inplace=True)
data_merge.reset_index(inplace=True)
data_merge['Date'] = pd.to_datetime(data_merge['Date'])
data_merge = data_merge.sort_values(by=['Date'], ascending=True).reset_index(drop=True)
data_merge.columns = ['Date', 'Salary', 'Stipend', 'Scholarship']

# group by month but keep columns separated, then sum by month
data_merge2 = data_merge.set_index('Date')
data_monthly2 = data_merge2.groupby(['Date'])[['Salary', 'Stipend', 'Scholarship']].sum().reset_index()
data_monthly2.set_index('Date', inplace=True)
data_monthly2['Month'] = data_monthly2.index.month
data_monthly2['Year'] = data_monthly2.index.year
data_monthly2['Month_Year'] = data_monthly2['Month'].map(str) + '-' + data_monthly2['Year'].map(str)
data_monthly2['Month_Year'] = pd.to_datetime(data_monthly2['Month_Year'])

# sum each column by month_year 
data_monthly3 = data_monthly2.groupby(['Month_Year'])[['Salary', 'Stipend', 'Scholarship']].sum().reset_index()
data_monthly3.set_index('Month_Year', inplace=True)

Để plot, có 2 cách. Cách 1 là dùng function plot.area có sẵn của library pandas. Cách 2 là convert dữ liệu sang numpy array, và dùng function fill_between của matplotlib.

Cách 1 (không thể đơn giản hơn)

data_monthly3.plot.area(figsize=(10,8))

Cách 2: phải convert dữ liệu sang numpy array.

# convert index to numpy array datettime 
x = np.array(data_monthly3.index, dtype=np.datetime64)
# Stack arrays in sequence vertically
y = np.row_stack((data_monthly3['Salary'], data_monthly3['Stipend'], data_monthly3['Scholarship']))
y_stack = np.cumsum(y, axis=0) #a 3 by x array of cumulative sum
# stacked bar plot
fig = plt.figure(figsize=(10,8))
ax1 = fig.add_subplot(111)
ax1.fill_between(x, 0, y_stack[0,:], color='tomato', label='Salary')
ax1.fill_between(x, y_stack[0,:], y_stack[1,:], color='cornflowerblue', label='Stipend')
ax1.fill_between(x, y_stack[1,:], y_stack[2,:], color='darkgreen', label='Scholarship')

Kết quả như nhau.

Thời gian đầu đi làm nhiều, nhưng về sau có học bổng thì ít hơn hẳn. Đáng chú ý là điểm của mình thấp nhất ở thời điểm đi làm nhiều nhất. Kể từ khi có học bổng, điểm luôn ở mức cao.

Tiêu gì nhiều nhất?

Treemap (hay heatmap) rất được ưa chuộng trong tableau. Nó là biểu đồ gồm nhiều ô vuông. Phần nào có tỷ trọng lớn thì ô vuông càng to và ngược lại. Thêm màu sắc dạng biểu đồ nhiệt nữa thì sẽ rất trực quan. Tuy nhiên, nhược điểm là nếu xài Tableau, các bạn không muốn chi tiền thì chỉ được dùng gói public và không được giấu data gốc (LOL). Kể từ lúc biết được xài Tableau Free là mất quyền riêng tư như thế, mình dùng matplotlib cho khoẻ. Và library các bạn cần để làm điều đó là squarify.

Đầu tiên, chúng ta sẽ tính tổng số tiền của các danh mục chi tiêu giống nhau, rồi sort theo thứ tự giảm dần cho income và theo thứ tự tăng dần cho expense (bởi vì expense là âm). Nhiều khi hạng mục chi tiêu rất nhiều, mấy cái nhỏ nhỏ sẽ không cần thiết, nên mình lấy ra top 20 thôi.

#group by category
data_group = data1.groupby('Category')['Amount'].sum()
data_group = data_group.sort_values(ascending=False)
data_group = data_group.reset_index()
# income if positive and expense if negative
income = data_group[data_group['Amount'] > 0]
expense = data_group[data_group['Amount'] < 0]
#
top = 20
expensetop = expense.sort_values(by='Amount', ascending=True)[:top]

Bây giờ các bạn plot. Cứ follow documentation của squarify là ra ngay. Đầu tiên là chọn color palette (ở đây mình chọn viridis cho màu mè, nhưng dùng monochrome cũng dc), sau đó các bạn khai báo giá trị min và giá trị max, và chuẩn hoá dư liệu để squarify gán màu. Ở mục plot, sizes là kích cỡ (chi gì càng nhiều thì cục này càng to), label là nhãn cho mỗi cục, alpha là độ rõ của màu (càng gần 0 thì nó càng nhạt), edgecolor và linewidth để kiểm soát đường chia giữa các ô.

# create a color palette, mapped to these values (normalized)
cmap = matplotlib.cm.viridis
mini=min(expensetop['Amount'])
maxi=max(expensetop['Amount'])
norm = matplotlib.colors.Normalize(vmin=mini, vmax=maxi)
colors = [cmap(norm(value)) for value in expensetop['Amount']]
# plotting
plt.figure(figsize=(25,15))
plt.rc('font', size=14, family='serif') #labeling style
squarify.plot(sizes=expensetop['Amount'], label=expensetop['Category'], color=colors, alpha=.7, edgecolor='k', linewidth=.5)

Theo như hình này thì mình chi nhiều nhất cho học phí và tiền nhà (nhóm màu tím). Nhóm tốn nhiều tiền tiếp theo với màu xanh lá cây bao gồm thiết bị điện từ (điện thoại, laptop), đi ăn ngoài và đồ ăn, sau đó là sách. Nhóm tiếp theo chủ yếu là để di chuyển như tàu, xe, bảo hiểm và đồ lặt vặt trong nhà. Nhóm cuối cùng của top 20 là màu vàng, bao gồm các chi phí cần thiết.

Bonus

Thành thực mà nói, nếu các bạn hỏi mình thì treemap của Tableau vẫn đẹp hơn hẳn 🙂

Advertisement

2 thoughts on “Đi du học có “lời” không?

  1. Cảm ơn anh đã thực hiện 1 user story rất hay và visualize thực sự sinh động ạ. Em mong sẽ được đón đọc nhiều bài viết của anh về chủ đề ứng dụng python như thế này hơn nữa.

    Đã thích bởi 1 người

Để lại bình luận

Điền thông tin vào ô dưới đây hoặc nhấn vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Đăng xuất /  Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Đăng xuất /  Thay đổi )

Connecting to %s

Trang web này sử dụng Akismet để lọc thư rác. Tìm hiểu cách xử lý bình luận của bạn.