日本大学数据#
本章以日本大学的数据为例,依次演示 Polars 的几项基本操作:读取文件、列的增删与转换、行过滤、分组聚合,以及画图。这些操作如果脱离数据逐条讲解,容易显得零散;放到一份真实数据上连贯地操作一遍,各步骤之间的衔接会更清楚。这份数据包含日文列名、缺失值,以及需要转换类型的字段,恰好覆盖了实际工作中最常见的几类问题。
导入语句按惯例写成 import polars as pl,后续代码都使用 pl 这个别名;随后读取 CSV 文件,并显示前两行。
读取文件#
import polars as pl
import hvplot.polars
df = pl.read_csv('data/japanese_universities.csv')
df.head(2)
| code | name | name_jp | type | type_jp | address | postal_code | phone | state | state_jp | latitude | longitude | found | faculty_count | department_count | has_grad | has_remote | review_rating | review_count | difficulty_SD | difficulty_rank | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| i64 | str | str | str | str | str | str | str | str | str | str | f64 | f64 | str | i64 | i64 | bool | bool | f64 | f64 | f64 | str |
| 0 | "F101110100010" | "Hokkaido University" | "北海道大学" | "National" | "国立" | "北海道札幌市北区北8条西5丁目" | "060-0808" | "011-716-2111" | "Hokkai Do" | "北海道" | 43.070446 | 141.347153 | "1876-08" | 33 | 78 | true | false | 4.16 | 1389.0 | 60.4 | "A" |
| 1 | "F101110100029" | "Hokkaido University of Educati… | "北海道教育大学" | "National" | "国立" | "北海道札幌市北区あいの里5条3-1-3" | "002-8501" | "011-778-0206" | "Hokkai Do" | "北海道" | 43.170498 | 141.393753 | "1943-04" | 3 | 8 | true | false | 3.79 | 544.0 | 47.1 | "D" |
columns 给出所有列名,顺序与 CSV 文件中的一致。
列的信息#
df.columns
['',
'code',
'name',
'name_jp',
'type',
'type_jp',
'address',
'postal_code',
'phone',
'state',
'state_jp',
'latitude',
'longitude',
'found',
'faculty_count',
'department_count',
'has_grad',
'has_remote',
'review_rating',
'review_count',
'difficulty_SD',
'difficulty_rank']
列名与数据类型的对应关系可以用 schema 查看。它返回一个字典,键表示列名,值表示数据类型。由于 dtypes 只按列的顺序给出类型,名称与类型需要自行对照;schema 把两者放在一起,阅读时不必再逐项对齐,这也是它更适合检查列信息的原因。
df.schema
Schema([('', Int64),
('code', String),
('name', String),
('name_jp', String),
('type', String),
('type_jp', String),
('address', String),
('postal_code', String),
('phone', String),
('state', String),
('state_jp', String),
('latitude', Float64),
('longitude', Float64),
('found', String),
('faculty_count', Int64),
('department_count', Int64),
('has_grad', Boolean),
('has_remote', Boolean),
('review_rating', Float64),
('review_count', Float64),
('difficulty_SD', Float64),
('difficulty_rank', String)])
列操作#
不需要的列可以用 drop() 删除;它和 Polars 的大多数方法一样返回新表,原表保持不变,因此结果需要用一个变量保存。
df2 = df.drop('', 'code', 'name', 'type', 'state', 'postal_code', 'phone')
df2.head(2)
| name_jp | type_jp | address | state_jp | latitude | longitude | found | faculty_count | department_count | has_grad | has_remote | review_rating | review_count | difficulty_SD | difficulty_rank |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| str | str | str | str | f64 | f64 | str | i64 | i64 | bool | bool | f64 | f64 | f64 | str |
| "北海道大学" | "国立" | "北海道札幌市北区北8条西5丁目" | "北海道" | 43.070446 | 141.347153 | "1876-08" | 33 | 78 | true | false | 4.16 | 1389.0 | 60.4 | "A" |
| "北海道教育大学" | "国立" | "北海道札幌市北区あいの里5条3-1-3" | "北海道" | 43.170498 | 141.393753 | "1943-04" | 3 | 8 | true | false | 3.79 | 544.0 | 47.1 | "D" |
新增列或转换已有列用 with_columns(),它返回的同样是一张新表。下面的代码对 df2 做两种转换,结果保存为 df3:
found:由字符串转换为日期型,YYYY-MM形式的文本变成可以比较大小的日期值。type_jp和difficulty_rank:由字符串转换为类别型。类别型不会重复存储相同的字符串,因而更节省内存,分组时也不必反复比较字符串。
传给 with_columns() 的不是计算结果,而是“表示计算的表达式”,这与 Pandas 的赋值写法差别很大。关于表达式的细节,可以参见表达式一章。
df3 = df2.with_columns(
pl.col('found').str.to_date("%Y-%m"),
pl.col('type_jp', 'difficulty_rank').cast(pl.Categorical)
)
转换后的列可以用 select() 单独取出,以确认结果是否正确。与其他 Polars 方法一样,它返回新表,df3 本身不会被改动。
df3.select('name_jp', 'found', 'type_jp', 'difficulty_rank').head()
| name_jp | found | type_jp | difficulty_rank |
|---|---|---|---|
| str | date | cat | cat |
| "北海道大学" | 1876-08-01 | "国立" | "A" |
| "北海道教育大学" | 1943-04-01 | "国立" | "D" |
| "室蘭工業大学" | 1897-05-01 | "国立" | "F" |
| "小樽商科大学" | 1910-03-01 | "国立" | "C" |
| "帯広畜産大学" | 1941-04-01 | "国立" | "B" |
小技巧
select() 与 with_columns() 都用于列操作,区别只在输出:前者只保留选中的列,其余列全部丢弃;后者把新算出的列并入原表,未被操作的列原样保留。因此,需要换一张表时用前者,需要保留原表并追加列时用后者。
行过滤#
filter() 只返回满足条件的行,原表保持不变。下面的代码筛选偏差值(difficulty_SD)大于 65 的行,这些行对应偏差值较高的学校。
df3.filter(
pl.col('difficulty_SD') > 65
)
| name_jp | type_jp | address | state_jp | latitude | longitude | found | faculty_count | department_count | has_grad | has_remote | review_rating | review_count | difficulty_SD | difficulty_rank |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| str | cat | str | str | f64 | f64 | date | i64 | i64 | bool | bool | f64 | f64 | f64 | cat |
| "東京大学" | "国立" | "東京都文京区本郷7-3-1" | "東京都" | 35.714146 | 139.763214 | 1877-04-01 | 25 | 132 | true | false | 4.34 | 2206.0 | 70.5 | "S" |
| "一橋大学" | "国立" | "東京都国立市中2-1" | "東京都" | 35.694389 | 139.443649 | 1920-04-01 | 10 | 15 | true | false | 4.26 | 432.0 | 67.9 | "S" |
| "京都大学" | "国立" | "京都府京都市左京区吉田本町" | "京都府" | 35.026962 | 135.781967 | 1886-04-01 | 28 | 94 | true | false | 4.2 | 1434.0 | 65.6 | "S" |
| "国際教養大学" | "公立" | "秋田県秋田市雄和椿川字奥椿岱" | "秋田県" | 39.62706 | 140.181107 | 2004-04-01 | 2 | 4 | true | false | 4.18 | 100.0 | 69.7 | "S" |
| "慶應義塾大学" | "私立" | "東京都港区三田2-15-45" | "東京都" | 35.649147 | 139.742828 | 1890-01-01 | 28 | 59 | true | true | 4.19 | 2682.0 | 66.3 | "S" |
| "日本医科大学" | "私立" | "東京都文京区千駄木1-1-5" | "東京都" | 35.721233 | 139.758453 | 1904-04-01 | 2 | 2 | true | false | 3.99 | 31.0 | 70.0 | "S" |
| "早稲田大学" | "私立" | "東京都新宿区戸塚町1-104" | "東京都" | 35.710083 | 139.722244 | 1902-10-01 | 35 | 94 | true | true | 4.14 | 4280.0 | 65.7 | "S" |
| "国際基督教大学" | "私立" | "東京都三鷹市大沢3-10-2" | "東京都" | 35.686474 | 139.527481 | 1953-03-01 | 2 | 6 | true | false | 4.43 | 206.0 | 68.0 | "S" |
多个条件可以用 &(AND)、|(OR)、~(NOT)组合;每个条件都要用括号括起来,否则运算符的优先级会使表达式的含义与预期不符。下面的代码筛选位于大阪、且偏差值高于 55 的大学。
df3.filter(
(pl.col('difficulty_SD') > 55) & (pl.col('state_jp').str.starts_with("大阪"))
)
| name_jp | type_jp | address | state_jp | latitude | longitude | found | faculty_count | department_count | has_grad | has_remote | review_rating | review_count | difficulty_SD | difficulty_rank |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| str | cat | str | str | f64 | f64 | date | i64 | i64 | bool | bool | f64 | f64 | f64 | cat |
| "大阪大学" | "国立" | "大阪府吹田市山田丘1-1" | "大阪府" | 34.819496 | 135.521164 | 1917-04-01 | 26 | 75 | true | false | 4.12 | 1840.0 | 61.3 | "A" |
| "大阪医科薬科大学" | "私立" | "大阪府高槻市大学町2-7" | "大阪府" | 34.85186 | 135.624405 | 1904-05-01 | 6 | 9 | true | false | 4.11 | 9.0 | 57.0 | "B" |
| "関西大学" | "私立" | "大阪府吹田市山手町3-3-35" | "大阪府" | 34.769669 | 135.510162 | 1904-01-01 | 29 | 44 | true | false | 3.95 | 2661.0 | 55.6 | "B" |
所谓方法链,是指把多个方法依次写在同一条语句中的写法。由于每一步都返回新的数据表,整个预处理流程可以写成一串连续的方法调用,从上往下读就是执行顺序,不必为中间结果逐一命名。
这段代码对 df3 依次做:
filter():只保留type_jp为“国立”的行。sort():按偏差值降序排列。drop_nulls():去掉偏差值为 NULL 的行。select():只保留大学名和偏差值。head():取前 5 行。
(df3
.filter(pl.col.type_jp == '国立')
.sort('difficulty_SD', descending=True)
.drop_nulls('difficulty_SD')
.select('name_jp', 'difficulty_SD')
.head(5)
)
| name_jp | difficulty_SD |
|---|---|
| str | f64 |
| "東京大学" | 70.5 |
| "一橋大学" | 67.9 |
| "京都大学" | 65.6 |
| "東京工業大学" | 65.0 |
| "浜松医科大学" | 64.4 |
分组处理#
先查看 type_jp 的取值,结果只有国立、公立、私立三种,说明这一列适合作为分组键。
df3.select(pl.col('type_jp').unique())
| type_jp |
|---|
| cat |
| "国立" |
| "私立" |
| "公立" |
按分类列聚合时,需要 group_by() 与 agg() 配合使用:前者决定数据按什么分组,后者决定每组计算哪些统计量。
group_by():按指定列的值把数据分组。agg():对每组做聚合,例如计数、求和、求平均等。
这个例子用 pl.len() 统计每个分组的行数,这也是分组聚合最简单的用法。
df3.group_by('type_jp').agg(pl.len())
| type_jp | len |
|---|---|
| cat | u32 |
| "私立" | 626 |
| "国立" | 86 |
| "公立" | 101 |
除了计数,偏差值的最小值、中位数、平均值和最大值也可以在同一个 agg() 中一次算出。这样不必反复聚合再合并结果,输出列的名称则通过关键字参数指定。
(df3
.group_by('type_jp')
.agg(
count = pl.len(),
min = pl.col('difficulty_SD').min(),
median = pl.col('difficulty_SD').median(),
mean = pl.col('difficulty_SD').mean(),
max = pl.col('difficulty_SD').max(),
)
)
| type_jp | count | min | median | mean | max |
|---|---|---|---|---|---|
| cat | u32 | f64 | f64 | f64 | f64 |
| "私立" | 626 | 35.0 | 38.5 | 40.801034 | 70.0 |
| "公立" | 101 | 40.0 | 50.0 | 50.042391 | 69.7 |
| "国立" | 86 | 35.7 | 51.7 | 52.917073 | 70.5 |
画图#
Polars 本身不提供画图功能,需要借助外部库。安装 hvplot 之后,可以直接通过 .hvplot 生成交互式图表,不必先转换成 Pandas 的 DataFrame。要把点叠加到地图上,还需要安装 geoviews,由它提供地理坐标与投影支持。
conda install -c conda-forge hvplot geoviews
柱状图#
下面的代码按都道府县统计大学的数量,从多到少排序,再画成柱状图。画图方法都由 .hvplot 提供,返回的是一个可交互的图对象,而不是静态图片。
用
x和y指定轴标签。rot=90让 X 轴标签竖排,避免都道府县名相互重叠。frame_width=900把图宽设为 900 像素。
x_label = "都道府县"
y_label = "大学数量"
(df3
.group_by(pl.col("state_jp").alias(x_label))
.agg(
pl.len().alias(y_label)
)
.sort(y_label, descending=True)
.hvplot.bar(
x=x_label, y=y_label,
rot=90, frame_width=900
)
)
堆叠柱状图#
仅看总数并不能反映大学的构成。总数相同的都道府县,可能以国立大学为主,也可能几乎全是私立大学。这里按大学种类(国立、公立、私立)把柱子堆叠起来,代码与上面的例子有三点不同。
在
group_by()里加上type_jp,按“都道府县 × 种类”分组。排序依据是“每个都道府县的总数”,而不是单根柱子的高度;在表达式中用
over()做分组聚合即可实现(over() 的用法)。by="type_jp"按种类着色,stacked=True堆叠显示。
(df3
.group_by(
pl.col("state_jp").alias(x_label),
"type_jp"
)
.agg(
pl.len().alias(y_label)
)
.sort(pl.col(y_label).sum().over(x_label), descending=True)
.hvplot.bar(
x=x_label, y=y_label, by="type_jp",
stacked=True, rot=90,
frame_width=900, frame_height=450
)
)
最后把经纬度转换成点,按大学种类着色,再叠加到地图上,用来观察各都道府县的学校分布。
by="type_jp":按种类着色。hover_cols=['name_jp']:鼠标悬停时显示大学名称。tiles=True:铺上地图底图。
请注意不要同时指定 geo=True:在 geoviews 1.15 与 pyproj 3.8 的组合下,它会使投影计算直接报错;因此,只保留 tiles=True,地图即可正常显示。
df3.hvplot.points(
"longitude",
"latitude",
by="type_jp",
hover_cols=["name_jp"],
tiles=True,
frame_width=600,
)