📊 第9章 网站 App 平台注册用户分析
📄 一句话概括

pandas 的 read_sql 能把任何数据库连接变成 DataFrame,一句就够。 连接串里 charset 必须写 utf8mb4,否则中文全是问号。 sqlite3 只有 integer、text、real、blob 四种类型,日期得存成文本。

📝 本章和前面几章的关系

这一篇干两件事:把数据搬进来,把 MySQL 和 sqlite3 的语法差异摊开。 教材的 pymysql 代码原样贴出,标注「本沙箱不执行」;真跑的是 sqlite3。

一、课本的连法

四行:导入驱动、建连接、写 SQL、read_sql 读成 DataFrame。 host 是服务器地址,user 和 password 是账号,database 是库名。 关键只有一句:pd.read_sql(sql, conn) 把任何连接变成 DataFrame。

连接用完要 close()。不关的话连接数会被占满,跑几百次就再也连不上。

二、charset 那一行不能省

charset="utf8mb4" 是命令 MySQL「按四个字节的 UTF-8 说话」。 省了它,默认可能是 latin1 或者三字节的 utf8,中文立刻出问题。 utf8mb4 比 utf8 多一个 mb4,意思是要能存四个字节的字符——表情符号就属于这类。

这个坑不会报错,只会让「张三」变成「??」,或者干脆变成一堆乱码。

三、沙箱里换成 sqlite3

sqlite3 不用装,import sqlite3 就有,connect(路径) 直接建库。 它的类型只有四种:integer、text、real、blob。没有 varchar,没有 datetime。 所以建表时日期字段写 text,值照旧写 2025-03-14 09:20:00。

这样存的好处是:字典序等于时间序,排序、切片、比大小全都照常。 坏处是:算「相差几天」得先转成真正的时间类型,不能直接减。

四、两边的语法差异

要做的事 MySQL sqlite3
取年份 YEAR(reg_time) strftime 里写 %Y
取月份 MONTH(reg_time) strftime 里写 %m
日期常量 date 后跟字符串 直接写字符串
限制行数 limit 5 limit 5

同一句 where,两边写法不同。照 MySQL 的书抄到 sqlite 上,报错能查半天。 这次 2025 年有 92 条记录,两边算出来必须一样,不一样就说明口径错了。

五、读进来之后先做什么

看形状、看头五行、看类型。这是第 3 章的老规矩,换什么数据源都不变。 这次读进来是 300 行 5 列,字段类型全是 object(文本)。 从 read_sql 返回的那一刻起,MySQL 就退场了。后面全是 pandas 的活。

这一步交接要清楚:取数的问题归数据库,分析的问题归 pandas,别混着查。

六、动手 Demo:导入 MySQL 数据

教材的 pymysql 连接代码原样列出并标注不执行。 真正跑的是 sqlite3:建表、插 300 条、read_sql 读成 DataFrame。

import sqlite3

import numpy as np
import pandas as pd

rng = np.random.default_rng(2026)
plan = [(2021, 30), (2022, 42), (2023, 58), (2024, 78), (2025, 92)]
w = np.array([0.5, 0.6, 1.5, 1.0, 0.9, 0.8, 0.9, 1.1, 1.4, 1.0, 0.9, 1.0])
w = w / w.sum()
chan = ["搜索引擎", "社交媒体", "广告投放", "自然访问", "老用户推荐"]
cw = [0.32, 0.26, 0.18, 0.15, 0.09]
city = ["北京", "上海", "广州", "深圳", "杭州", "成都", "武汉", "其他"]
dw = [0.16, 0.14, 0.11, 0.12, 0.10, 0.09, 0.08, 0.20]

rows = []
uid = 1
for year, n in plan:
    for m in rng.choice(np.arange(1, 13), size=n, p=w):
        day = int(rng.integers(1, 29))
        hh = int(rng.integers(0, 24))
        mm = int(rng.integers(0, 60))
        rows.append((uid, "用户%03d" % uid,
                     "%04d-%02d-%02d %02d:%02d:00" % (year, m, day, hh, mm),
                     str(rng.choice(chan, p=cw)), str(rng.choice(city, p=dw))))
        uid += 1

conn = sqlite3.connect(":memory:")
conn.execute("create table users(user_id integer primary key, user_name text, "
             "reg_time text, channel text, city text)")
conn.executemany("insert into users values (?, ?, ?, ?, ?)", rows)
conn.commit()

