在 2023 年底的时候,我开始准备记录自己每天的时间是怎么安排的。

其实对于我自己来说,最大的驱动力来自记录自己的睡眠。毕竟作为「研究生」,平时也没有白天一定要做什么事情的需求(除了开会),生物钟经常颠倒,睡觉也睡的不好。如果能够自己统计一些具体的数据,或许可以分析分析怎么才能让自己的生物钟正常点,每天睡的时间也能多一点。

其实当时我也调研了一些可能的 app 去做时间记录,但是我自己可能是一个比较挑剔的人,根据我的以下需求,确实是很难有满足我要求的第三方工具了:

  • 能导出 CSV 数据:别人画的图毕竟受限,想好看我有了数据可以自己 Python 画
  • 足够方便:随时随地记录,而且点击要少,时间要短(一定一定不能有诸如开屏广告的玩意儿)
  • 可自由定制类型:我可以只把时间分成两类(生活+工作),也可以细分到每一个工作项目,每一种生活类型(甚至玩哪个游戏),选择哪种方案是我的自由,想简单可简单,想复杂可复杂

好吧,说到这儿,我说实话已经不太可能找别人开发的方案了。

基于快捷指令的时间记录「app」

聊几句我自己的背景:我是做生信的,代码能力不强,也就玩玩 Python 的水平,写 iOS app 是绝对不行的。不过本段涉及的算法技术只有 if-else,其他部分大家真的随便看看就懂了。

既然都想自己搞了,我就把目光投向了 iOS 上的快捷指令。

在讲快捷指令之前,我甚至还尝试过一个更离谱的做法,写一段 Python 程序,然后手机执行代码,把记录结果远程发送到电脑。(这听着也太麻烦了吧)

一阵摸索后,我发现快捷指令比我想象中好用太多,其实本质上就是傻瓜式编程的软件,或者可以理解成现在流行的「无代码编程」。而且作为颜值控,快捷指令居然能生成一个类似于 app 的图标放在主屏,甚至这个图标还可以用自己的图片!能拥有自己定制的 logo 真是太美好了。

那么到这里,我们已经准备好了实现的途径,现在来列一下实际的需求:

  • 能导出 CSV 数据:app 的输出存放在便签中,作为纯文本数据,其实已经很容易读取;
  • 足够方便:每次记录时间点击屏幕三次(包括打开 app),不能再多了;
  • 可自由定制类型:方便地添加/删除记录的类型。

于是我的流程便是:

手动打开 app ➡️ 手动选择大类 (Work/Life) ➡️ 手动选择小类 (Relax/Gaming/Sleep/Travel) ➡️ 自动生成一段文字 ➡️ 自动添加到某个便签的最后一行

第一步:选择大类
第二步:工作大类中选择小类
第二步:生活大类中选择小类
最后一步:把结果(一句话)添加到便签
实际操作:点两下就行

上图就是实际操作的界面,两次点击(算上打开 app 三次)就可以了,真的很方便。而且我还换了一个好看的 logo,这样我就更有动力去点它了

输出的便签长这样:

Major: [Life]. Minor: [Life: Relax]. Starting at: [Jul 26, 2024 at 05:38]
Major: [Life]. Minor: [Life: Sleep]. Starting at: [Jul 26, 2024 at 08:01]
Major: [Life]. Minor: [Life: Relax]. Starting at: [Jul 26, 2024 at 12:00]
Major: [Life]. Minor: [Life: Relax]. Starting at: [Jul 27, 2024 at 12:04]

养成习惯还是需要一些过程的,平时的我需要睡觉前点一下,起床点一下,开始干活点一下,准备吃饭点一下,说白了还是小有点麻烦。所以我一开始分的类多,后面其实分类就越来越少了,无非就分休息/睡觉/工作这几个了。

统计结果可视化

既然都有了统计的结果,我们就可以根据记录的时间来做图了。我最近还是比较关心我每天睡的好不好,特别是每天到底睡够了没。

于是我就写了一段 Python 画图的代码,看看从今年第一天到现在(八月)每天睡了几个小时:

钟二每天睡了几个小时?

哎,上两周睡的也太惨了😭

如果统计一个直方图的话:

钟二睡觉时长居然符合正态分布?

群友锐评 1:睡眠时间能拉出以小时为单位的正态有点狠啊

群友锐评 2:感觉我要是用钟老师的 colorbar,这一张热图都是蓝的

不过我自己感觉还算良好,居然我大半部分时间都有错过六小时睡眠,我已经很满足了。

后话

我自己算是一个比较爱折腾的人,能为了记录个时间折腾半天也是没谁了 hhh。

