Python 数据分析入门
从基础语法到 Pandas、可视化、Git 与 Marimo
笔记本风格 · 边学边练 · 12 章完整路径变量
变量就像贴了标签的收纳盒。你可以在盒子里放东西(赋值),通过标签找到里面的东西(使用变量),盒子里的东西还可以随时更换(重新赋值)。
📓 关于代码单元格
本网站的代码块模拟了笔记本工具(如 Marimo、Jupyter)的单元格样式。代码写在"单元格"里,点击运行或按 Shift+Enter 就能执行,结果直接显示在单元格下方。In [1] 表示第 1 个执行的单元格。在第10章你将学到 Marimo 这款现代化笔记本工具。
name = 小明"
age = 25
height = 1.75
is_student = True
print(name)
print(age)
print(height)
print(is_student)小明 25 1.75 True
age = 26
print(age)26
🔍 逐行解析
- name = "小明"等号是"赋值",把右边的值放进左边的变量。
"小明"是字符串。 - age = 25整数类型 int,存放整数。
- height = 1.75浮点类型 float,存放小数。
- is_student = True布尔类型 bool,只有
True和False,首字母大写。 - print(name)
print()是内置函数,把内容显示在屏幕上。
✏️ 小测验
1. 正确的变量赋值是?
2. age = 25 的数据类型是?
3. Python 布尔值是?
True/False 是关键字,不加引号。函数
函数就像食谱。给它食材(参数),按固定步骤操作(函数体),最后给你一道菜(返回值)。食谱可以反复使用。
def greet(name, greeting=你好"):
message = f{greeting},{name}!"
return message
print(greet(小明"))
print(greet(小红", 早上好"))你好,小明! 早上好,小红!
def calc_bmi(weight, height):
bmi = weight / (height ** 2)
return round(bmi, 1)
my_bmi = calc_bmi(65, 1.75)
print(fBMI指数:{my_bmi}")BMI指数:21.2
🔍 逐行解析
- def greet(...):
def定义函数,函数名后有括号和冒号。 - greeting="你好"默认参数,不传就用默认值。
- (缩进)message = ...函数体必须缩进 4 个空格。
- f"{greeting},{name}!"f-string:字符串前加
f,花括号里放变量。 - return message
return把结果返回给调用者。
✏️ 小测验
1. 定义函数的关键字是?
def 是 define 的缩写。2. 返回结果用哪个关键字?
return 返回值;print 只是显示。3. def f(x, y=0) 中 y 是?
循环
循环就像跑步机——重复同一个动作直到停止条件。for 是"固定次数重复",while 是"条件满足时重复"。
fruits = [苹果", 香蕉", 橙子"]
for fruit in fruits:
print(f我喜欢{fruit}")我喜欢苹果 我喜欢香蕉 我喜欢橙子
for i in range(1, 4):
print(f第{i}次")第1次 第2次 第3次
count = 0
while count < 3:
print(fcount = {count}")
count = count + 1count = 0 count = 1 count = 2
🔍 逐行解析
- for fruit in fruits:让 fruit 依次等于列表中每个元素。
- range(1, 4)生成 1,2,3(不包含4),左闭右开。
- while count < 3:条件为 True 就继续,False 就停止。
- count = count + 1必须改变条件,否则死循环。
✏️ 小测验
1. range(1,4) 循环几次?
2. while 何时停止?
3. ["苹果","香蕉"] 有几个元素?
条件判断与数据结构
条件判断就像路口的红绿灯——根据不同情况走不同的路。数据结构则是不同类型的收纳盒:列表是一排抽屉,字典是带标签的文件柜。
📋 本章你将学会
if / elif / else条件判断- 列表(list)的常用操作:索引、切片、追加
- 字典(dict)的键值对操作
- 字符串常用方法
score = 85
if score >= 90:
print(优秀")
elif score >= 60:
print(及格")
else:
print(不及格")及格
🔍 比较与逻辑运算符
| 运算符 | 含义 | 示例 |
|---|---|---|
== | 等于 | x == 5 |
!= | 不等于 | x != 0 |
> < >= <= | 大小比较 | age >= 18 |
and | 并且(两个都为True) | x > 0 and x < 10 |
or | 或者(任一为True) | x == 1 or x == 2 |
not | 取反 | not done |
fruits = [苹果", 香蕉", 橙子", 葡萄"]
# 索引:从0开始
print(fruits[0])
print(fruits[-1])
# 切片:[起始:结束],不包含结束
print(fruits[1:3])
# 追加和删除
fruits.append(芒果")
print(fruits)
fruits.remove(香蕉")
print(fruits)
# 列表长度
print(len(fruits))苹果 葡萄 ['香蕉', '橙子'] ['苹果', '香蕉', '橙子', '葡萄', '芒果'] ['苹果', '橙子', '葡萄', '芒果'] 4
student = {name": 小明", age": 25, city": 北京"}
# 按键取值
print(student[name"])
# 添加/修改键值对
student[major"] = 计算机"
student[age"] = 26
# 遍历字典
for key, value in student.items():
print(f{key}: {value}")小明 name: 小明 age: 26 city: 北京 major: 计算机
text = Hello, Python World "
print(text.strip())
print(text.upper())
print(text.replace(Python", Java"))
print(text.strip().split(, "))
print(len(text.strip()))Hello, Python World HELLO, PYTHON WORLD Hello, Java World ['Hello', 'Python World '] 19
💡 列表 vs 字典怎么选?
- 有顺序、用编号访问 → 列表
["苹果", "香蕉"] - 有名称、按键访问 → 字典
{"name": "小明"} - Pandas 的每一列就是一个有序列表,每行就是一个字典
✏️ 小测验
1. fruits = ["a","b","c"],fruits[0] 是?
2. 字典通过什么取值?
dict["key"] 按键名取值。3. " hi ".strip() 的结果是?
strip() 去掉首尾空白。API 调用
API 就像餐厅服务员。你不用自己去厨房做菜,告诉服务员要点什么(请求),服务员把菜端给你(返回数据)。requests 库就是沟通工具。
🌐 API 调用流程
- 发送请求:
requests.get(url) - 检查状态:
status_code为 200 表示成功 - 解析数据:
response.json()转成字典 - 提取信息:
data["键名"]
requests 库,在 Marimo/Jupyter 中也是这样写。输出为预计算结果。import requests
url = https://jsonplaceholder.typicode.com/posts/1"
response = requests.get(url)
if response.status_code == 200:
data = response.json()
print(状态码:", response.status_code)
print(文章ID:", data[id"])
print(标题:", data[title"])
print(正文:", data[body"][:50] + ...")
else:
print(请求失败,状态码:", response.status_code)状态码: 200 文章ID: 1 标题: sunt aut facere repellat provident occaecati excepturi optio reprehenderit 正文: quia et suscipit suscipit recusandae consequuntur ...
🔍 逐行解析
- import requests导入库,使用前需
pip install requests。 - requests.get(url)发送 GET 请求,返回 Response 对象。
- response.status_code200 成功,404 未找到,500 服务器错误。
- response.json()把 JSON 转成 Python 字典。
- data["body"][:50]切片取前 50 个字符。
✏️ 小测验
1. 发送 GET 请求用?
requests.get() 是最常用的 GET 请求方法。2. 解析 JSON 用?
.json() 把 JSON 字符串解析为字典。3. 状态码 200 表示?
数据读取与 DataFrame
Pandas 是 Python 数据分析的"瑞士军刀"。DataFrame 就像一张 Excel 表格——有行有列,每列有名字,每行有编号。read_csv 就是把文件读成这样的表格。
📦 安装与导入
在 Marimo/Jupyter 中,导入 pandas 的惯例写法:
📂 文件路径:绝对路径 vs 相对路径
读取文件前需要告诉 Python 文件在哪里。路径有两种写法:
- 相对路径:相对于当前工作目录,如
"data/iris.csv"。.表示当前目录,..表示上一级目录。推荐使用,换台电脑也能运行。 - 绝对路径:从根目录开始的完整路径,如
"/home/user/project/data/iris.csv"(Windows 为"C:\\Users\\...\\iris.csv")。只在当前电脑有效。
import os; os.getcwd() 查看当前工作目录,确保相对路径的起点正确。import pandas as pd
# 从本地 CSV 文件读取
# df = pd.read_csv("data.csv")
# 也可以直接从 URL 读取
url = https://raw.githubusercontent.com/mwaskom/seaborn-data/master/iris.csv"
df = pd.read_csv(url)
# 查看前5行
df.head()| sepal_length | sepal_width | petal_length | petal_width | species | |
|---|---|---|---|---|---|
| 0 | 5.1 | 3.5 | 1.4 | 0.2 | setosa |
| 1 | 4.9 | 3.0 | 1.4 | 0.2 | setosa |
| 2 | 4.7 | 3.2 | 1.3 | 0.2 | setosa |
| 3 | 4.6 | 3.1 | 1.5 | 0.2 | setosa |
| 4 | 5.0 | 3.6 | 1.4 | 0.2 | setosa |
# 形状:(行数, 列数)
print(形状:", df.shape)
# 列名
print(列名:", df.columns.tolist())
# 数据类型
print(df.dtypes)
# 详细信息:类型、非空值数量、内存占用
df.info()
# 基本统计信息
df.describe()形状: (150, 5) 列名: ['sepal_length', 'sepal_width', 'petal_length', 'petal_width', 'species'] sepal_length float64 sepal_width float64 petal_length float64 petal_width float64 species object dtype: object
<class 'pandas.core.frame.DataFrame'> RangeIndex: 150 entries, 0 to 149 Data columns (total 5 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 sepal_length 150 non-null float64 1 sepal_width 150 non-null float64 2 petal_length 150 non-null float64 3 petal_width 150 non-null float64 4 species 150 non-null object dtypes: float64(4), object(1) memory usage: 6.0+ KB
| sepal_length | sepal_width | petal_length | petal_width | |
|---|---|---|---|---|
| count | 150.00 | 150.00 | 150.00 | 150.00 |
| mean | 5.84 | 3.06 | 3.76 | 1.20 |
| std | 0.83 | 0.44 | 1.77 | 0.76 |
| min | 4.30 | 2.00 | 1.00 | 0.10 |
| max | 7.90 | 4.40 | 6.90 | 2.50 |
🔍 常用读取参数
- pd.read_csv("f.csv")读取逗号分隔文件
- sep="\t"指定分隔符(如制表符)
- header=None文件没有列名时使用
- skiprows=2跳过前2行
- pd.read_excel("f.xlsx")读取 Excel 文件(需
pip install openpyxl)
# 将 DataFrame 保存为 CSV
df.to_csv(output.csv", index=False)
# index=False 表示不写入行号(推荐)
# 也可以保存为 Excel
# df.to_excel("output.xlsx", index=False)# 文件已保存到当前工作目录 # 用 index=False 避免多出一列无意义的行号
to_csv() 保存,方便分享或下次继续分析。读取用 read_csv(),写入用 to_csv(),方向相反。✏️ 小测验
1. pandas 的惯例导入别名是?
import pandas as pd。2. 查看 DataFrame 前5行用?
.head() 默认显示前5行,可传参数指定数量。3. df.shape 返回什么?
.shape 返回元组 (行数, 列数)。数据筛选与变换
数据筛选就像在 Excel 中用筛选器——只看满足条件的行。数据变换则是添加计算列或排序,让数据变成你需要的样子。
# 选择一列(返回 Series)
print(df[species"].head())
# 选择多列(返回 DataFrame)
df[[sepal_length", species"]].head()0 setosa 1 setosa 2 setosa 3 setosa 4 setosa Name: species, dtype: object
| sepal_length | species | |
|---|---|---|
| 0 | 5.1 | setosa |
| 1 | 4.9 | setosa |
| 2 | 4.7 | setosa |
| 3 | 4.6 | setosa |
| 4 | 5.0 | setosa |
# 筛选花萼长度大于5.0的行
long_sepal = df[df[sepal_length"] > 5.0]
print(f筛选后行数:{len(long_sepal)}")
# 多条件筛选:用 &(且)和 |(或)
setosa_long = df[(df[species"] == setosa") & (df[sepal_length"] > 5.0)]
print(fsetosa且长萼片:{len(setosa_long)}行")
# 用 .loc 同时筛选行和列
df.loc[df[species"] == virginica", [petal_length", petal_width"]].head()筛选后行数:118 setosa且长萼片:7行
| petal_length | petal_width | |
|---|---|---|
| 100 | 6.0 | 2.5 |
| 101 | 5.1 | 1.9 |
| 102 | 5.9 | 2.1 |
| 103 | 5.6 | 1.8 |
| 104 | 5.8 | 2.2 |
# 筛选物种在指定列表中的行
# 等价于多个 == 用 | 连接,但更简洁
selected = df[df["species"].isin(["setosa", "virginica"])]
print(selected["species"].value_counts())species setosa 50 virginica 50 Name: count, dtype: int64
isin() 适合"值在这几个之中"的筛选场景,比写一长串 | 条件更清晰。# 添加新列:花萼面积
df[sepal_area"] = df[sepal_length"] * df[sepal_width"]
# 按花萼面积降序排列,取前3行
top3 = df.sort_values(sepal_area", ascending=False).head(3)
top3[[species", sepal_length", sepal_width", sepal_area"]]| species | sepal_length | sepal_width | sepal_area | |
|---|---|---|---|---|
| 131 | virginica | 7.9 | 3.8 | 30.02 |
| 117 | virginica | 7.7 | 3.8 | 29.26 |
| 118 | virginica | 7.7 | 2.6 | 20.02 |
💡 方法链(Chaining)
Pandas 支持把多个操作连起来写,像流水线一样:
(df[df[species"] == setosa"]
.sort_values(petal_length")
.head(3)
[[sepal_length", petal_length"]])
用括号包裹可以换行写,更清晰。
# 直接赋值会修改原 DataFrame
df["sepal_area"] = df["sepal_length"] * df["sepal_width"]
# assign 返回新 DataFrame,不修改原数据,适合链式调用
result = (df[df["species"] == "setosa"]
.assign(petal_ratio=df["petal_length"] / df["petal_width"])
.sort_values("petal_ratio", ascending=False)
.head(3))
result[["species", "petal_length", "petal_width", "petal_ratio"]]| species | petal_length | petal_width | petal_ratio | |
|---|---|---|---|---|
| 42 | setosa | 1.3 | 0.2 | 6.50 |
| 22 | setosa | 1.0 | 0.2 | 5.00 |
| 44 | setosa | 1.9 | 0.4 | 4.75 |
df["新列"] = ... 直接修改原表;df.assign(新列=...) 返回副本不改动原表,在方法链中更安全。import numpy as np
# 创建含缺失值的数据
demo = pd.DataFrame({
name": [小明", 小红", 小刚", 小丽"],
score": [90", 85", np.nan, 78"], # 分数是字符串+缺失
age": [20, 21, np.nan, 22]
})
print(=== 转换前 ===")
print(demo.dtypes)
# astype 转换类型(先填缺失值再转)
demo[score"] = demo[score"].fillna(0).astype(float)
# 缺失值处理
print(\n=== 缺失值统计 ===")
print(demo.isnull().sum())
# dropna 删除含缺失值的行
demo_clean = demo.dropna(subset=[age"])
print(f\n删除前:{len(demo)}行,删除后:{len(demo_clean)}行")
# fillna 填充缺失值
demo[age"] = demo[age"].fillna(demo[age"].mean())
demo=== 转换前 === name object score object age float64 dtype: object === 缺失值统计 === name 0 score 1 age 1 dtype: int64 删除前:4行,删除后:3行
| name | score | age | |
|---|---|---|---|
| 0 | 小明 | 90.0 | 20.0 |
| 1 | 小红 | 85.0 | 21.0 |
| 2 | 小刚 | 0.0 | 21.0 |
| 3 | 小丽 | 78.0 | 22.0 |
🔍 缺失值与类型转换速查
- df.isnull().sum()统计每列缺失值数量
- df.dropna()删除含缺失值的行(
subset=["列"]指定列) - df["列"].fillna(值)填充缺失值(可用均值、中位数等)
- df["列"].astype(float)转换数据类型(如字符串转数字)
- np.nan表示缺失值(Not a Number)
✏️ 小测验
1. 选择多列应该用?
df[["a","b"]],注意双层方括号。2. 多条件"且"用什么符号?
& 表示"且",| 表示"或"。3. 降序排列用?
ascending=False 表示降序。分组聚合与合并
分组聚合就像按班级统计平均分——先把学生按班级分组(groupby),再对每组计算统计量(mean/count/sum)。合并则是把两张表按共同列拼起来,像 Excel 的 VLOOKUP。
# 按物种分组,计算每组的平均值
df.groupby(species").mean()| species | sepal_length | sepal_width | petal_length | petal_width | sepal_area |
|---|---|---|---|---|---|
| setosa | 5.01 | 3.43 | 1.46 | 0.25 | 17.26 |
| versicolor | 5.94 | 2.77 | 4.26 | 1.33 | 16.53 |
| virginica | 6.59 | 2.97 | 5.55 | 2.03 | 19.69 |
# 对不同列用不同聚合方式
summary = df.groupby(species").agg(
平均花萼长度=(sepal_length", mean"),
最大花瓣长度=(petal_length", max"),
样本数量=(sepal_length", count")
).reset_index() # reset_index 把分组列从索引变回普通列
summary| species | 平均花萼长度 | 最大花瓣长度 | 样本数量 |
|---|---|---|---|
| setosa | 5.01 | 1.9 | 50 |
| versicolor | 5.94 | 5.1 | 50 |
| virginica | 6.59 | 6.9 | 50 |
# 统计每个物种有多少条记录
df[species"].value_counts()species setosa 50 versicolor 50 virginica 50 Name: count, dtype: int64
# 创建两张表
orders = pd.DataFrame({
order_id": [1, 2, 3],
customer_id": [101, 102, 101],
amount": [99, 199, 50]
})
customers = pd.DataFrame({
customer_id": [101, 102, 103],
name": [小明", 小红", 小刚"]
})
# 按 customer_id 合并(类似 VLOOKUP)
pd.merge(orders, customers, on=customer_id", how=left")| order_id | customer_id | amount | name | |
|---|---|---|---|---|
| 0 | 1 | 101 | 99 | 小明 |
| 1 | 2 | 102 | 199 | 小红 |
| 2 | 3 | 101 | 50 | 小明 |
🔍 merge 的 how 参数
- how="inner"只保留两表都有的匹配(默认)
- how="left"保留左表所有行,右表无匹配填 NaN
- how="right"保留右表所有行
- how="outer"保留两表所有行
📐 什么是整洁数据?
Pandas 和大多数可视化工具都要求数据是"整洁的"。整洁数据有三条原则:
- 每行是一个观测(一个实体的一组测量值)
- 每列是一个变量(一个测量特征)
- 每个单元格是一个值(不混合多个信息)
不整洁的数据常见问题:年份作为列名(宽格式)、一个单元格里塞了多个值。用 melt 和 pivot 可以在宽格式和长格式之间转换。
# 宽格式:年份作为列名,不利于分析
sales_wide = pd.DataFrame({
product": [A", B", C"],
2023": [100, 200, 150],
2024": [120, 210, 180]
})
print(=== 宽格式 ===")
print(sales_wide)
# melt 转成长格式:年份变成一列,销量变成一列
sales_long = sales_wide.melt(
id_vars=[product"], # 保持不变的列
var_name=year", # 原列名放到新列 "year"
value_name=sales" # 原值放到新列 "sales"
)
print(\n=== 长格式(整洁数据)===")
print(sales_long)=== 宽格式 === product 2023 2024 0 A 100 120 1 B 200 210 2 C 150 180 === 长格式(整洁数据)=== product year sales 0 A 2023 100 1 B 2023 200 2 C 2023 150 3 A 2024 120 4 B 2024 210 5 C 2024 180
year 和 sales 各是一列,可以直接用于分组、绘图。这是 Pandas 最推荐的数据形状。# pivot 是 melt 的逆操作
sales_wide_again = sales_long.pivot(
index=product", # 行标识
columns=year", # 展开成列的值来源
values=sales" # 填充的数据
).reset_index() # 把 product 从索引变回普通列
sales_wide_again| product | 2023 | 2024 | |
|---|---|---|---|
| 0 | A | 100 | 120 |
| 1 | B | 200 | 210 |
| 2 | C | 150 | 180 |
🔍 数据重塑速查
- df.melt(id_vars, var_name, value_name)宽转长:多列合并成"键列+值列"
- df.pivot(index, columns, values)长转宽:一列的值展开成多列
- .reset_index()把索引变回普通列(groupby/pivot 后常用)
✏️ 小测验
1. groupby 后计算平均值用?
.mean() 计算平均值。2. 统计某列各值出现次数用?
.value_counts() 是最常用的计数方法。3. 两表合并用?
pd.merge() 按共同列合并两张表。数据可视化
数据可视化就是把数字变成图——条形图比表格更直观地看出大小,折线图展示趋势,散点图发现关系。选对图表类型,数据自己会"说话"。
📊 如何选择图表类型?
| 你想展示什么 | 推荐图表 | 示例场景 |
|---|---|---|
| 比较不同类别的大小 | 条形图/柱状图 | 各产品销售额对比 |
| 展示随时间变化的趋势 | 折线图 | 月度收入变化 |
| 发现两个变量的关系 | 散点图 | 身高与体重的关系 |
| 展示数据分布 | 直方图 | 学生成绩分布 |
| 展示部分占整体比例 | 饼图 | 市场份额 |
matplotlib、seaborn 或 altair 画图。下面用交互式图表展示每种图表的效果。import pandas as pd
# Pandas 内置绘图(需要 matplotlib)
# df.groupby("species")["petal_length"].mean().plot(kind="bar")
# 或用 altair(更美观)
# import altair as alt
# alt.Chart(df).mark_bar().encode(
# x="species", y="mean(petal_length)"
# )# 在 Marimo/Jupyter 中运行上面的代码即可生成柱状图 # 上方交互式图表展示了相同数据的可视化效果
💡 可视化最佳实践
- always add title and axis labels:每张图都要有标题和轴标签
- 从0开始柱状图:柱状图Y轴从0开始,避免误导
- 少用3D图和饼图:3D图难读,多类别饼图难分辨
- 颜色有意义:用颜色区分类别或数值大小,不是为了好看
- 保存高清图:
plt.savefig("fig.png", dpi=300)
✏️ 小测验
1. 展示月度销售趋势用什么图?
2. 发现两个变量关系用?
3. Python 最基础的绘图库是?
Marimo 工作流
Marimo 是一款响应式 Python 笔记本——改一个单元格,所有依赖它的单元格自动重新运行,就像 Excel 改了一个数字,引用它的公式自动更新。笔记本以纯 Python 存储,天生 Git 友好,还能一键变成应用。
🚀 为什么选择 Marimo?
Marimo 解决了 Jupyter 的几个经典痛点:隐藏状态导致结果不一致、.ipynb 是 JSON 难用 Git 管理、无法轻松分享为应用。
- 响应式:运行一个单元格,自动运行所有依赖它的单元格
- 纯 Python 存储:
.py文件,Git diff 清晰可见 - 可复现:无隐藏状态,删除单元格即删除其变量
- 可部署:一键作为脚本执行或 Web 应用分享
- 内置交互:滑块、下拉框、表格等 UI 元素,无需 ipywidgets
- 内置 SQL:原生支持 SQL 单元格
📦 安装 Marimo
.py 文件,而不是 .ipynb。可以直接用任何编辑器打开,也能用 python notebook.py 作为脚本执行。⚡ 响应式 = 自动依赖追踪
Marimo 静态分析每个单元格的定义(它创建了什么变量)和引用(它读取了什么变量),构建依赖图(DAG)。当一个单元格运行时,所有引用其变量的单元格自动重新运行。
import marimo as mo
import pandas as pd
# 单元格1:定义数据
df = pd.DataFrame({
姓名": [小明", 小红", 小刚"],
成绩": [85, 92, 78]
})| 姓名 | 成绩 | |
|---|---|---|
| 0 | 小明 | 85 |
| 1 | 小红 | 92 |
| 2 | 小刚 | 78 |
# 单元格2:引用 df(当单元格1变化时自动重新运行)
平均分 = df[成绩"].mean()
print(f班级平均分:{平均分:.1f}")班级平均分:85.0
# 单元格3:引用平均分(当单元格2变化时自动重新运行)
mo.md(f## 班级平均分是 **{平均分:.1f}** 分")## 班级平均分是 85.0 分
🔍 关键规则
- 运行单元格1Marimo 自动运行单元格2和3,因为它们依赖 df 和平均分
- 删除单元格1df 从内存中删除,单元格2和3自动标记为过时
- 全局变量名唯一每个全局变量只能由一个单元格定义,避免冲突
- 下划线前缀 = 局部变量
_temp只在当前单元格可见,不触发其他单元格重跑
🎛️ 无需 ipywidgets
Marimo 内置了丰富的 UI 组件,交互即响应——拖动滑块,引用其值的单元格自动更新。
import marimo as mo
# 创建一个滑块
threshold = mo.ui.slider(0, 100, value=60, label=及格线")
threshold[滑块组件:及格线 = 60] ← 拖动时下方单元格自动更新
# 引用 threshold.value —— 滑块变化时自动重跑
及格人数 = (df[成绩"] >= threshold.value).sum()
mo.md(f及格线以上有 **{及格人数}** 人")及格线以上有 2 人
📋 常用 UI 组件
| 组件 | 代码 | 用途 |
|---|---|---|
| 滑块 | mo.ui.slider() | 选择数值范围 |
| 下拉框 | mo.ui.dropdown() | 选择单个选项 |
| 复选框 | mo.ui.checkbox() | 布尔开关 |
| 文本输入 | mo.ui.text() | 输入文字 |
| 表格 | mo.ui.table() | 可交互筛选的数据表 |
| 数字输入 | mo.ui.number() | 输入精确数值 |
| 特性 | Jupyter | Marimo |
|---|---|---|
| 文件格式 | .ipynb(JSON) | .py(纯 Python) |
| 执行模型 | 手动按顺序运行 | 响应式自动运行依赖单元格 |
| 隐藏状态 | 容易产生不一致 | 无隐藏状态,删除即清除 |
| Git 友好 | 差(JSON diff 难读) | 好(纯 Python diff 清晰) |
| UI 交互 | 需安装 ipywidgets | 内置组件 |
| 部署为应用 | 需额外工具 | marimo run 一键部署 |
| 作为脚本执行 | 需 jupytext 转换 | python file.py 直接运行 |
| SQL 支持 | 需 %%sql 魔法命令 | 原生 SQL 单元格 |
| 快捷键 / 命令 | 功能 |
|---|---|
| Ctrl+Enter | 运行当前单元格 |
| Shift+Enter | 运行单元格并跳到下一个 |
| Ctrl+B | 在下方插入单元格 |
| Ctrl+/ | 注释/取消注释 |
marimo edit | 启动编辑器 |
marimo run file.py | 作为 Web 应用运行 |
python file.py | 作为普通脚本执行 |
marimo export html file.py | 导出为静态 HTML |
- 一个全局变量只在一个单元格定义:这是 Marimo 的强制规则,也是好的编程习惯
- 用下划线前缀创建临时变量:
_temp_df不会触发其他单元格重跑 - 不要跨单元格修改变量:在定义它的单元格内完成修改,或创建新变量
- 用
mo.md()写 Markdown:Marimo 没有单独的 Markdown 单元格,用mo.md()在代码单元格中写笔记 - 大型计算用 Lazy 模式:设置中开启 Lazy,单元格只标记为过时而非自动运行
- 用函数封装临时逻辑:避免全局变量过多
list.append() 或 df["col"] = ...)。如果需要修改数据,在定义它的单元格内修改,或创建新变量。✏️ 小测验
1. Marimo 笔记本保存为什么格式?
2. 运行一个单元格时,Marimo 会自动做什么?
3. 如何让变量只在当前单元格可见?
Git 版本控制
Git 就像代码的"时光机"——记录每次修改,随时能回到任意版本。GitHub 则是"云盘",把代码备份到网上,还能和别人协作。
📦 为什么需要版本控制?
- 回溯历史:改坏了能回到上一个能跑的版本
- 备份同步:代码在 GitHub 上,换电脑也不怕
- 多人协作:各改各的,Git 自动合并
- 实验自由:开分支随便试,不影响主线代码
| 概念 | 类比 | 说明 |
|---|---|---|
| Repository(仓库) | 项目文件夹 | 被 Git 管理的项目目录 |
| Commit(提交) | 存档点 | 一次快照,记录"谁在什么时候改了什么" |
| Push(推送) | 上传到云盘 | 把本地提交传到 GitHub |
| Pull(拉取) | 从云盘下载 | 获取别人的最新修改 |
| Branch(分支) | 平行宇宙 | 在分支上实验,不影响主线 |
🔧 .gitignore 文件
在项目根目录创建 .gitignore,告诉 Git 哪些文件不要备份:
# 数据文件(太大不上传)
*.csv
*.xlsx
data/
# Jupyter 检查点
.ipynb_checkpoints/
# Marimo
.marimo/
# Python 缓存
__pycache__/
*.pyc
# 环境文件
.env
venv/
💡 Git + Marimo 工作流
- Marimo 笔记本是纯
.py文件,Git diff 能清晰看到代码变更 - 大文件(数据集)放
.gitignore,用云盘或数据平台分享 - 每次完成一个分析步骤就 commit 一次,写清楚说明
- 在 GitHub 上
.py文件直接可读,无需额外渲染插件
⚠️ 什么是合并冲突?
当两个人修改了同一个文件的同一行,Git 无法自动决定保留哪个版本,就会产生"合并冲突"(merge conflict)。用 git pull 拉取别人的更改时可能遇到。
Git 会在冲突文件中插入特殊标记:
<<<<<<< HEAD
print("我的版本") # 你本地的修改
=======
print("别人的版本") # 远程仓库的版本
>>>>>>>> origin/main
🔧 解决冲突的步骤
- 1. 打开冲突文件找到
<<<<<<<标记的位置 - 2. 决定保留哪个版本保留你的、保留别人的、或手动合并两者
- 3. 删除所有冲突标记删掉
<<<<<<< HEAD、=======、>>>>>>>>三行 - 4. 保存文件确认代码能正常运行
- 5. 重新提交
git add .→git commit -m "解决合并冲突"→git push
<<<<<<<、=======、>>>>>>>> 后再 commit。💡 避免冲突的最佳实践
- 每次开始工作前先
git pull,获取最新版本 - 频繁小步提交,不要攒一大堆改动再 push
- 和协作者沟通,避免同时编辑同一个文件的同一区域
- 不同功能用不同分支(
git branch),完成后再合并
✏️ 小测验
1. 提交代码的命令顺序是?
2. 下载 GitHub 项目用?
git clone 首次下载项目;git pull 获取后续更新。3. .gitignore 的作用是?
综合实战
从 API 获取数据 → 用 Pandas 整理 → 分组统计 → 可视化展示。一个完整的数据分析流程,把前面学的全部串起来!
import requests
import pandas as pd
# 步骤1:从API获取数据
url = https://jsonplaceholder.typicode.com/posts"
response = requests.get(url)
posts = response.json()
# 步骤2:转成DataFrame
df = pd.DataFrame(posts)
print(f共获取 {len(df)} 篇文章")
print(df.columns.tolist())
# 步骤3:添加新列——标题长度
df[title_length"] = df[title"].str.len()
# 步骤4:按用户ID分组统计
user_stats = df.groupby(userId").agg(
文章数=(id", count"),
平均标题长度=(title_length", mean")
).round(1)
print(user_stats.head())共获取 100 篇文章 ['userId', 'id', 'title', 'body']
| userId | 文章数 | 平均标题长度 |
|---|---|---|
| 1 | 10 | 37.9 |
| 2 | 10 | 39.5 |
| 3 | 10 | 35.4 |
| 4 | 10 | 35.0 |
| 5 | 10 | 38.2 |
# 步骤5:可视化(matplotlib 写法)
# import matplotlib.pyplot as plt
#
# user_stats["文章数"].plot(kind="bar")
# plt.title("各用户文章数量")
# plt.xlabel("用户ID")
# plt.ylabel("文章数")
# plt.tight_layout()
# plt.savefig("user_posts.png", dpi=300)
# plt.show()
print(上方柱状图展示了相同的统计结果")上方柱状图展示了相同的统计结果
🗺️ 概念对应表
| 代码 | 用到的知识 |
|---|---|
import requests, pandas as pd | 第2章 函数/导入库 + 第5章 API |
requests.get(url) | 第5章 API 调用 |
pd.DataFrame(posts) | 第6章 DataFrame |
df.groupby("userId").agg(...) | 第8章 分组聚合 |
df["title"].str.len() | 第4章 字符串方法 + 第7章 添加列 |
.plot(kind="bar") | 第9章 数据可视化 |
for ... in ... | 第3章 循环 |
🚀 下一步
恭喜你完成全部 12 章!你已经掌握了数据分析的核心技能。继续探索:
- 用
marimo edit创建你的第一个响应式笔记本 - 学习
numpy做数值计算 - 尝试
altair或plotly做交互式图表 - 在 GitHub 上创建你的第一个数据分析项目
pip install pandas requests matplotlib marimo