df = pd.read_sql("select * from users order by user_id", conn)
print("形状 %s" % (df.shape,))
print(df.head(3).to_string(index=False))
print("2025 年条数 %d"
      % pd.read_sql("select count(*) as n from users "
                   "where strftime('%Y', reg_time) = '2025'", conn)["n"][0])
conn.close()

实跑输出:

--------------------------------------------------------------------
导入 MySQL 数据
--------------------------------------------------------------------

  ① 课本的连法
  --------------------------------------------------------------------

  [平台原样 · 本沙箱不执行]
  | import pymysql
  | conn = pymysql.connect(host="localhost", user="root",
  |                        password="123456", database="user_db",
  |                        charset="utf8mb4")
  | sql = "select * from users"
  | df = pd.read_sql(sql, conn)
  | conn.close()
  host/user/password 换成你自己的;charset 别省

  关键就一句:pandas 的 read_sql 能把任何 DBAPI 连接变成 DataFrame。
  连接串里 charset 一定要写 utf8mb4,否则中文全是问号。

  ② 沙箱里换成 sqlite3
  --------------------------------------------------------------------
  表建好了:users,300 条记录。
  sqlite3 没有 varchar、datetime 这些类型,只有 integer、text、real、blob 四种。
  日期就存成文本,格式照旧写 2025-03-14 09:20:00,照样能排序、能切片。

  ③ 读成 DataFrame
  --------------------------------------------------------------------
  读进来是 DataFrame,形状 300 行 5 列。

| user_id | user_name | reg_time | channel | city |
|---|---|---|---|---|
| 1 | 用户001 | 2021-03-28 05:56:00 | 自然访问 | 杭州 |
| 2 | 用户002 | 2021-09-25 04:20:00 | 老用户推荐 | 杭州 |
| 3 | 用户003 | 2021-07-17 10:25:00 | 搜索引擎 | 成都 |
| 4 | 用户004 | 2021-05-26 09:18:00 | 搜索引擎 | 成都 |
| 5 | 用户005 | 2021-05-12 05:32:00 | 广告投放 | 上海 |

  从这一步起,MySQL 就退场了。后面全是 pandas 的活。
  ! 读进来先看形状和头几行,这是第 3 章的老规矩,换什么数据源都不变。

  ④ 两边的语法差异
  --------------------------------------------------------------------

| 要做的事 | MySQL | sqlite3 |
|---|---|---|
| 取年份 | YEAR(reg_time) | strftime('%Y', reg_time) |
| 取月份 | MONTH(reg_time) | strftime('%m', reg_time) |
| 字符串日期 | date '2025-01-01' | 直接写 '2025-01-01' |
| 限制行数 | limit 5 | limit 5 |

  2025 年这一年有 92 条注册记录。
  换成 MySQL 就写成 where YEAR(reg_time) = 2025。同一个意思,两种写法。
  ! 这些差异不是小事。照着 MySQL 的书抄到 sqlite 上,报错能查半天。

executemany 一次插一批,比循环单条 insert 快得多。 pd.read_sql 的第一个参数是字符串 SQL,不是表名。要全表就读 select * from 表名。 换成 MySQL 只需把 sqlite3.connect 换成 pymysql.connect(...),后面一个字不用改。

30 秒实验

  1. 把 strftime 那句改成 reg_time like '2025%',看结果一不一样。
  2. 把 read_sql 换成 pd.read_sql_table(sqlite 用不了,想想为什么)。
  3. 想一想:为什么 sqlite3 不提供 datetime 类型,日期却照样能排序?

常见误区

  1. 连接串不写 charset:中文全变问号,还不报错。
  2. read_sql 传表名:它要的是 SQL 语句,不是表名。
  3. 忘了 close:连接数被占满,跑几百次就再也连不上。
  4. 照抄 MySQL 的 YEAR():sqlite 没这个函数,要写 strftime。
自测三问(点开对答案) 1. `pd.read_sql` 两个参数分别是什么? —— 一句 SQL 字符串,和一个数据库连接对象。返回 DataFrame。 2. 为什么 charset 要写 utf8mb4? —— 省了它默认字符集可能存不下中文和表情,会变问号或乱码,而且不报错。 3. sqlite3 没有 datetime 类型,日期怎么存的? —— 存成 text,格式照旧写年-月-日 时:分:秒,字典序就等于时间序。

数据进来了,但真实的库不会这么干净。下一篇:缺失、重复、异常,三步洗一遍。

相关笔记