900格人生表格这个概念,估计不少人都刷到过:把人的一生按月切成格子,假设活到75岁,就是75乘12,整整900格。每过一个月涂掉一格,回头一看,人生进度一目了然。我第一次刷到这种图的时候,第一反应就是“这玩意儿肯定能用代码画”,而且一定要短,最好一行代码搞定。这个项目做的就是这件事:输入出生年月,用Python加matplotlib,一行核心代码画出你要的900格人生表格,剩下的全是细节打磨——颜色、尺寸、百分比标注、当前月高亮,一个都不少。
先说清楚,这不是什么高深技术。整段代码的核心逻辑就是一个日期差计算加一个布尔矩阵,难度约等于入门Python一周后的水平。但它解决了一个很具体的问题:让你直观看见自己已经走过的岁月。网上很多版本要么是Excel手工涂色,要么是App里的付费功能,自己写的好处是数据完全在自己手里,想怎么改就怎么改。这篇文章把思路、代码、坑点都摊开讲,适合三类人:想用代码做点生活化小工具的初学者、对人生规划感兴趣但不想用现成模板的人,以及纯粹想折腾可视化技术的人。
1. 900格表格背后的设计逻辑
1.1 为什么是900格:计算方式与视觉冲击力
900这个数字不是随手拍的,它背后是一个平凡的乘法:目标寿命75岁,乘上每年12个月,得到900个月。换句话说,格子图默认的假设是“一个人能活到75岁”。你可以不认同这个假设,把目标寿命改成80岁就是960格,改到90岁就是1080格。这个数字不必跟风,完全取决于你打算用“多少岁”作为人生进度的参考线。
我个人的建议是,第一次做先用75岁这个标准。原因很简单:它够扎心。把预期寿命调得越长,未涂满的格子越多,紧迫感反而会被稀释。900格这个方案的传播力之所以强,是因为人对抽象数字不敏感,但对具体图形敏感。你告诉一个30岁的人“人生已过40%”,他大概率无感;但你把360个格子涂成深色、剩下540个格子留着空白,那幅画面自带冲击力。
这其实就是数据可视化的本质:把不可见的时间流逝,变成一个可见的进度条。表格里每一格没有高低贵贱之分,它们完全等大,却承载着完全不同的生命片段。有人把这种图叫作“人生倒计时”,我觉得它更像是“人生进度条”——倒计时容易让人焦虑,进度条则更像游戏里的经验条,反而会让人思考下一步该怎么走。
1.2 为什么用代码绘制:自动更新和可定制性
网上流传最多的版本是手绘:打印一张网格图,用荧光笔每月涂一格。手绘有仪式感,适合贴在墙上,我也确实建议有条件的人打印一份挂起来。但手绘有两个硬伤:一是容易断,每个月都想着涂一下,坚持半年就忘了;二是改不了样式,想换配色、加百分比、给每个格子写备注,手绘基本做不到。
代码版解决的就是这两个痛点。只要把出生年月填进配置区,程序会实时读取今天的日期,自动算出已走过多少个月,然后一次性画好整张图。下个月再看,重新运行一次,新的格子自动涂满,不需要任何手动维护。想换颜色、换尺寸、加标题、存成高清图,全是改参数的事。
我实测下来,这个脚本完整运行时间不超过1秒。把画好的图设置成电脑桌面壁纸,或者每个月初运行一次存个档,都比手绘省心太多。这也是我认为“一行代码”值得做的根本原因——它把一件偶尔想起来才做的事,变成了一条可持续更新的自动化流程。
2. 动手前的关键细节
2.1 日期计算:已过月数怎么算才准确
这张图最核心的计算,就是把“出生年月日”和“今天的日期”换算成“已经过去了多少个月”。公式很简单:
已过月数 = (当前年份 - 出生年份) * 12 + (当前月份 - 出生月份)注意,这里不精确到“天”,也没有处理闰年,因为格子图的最小单位就是“月”。到了一个新的月份,就算过了一个格子;哪怕你出生那天是1月31日,在2月1日时计算,也只是看月份差,不看日。这样反而简洁,也符合人生格子的语义。
举个例子:1995年6月出生的人,到2025年4月,已过月数就是(2025 - 1995) * 12 + (4 - 6),也就是360 - 2 = 358个月。这个公式在“当前月份小于出生月份”时会出现负数偏差,但最终结果依然正确,因为负数会被年份差乘12拉平,不用额外判断。
边界情况要自己想清楚:出生当月到底算不算“已过一月”?我的做法是不算。出生到满一个月,才算涂掉第一格;所以1995年6月出生,到1995年7月,已过月数才是1。这样处理的好处是语义干净:每个格子代表“你过完了的那个完整的月”,而不是“你正在过的月”。
2.2 表格布局与色彩方案选型
900个格子怎么摆放,直接决定这张图的阅读体验。主流方案是reshape(75, 12),也就是75行12列。行代表年,列代表月,一行正好是一年。这样排列的好处是符合人脑按年组织记忆的方式,看到一行里涂满了深色,就直觉感到“那一年已经过去了”。
为什么不用12行75列?因为那意味着每一列代表一年,整张图会非常宽,阅读时视线横向跨度太大,很难一眼定位自己所在的年份。75行12列接近一张A4纸的比例,在手机上竖屏看也舒服。
色彩方案是另一个决定性细节。最简单的做法是直接用matplotlib内置的色带,比如cmap="RdYlBu_r",已过格子显示深红或深蓝,未过格子显示浅色。但我后来更推荐自定义颜色,因为内置色带的对比度有时太强,看着像热力图,缺少一点“手账”质感。
我常用的自定义配色是:已过格子用深灰蓝#2A5C82,未过格子用浅灰#E8E8E8,当前月用橙色高亮#FF8C42。这样整张图看起来克制、清楚,深色部分就是你走过的岁月,一眼就能读出比例。不要用大红大绿,看久了眼睛累,朋友圈晒图也显得不够精致。
3. 实操过程:从一行代码到完整版
3.1 一行代码版:先让它跑起来
我标题里说“一行代码”,当然不是指整个程序只有一行——那既不可读也没必要。这里的“一行”指的是核心绘图语句可以压缩成一行,而且这一行就能完成90%的工作。先把最简版跑通,后面想加功能再拆开。
直接用一行代码实现核心逻辑,可以这么写:
import numpy as np, matplotlib.pyplot as plt from datetime import date # 假设1995年6月出生,目标寿命75岁 plt.imshow(np.arange(900).reshape(75, 12) < (date.today().year - 1995) * 12 + (date.today().month - 6), cmap='RdYlBu_r', aspect='auto') plt.axis('off') plt.show()这行的核心思路是:np.arange(900)生成0到899这900个数字,reshape(75, 12)把它们排成75行12列;然后整体和“已过月数”做比较。已过的月份索引小于已过月数,结果是True,未过的是False。matplotlib的imshow会自动把True和False映射成两种颜色,于是900格人生图就出来了。
这个写法非常Pythonic——用布尔矩阵代替显式的双重循环。你不需要逐个判断每个格子该涂什么颜色,一次比较运算就完成了全部赋值。我第一次跑通的时候也愣了一下,原来900个格子的涂色逻辑,一行就能搞定。
3.2 完整版拆解:分步骤讲清楚每一行
一行版本能跑,但缺少细节。接下来我把代码拆成干净的结构,每个部分加注释,方便你按需修改。完整版代码如下:
import numpy as np import matplotlib.pyplot as plt from datetime import date # ========== 配置区:只改这里 ========== BIRTH_YEAR = 1995 BIRTH_MONTH = 6 TARGET_AGE = 75 # 人生目标寿命,可以改成80或90 CURRENT_COLOR = '#FF8C42' # 当前月高亮颜色 PASSED_COLOR = '#2A5C82' # 已过格子的颜色 FUTURE_COLOR = '#E8E8E8' # 未过格子的颜色 # ========== 自动计算 ========== today = date.today() months_passed = (today.year - BIRTH_YEAR) * 12 + (today.month - BIRTH_MONTH) total_grids = TARGET_AGE * 12 grid = np.arange(total_grids).reshape(TARGET_AGE, 12) matrix = grid < months_passed # 布尔矩阵,True代表已过 # ========== 自定义颜色映射 ========== from matplotlib.colors import ListedColormap cmap = ListedColormap([FUTURE_COLOR, PASSED_COLOR]) # ========== 绘图 ========== fig, ax = plt.subplots(figsize=(12, 8), dpi=150) ax.imshow(matrix, cmap=cmap, aspect='auto') # 当前月格子高亮 current_row = months_passed // 12 current_col = months_passed % 12 ax.add_patch(plt.Rectangle((current_col - 0.5, current_row - 0.5), 1, 1, fill=False, edgecolor=CURRENT_COLOR, linewidth=3)) # ========== 文字标注 ========== percent = months_passed / total_grids * 100 ax.set_title(f"{BIRTH_YEAR}.{BIRTH_MONTH:02d} 出生 · 已走过 {months_passed} 个月 · 人生进度 {percent:.1f}%", fontsize=16, pad=15) ax.set_xlabel(f"目标寿命 {TARGET_AGE} 岁", fontsize=12) # ========== 隐藏坐标轴,只留网格线 ========== ax.set_xticks(np.arange(-0.5, 12, 1), minor=True) ax.set_yticks(np.arange(-0.5, TARGET_AGE, 1), minor=True) ax.grid(which='minor', color='white', linewidth=1) ax.tick_params(left=False, bottom=False, labelleft=False, labelbottom=False) plt.tight_layout() plt.savefig('life_grid.png', bbox_inches='tight') plt.show()这段代码我建议你直接复制到本地跑。每一块都有明确用途:
months_passed是唯一的核心计算,它把日期差换算成月数。np.arange加reshape生成一个二维序号矩阵,序号0到899刚好对应900个格子。拿整个矩阵和months_passed比较,得到的布尔矩阵就是涂色依据。这里有个细节:布尔矩阵True会被ListedColormap映射成第二个颜色,也就是PASSED_COLOR;False映射成第一个颜色,也就是FUTURE_COLOR。顺序千万别写反,否则已过和未过的颜色就颠倒了。
3.3 参数微调:做出真正好看的图
跑通之后再谈审美。我介绍几个实测有效的微调方向。
第一个是尺寸和分辨率。figsize=(12, 8)在电脑上看合适,但如果要发朋友圈或者设成壁纸,建议把dpi拉到200以上,保存时用bbox_inches='tight'。这样格子边缘的描线不会糊。第二个是当前月高亮,这是我强烈建议加的功能。一块橙色边框把“这个月”标出来,每次看图能立刻定位到自己站在时间的哪个位置,比单纯看深色面积更震撼。第三个是百分比标注,把它放进标题里,配合格子图,信息就完整了。
如果你想让格子更加细节丰富,可以在每行左侧标注年份,比如“1995”“1996”,这样每行对应哪一年一眼就能看清。实现方法很简单:ax.set_yticks(range(TARGET_AGE)),ax.set_yticklabels(range(BIRTH_YEAR, BIRTH_YEAR + TARGET_AGE)),但记得要保留minor grid线,避免和年份刻度冲突。
我后来还试过把“已过年份”和“未过年份”分别标注不同颜色,但发现效果并不好——文字一多,视觉焦点就散了。一张900格图最好的状态就是简洁:大量留白,深色只代表过去,橙色只代表当下。
4. 常见问题与排查技巧实录
4.1 中文显示成方块的解决方案
这是我被问过最多的问题:中文标题在matplotlib里显示成一个个方框。原因很简单,matplotlib的默认字体不支持中文。解决办法是在绘图前指定中文字体:
plt.rcParams['font.sans-serif'] = ['PingFang SC', 'Microsoft YaHei', 'SimHei'] plt.rcParams['axes.unicode_minus'] = False不同系统可用的字体不一样:macOS用PingFang SC,Windows用Microsoft YaHei或SimHei,Linux可以装文泉驿微米黑。如果设置了还是不生效,最笨但最稳的办法是直接用英文标题,比如Life in 900 Grids,中文字体问题就彻底绕开了。我个人后来反而觉得英文标题更简洁,和深色格子风格更搭。
4.2 月份差计算的边界问题
日期计算有几个边界情况容易踩坑。
第一个是年初出生和年末出生的差异。比如2000年12月出生,到2025年1月,months_passed = (2025 - 2000) * 12 + (1 - 12) = 300 - 11 = 289,这个结果是对的。反过来,如果出生月份大于当前月份,公式里的月份差是负的,但年份差乘12已经把负值拉平了,不用担心算错。
第二个是“当前月是否该算作已过”。我前面讲了,我的语义是“过完的完整月才算”,所以当前月不涂满。如果你希望“一到这个月就立刻涂掉”,可以把months_passed加1。两种口径差别很小,但会导致所有后续格子整体偏移一位,你自己选一种保持一致就好。
第三个是目标寿命到期后怎么办。如果设置TARGET_AGE=75但实际年龄已经超过75岁,months_passed会大于900,布尔矩阵全都是True,整张图全涂满。这也不算bug,该你涂满的时候妥妥是涂满的。如果不想全满,可以把TARGET_AGE改成90,留点余地。
4.3 配色和尺寸调整经验
颜色搭配是审美问题,但也有技术层面。用ListedColormap时,颜色的顺序决定了它对应True还是False,前面已经说过,写反了就是整张图颜色对调。这是个特别容易忽略的坑,我见过不止一个人跑出反色版本,还以为是matplotlib的问题。
尺寸方面,900格在默认的figsize下会显得非常扁。我试过figsize=(12, 8)、(10, 10)、(16, 9),直观感受是宽高比接近16:9时最适合当壁纸,接近3:4时最适合发朋友圈竖图。你可以用figsize第二个参数控制高度,再配合aspect='auto'让matplotlib自动拉伸格子。注意,aspect='equal'会让格子变成正圆形或者正方形,但整体图会被拉得又细又长,除非你刻意想要那种效果,否则用'auto'就好。
网格线粗细也值得调。细线显精致,粗线显厚重。我用linewidth=1配白色网格线,效果好于默认的黑色网格线,因为深色格子和浅色格子之间的分界被白色线条统一了,视觉更干净。
4.4 常见错误速查表
| 报错或异常 | 原因 | 解决办法 |
|---|---|---|
| 中文标题显示方块 | 默认字体不支持中文 | 设置plt.rcParams['font.sans-serif']或用英文标题 |
| 格子颜色全部反了 | ListedColormap颜色顺序写反 | 把FUTURE_COLOR放前面,PASSED_COLOR放后面 |
reshape报错,长度不匹配 | 格子数不是行数乘列数 | TARGET_AGE * 12必须等于总格子数 |
| 图片太窄,格子都变成竖条 | figsize比例失调 | 改用figsize=(12, 8),并设置aspect='auto' |
| 月份差计算出来是负数 | 出生月份大于当前月份,且年份差为0 | 确认公式是(今年-出生年)*12 + (今月-出生月) |
| 保存的图片四周留白太多 | 默认边距过大 | 保存时加bbox_inches='tight' |
5. 从表格到工具:更多玩法扩展
5.1 网页交互版:点击就能涂格子
Python生成的静态图片够用了,但如果你想把格子图变成可以交互的工具,网页版是更好的方向。用HTML加CSS就能做:写一个div容器,里面放900个span,每个span是一个格子,用CSS Grid布局,宽高比例天然可控。JavaScript负责两件事:读取当前日期算出已过月数,以及给前N个格子加上“已过”类名。整个网页不需要后端,一个单文件就能跑。
我这个方案实测下来,浏览器渲染900个div毫无压力,加载速度和静态图片差不多。交互的好处是你可以点击任何一个格子,给它加上备注,比如“第一次出国”“孩子出生”“换了工作”。这些备注可以存到LocalStorage里,刷新不丢。900个格子,每个都成了人生的一个时间锚点,远比一张静态图有记录价值。
5.2 定时自动打卡:让脚本每月提醒你
Python版的一个天然优势是容易自动化。把脚本存成文件,在系统里加一个定时任务,每个月的1号凌晨自动运行一次,生成最新的人生进度图。Windows可以用计划任务,macOS和Linux可以用cron,配置语法不同,但思路一致:一行命令运行Python脚本,自动把最新的PNG存到指定目录。
如果你想把这个图“打开率”最大化,可以把脚本和通知服务联动,每月定时把图片推送到手机上。我自己试过之后发现,这件事最难的不是技术,而是“愿意面对”的心态。月初第一眼看到那个橙色边框又往后移了一格,有一种奇妙的紧迫感:又过去一个月了,这个月我做了点什么?
5.3 用表格反向审视人生
到最后,我想说这个表格真正的作用。它最直观的用法是“看过去”,看自己活了多少个月、涂了多少格子。但它的力量其实在“看未来”和“看现在”。
看未来:还剩多少格子,一眼就能数出来。假设你今年30岁,目标75岁,那么还剩540格。540看起来很多,但换成“你大概还能过45个春节”“还能陪父母过45个团圆夜”,数字就变得具体了。
看现在:当前月被高亮的那一格,是整张图唯一处于“正在发生”的格子。过去都涂死了,未来还空白,只有现在这一格带着橙色边框。这个设计是有意为之的——每当我盯着那张图,注意力最后总会落在橙色格子上。它提醒我,过去无法改变,未来还没来,唯一有操作空间的就是当下这个月。
我把这张图作为电脑桌面已经半年了。头几天确实有点焦虑,盯着那一片深色格子总觉得自己浪费了太多时间。但看久了反而平静下来,深色格子代表的是已经发生的一切,不管是虚度还是充实,都已经是事实。真正值得关注的,是那个橙色格子和它右边接下来的空白。
代码之外的一点体会
如果你也想做一张,我的建议是:先别急着调颜色、加功能,先把“一行代码”版本跑通。看到那900个格子在你屏幕上铺开的那一瞬间,你对时间的感知会和看别人的截图完全不同。然后,再花十分钟把完整版跑起来,填上自己的出生年月,让它替你算清楚已过的岁月。
我最后分享一个实用习惯:把这张图设成手机锁屏,或者打印出来贴在书桌前。它不需要你每天盯着看,甚至一个月看一次就够。但每当你想“时间还很多”的时候,瞥一眼那张图上越来越短的空白区域,很多犹豫就会变得不再犹豫。900格不多不少,正好够一个人认真活完一生。