其实除了看睡了几个小时,我之前还统计过我自己做的每个项目到底花了多少时间。等最后有了回报之后,我就能统计下我的时间/产出比了。还有一段时间我总觉得我的人生浪费在了无穷无尽无聊的开会中,我当时也统计了下我一周到底花了多少时间在开会上(真的很多!),那次之后我就开始翘会了,能不开就别开了

这个 TimeTracker 已经变成我生活中有趣的一部分了,今天分享给大家,要是感兴趣的话也欢迎大家来交流和尝试

Source code

画图的代码就不公开了,大家多多培养自己的艺术细胞吧

读便签数据:

# load timetracker data
# the line format: Major: [MAJOR_TYPE]. Minor: [MINOR_TYPE]. Starting at: [MONTH DAY, YEAR at TIME]
def load_timetracker_records(file_path):
    """Load timetracker records from a file

    Args:
        file_path (str): The path to the file

    Returns:
        pd.DataFrame: A DataFrame containing the records
        - major: The major activity
        - minor: The minor activity
        - start_time: The starting time
        - end_time: The ending time
    """

    major_activities = []
    minor_activities = []
    start_times = []

    with open('timetracker.txt', 'r') as f:
        # skip first line (first line is "time tracker")
        f.readline()
        for line in f:
            major, minor, start_time = re.findall(r"\[([^\]]+)\]", line)
            start_time = datetime.strptime(start_time, "%b %d, %Y at %H:%M")
            
            # add to lists
            major_activities.append(major)
            minor_activities.append(minor)
            start_times.append(start_time)
    
    end_times = start_times[1:] + ["None"]

    return pd.DataFrame({
        'major': major_activities,
        'minor': minor_activities,
        'start_time': start_times,
        'end_time': end_times
    })
        

统计每天睡眠时长:

# create a blank calendar
# result: a list of day names, a list of start of day time, a list of end of day time
calendar_name_list = []
calendar_start_of_day_time_list = []
calendar_end_of_day_time_list = []

# generate calendar data
for i in range((time_end_date - time_start_date).days):
    date = time_start_date + timedelta(days=i)
    start_of_day = date.replace(hour=0, minute=0)
    end_of_day = start_of_day + timedelta(days=1)

    calendar_name_list.append(date.strftime("%b %d, %Y"))
    calendar_start_of_day_time_list.append(start_of_day)
    calendar_end_of_day_time_list.append(end_of_day)


sleep_calendar = [0 for _ in range(len(calendar_name_list))]    # sleep calendar record sleep time for each day


# enumerate through time tracker records
for i in range(len(data)):
    record = data.iloc[i]
    major = record['major']
    minor = record['minor']
    start_time = record['start_time']
    end_time = record['end_time']

    # if the record is a sleep record
    if major == "Life" and minor == "Life: Sleep":
        # find the corresponding day
        for j in range(len(calendar_name_list)):
            if start_time >= calendar_start_of_day_time_list[j] and start_time < calendar_end_of_day_time_list[j]:
                if end_time >= calendar_start_of_day_time_list[j] and end_time < calendar_end_of_day_time_list[j]:   # if the sleep record is within the same day
                    duration = end_time - start_time
                    duration_hours = duration.total_seconds() / 3600
                    sleep_calendar[j] += duration_hours
                elif end_time >= calendar_end_of_day_time_list[j]:  # if the sleep record spans multiple days
                    duration_today = calendar_end_of_day_time_list[j] - start_time
                    duration_today_hours = duration_today.total_seconds() / 3600
                    sleep_calendar[j] += duration_hours

                    remaining_duration = end_time - calendar_end_of_day_time_list[j]
                    remaining_duration_hours = remaining_duration.total_seconds() / 3600
                    for k in range(j+1, len(calendar_name_list)):
                        if remaining_duration_hours > 0:
                            if remaining_duration >= timedelta(days=1):
                                sleep_calendar[k] += 24
                                remaining_duration -= timedelta(days=1)
                                remaining_duration_hours = remaining_duration.total_seconds() / 3600
                            else:
                                sleep_calendar[k] += remaining_duration_hours
                                remaining_duration_hours = 0
                        else:
                            break
                else:
                    raise Exception("Invalid sleep record: " + str(record))

sleep_calendar = np.array(sleep_calendar)
# append np.nan to be the 7*n
n = 7
if len(sleep_calendar) % n != 0:
    sleep_calendar_reshape = np.append(sleep_calendar, [np.nan for _ in range(n - len(sleep_calendar) % n)])
    sleep_calendar_reshape = sleep_calendar_reshape.reshape(-1, n)
else:
    sleep_calendar_reshape = sleep_calendar.reshape(-1, n)

> 关注 少数派小红书,感受精彩数字生活 🍃

> 实用、好用的 正版软件,少数派为你呈现 🚀