
1. 别再问我为什么换掉Pandas了先花十分钟认识Polars先说一件挺现实的事情这两年只要我在技术群里提到数据处理几乎每三次讨论里就有一次会蹦出Polars这个名字。你要是还没用过它现在入局一点都不晚恰恰是最好的时候。Polars是什么一句话讲清楚它是一个用Rust编写的DataFrame库提供了和Pandas高度相似的API但底层是列式存储、多线程并行、惰性计算这套完全不同的引擎设计。你可以把它理解成“一辆换了发动机的跑车”——外观看起来还是你熟悉的仪表盘但踩下油门的那一瞬间提速完全不是一个量级。这篇入门教程解决什么问题就是让你从零开始花一顿午饭的工夫把Polars跑起来并搞清楚它和Pandas的核心差异在哪儿。无论你是被几千万行Excel逼到崩溃的运营同学还是每天清洗数据到想离职的数据分析师亦或是做机器学习特征工程的算法工程师这篇文章都能帮你找到一个更省心的数据工具。我不会废话连篇地复制官方文档而是按照我自己从Pandas迁移过来时的踩坑顺序带你走一遍Polars最核心的玩法创建数据、筛选过滤、分组聚合、惰性查询。这些都搞明白了你在Polars里就已经超过七成使用者了。2. Polars到底哪里比Pandas强我的真实体感2.1 性能差距不是玄学是架构决定的很多人一听到“Polars快”第一反应是“又是营销号标题党”。我第一次看到性能对比图的时候也是这个态度直到自己拿真实数据跑了一遍才彻底服气。Pandas最大的痛点在于它几乎是单线程运行的。你处理一列数据的时候其他列都在旁边闲着。而Polars从设计之初就把“利用所有CPU核心”当作头条目标。你的电脑是八核十六线程Polars就能让这十六个线程一起干活Pandas只能让其中一个忙碌剩下的看着。这就是最直观的差距来源。再往深一层说Polars的底层是基于Apache Arrow列式内存格式的。列式存储意味着当你只需要读某几列时Polars可以只加载这几列进内存而Pandas需要把整份数据从磁盘上拖起来。这就像你去图书馆找一本书Polars知道书在哪个架子的哪一排Pandas则要把整个图书馆翻个底朝天。数据量一上来这种差异就会从“有点慢”演变成“等到怀疑人生”。2.2 惰性计算带来的性能飞跃Polars真正让人惊艳的是它的惰性计算LazyFrame机制。这个概念看起来高大上我用一个生活场景来给你解释清楚。想象你去买菜Pandas的做事方式是先跑到A摊买土豆拎回家再跑到B摊买牛肉又拎回家然后跑到C摊买调料再次拎回家。每买一样东西就往返一次每一步都立即执行、立即返回结果。Polars的惰性模式则是你先在纸上列一个采购清单土豆、牛肉、调料然后等你确认“就这些了”的那一刻一口气规划出一条最优路线一次性把所有东西买齐。在实际数据处理里这意味着你写了五步操作Polars不会一步步傻乎乎地执行而是会先看看这五步里有哪些中间环节根本不需要直接把它砍掉。比如你先筛选了某个月份的数据后面又只需要三列那Polars就不会傻到把全部列都加载进来再筛选。这种“查询优化器”的存在让Polars在处理大规模数据集的时候有着恐怖的性能优势。2.3 内存占用更小大文件不再轻易OOM用过Pandas处理几个G的CSV文件的朋友应该都有过濒临崩溃的体验——内存占用动不动就十几个G电脑风扇狂转时间慢到像在看慢放视频运气不好直接内存溢出一切从头再来。Polars在这方面的表现要优雅得多。由于它的列式存储和高效的内存管理机制大多数场景下内存占用只有Pandas的三分之一到二分之一。我自己处理过一份大约3.8G的日志文件Pandas在这个任务上吃了大概14G内存最后还失败了换成Polars内存峰值在5G左右整个过程跑完用了不到三分钟。这是我最直观、最震撼的一次体验也是我下定决心把新项目切到Polars的导火索。2.4 潜力巨大的生态后发优势Polars还有一个隐藏优势是很多人会忽略的——它是“后发者”不需要背负历史包袱。Pandas发展这么多年API已经非常丰富但很多历史API设计在今天看来并不合理。Polars可以轻装上阵API设计更加一致、更加直观。比如Pandas里的apply同一个操作在不同场景下可能表现完全不同给初学者带来不少困惑。Polars里对这类问题做了更清晰的设计函数式编程的味道更浓。你写Polars的代码本质上是在组合一个个表达式expression而不是在不断地对DataFrame进行原地修改。这种风格一旦适应了写起来会非常流畅。3. 上手第一步环境准备和数据结构认知3.1 安装Polars一分钟搞定Polars的安装没有太多可以讲的直接pip一把梭pip install polars如果你打算同时体验Pandas和Polars之间的数据转换建议装全量扩展版本这样还能顺便解锁一些Excel、SQL等格式的支持pip install polars[all]装上之后验证一下版本确认一切正常import polars as pl print(pl.__version__)我自己目前用的是1.x的版本语法和0.20之后的版本基本一致。如果你之前在网上的老教程里看到过一些功能在新版本里如果报错通常是因为API改名了以官方文档为准。注意一点Polars对Python版本有一定要求太老的Python版本比如3.8以下就不要想了。建议用Python 3.9以上最好3.11以上性能上还有额外加成。3.2 DataFrame和Series的创建和Pandas很像但又有区别创建Polars的DataFrame有几种很常用的方式。第一种直接用一个字典来创建这是最直观的import polars as pl data { 姓名: [张三, 李四, 王五, 赵六], 部门: [技术部, 市场部, 技术部, 财务部], 薪资: [15000, 12000, 18000, 11000], 绩效: [88, 76, 92, 65], } df pl.DataFrame(data) print(df)输出看起来像是这样的shape: (4, 4) ┌──────┬────────┬───────┬──────┐ │ 姓名 ┆ 部门 ┆ 薪资 ┆ 绩效 │ ╞══════╪════════╪═══════╪══════╡ │ 张三 ┆ 技术部 ┆ 15000 ┆ 88 │ │ 李四 ┆ 市场部 ┆ 12000 ┆ 76 │ │ 王五 ┆ 技术部 ┆ 18000 ┆ 92 │ │ 赵六 ┆ 财务部 ┆ 11000 ┆ 65 │ └──────┴────────┴───────┴──────┘你可能会发现Polars打印DataFrame时会同时输出一个shape: (4, 4)告诉你这是4行4列的数据。这个细节在Jupyter Notebook里特别实用你一眼就能看清自己手里数据的长宽。另一种常见的创建方式是用一个列表的列表来生成df2 pl.DataFrame( [ [1, A, 10.5], [2, B, 20.5], [3, C, 30.5], ], schema[id, group, value], orientrow, )注意这里需要指定orientrow因为Polars默认会把每行解释成一列数据。很多新手第一次在这翻车输出结果奇奇怪怪多半就是忘记了这个参数。你也可以从已有的Pandas DataFrame转换过来import pandas as pd pandas_df pd.DataFrame({a: [1, 2, 3], b: [4, 5, 6]}) polars_df pl.from_pandas(pandas_df)反过来转换也很方便back_to_pandas polars_df.to_pandas()3.3 lazy()方法和LazyFrame的认知刚刚我们提到惰性计算现在就需要正式认识一下它了。当你需要处理一个复杂任务时建议使用lazy()方式。创建LazyFrame的方式也非常简单在DataFrame上调用.lazy()就好了lazy_df df.lazy()你也可以直接从文件读取时就启用惰性模式lazy_df pl.scan_csv(大文件.csv)注意这个scan_csv和read_csv的区别read_csv是立即把所有数据读进内存返回一个DataFramescan_csv只是“瞄一眼”文件结构返回一个LazyFrame真正执行要等后面的.collect()。这种模式的好处在于当你连着写多个操作时Polars可以整体优化执行计划而不是读一下、算一下、停一下。我强烈建议你从一开始就习惯这种写法一旦用顺手了你再回去写Pandas会觉得浑身难受。4. 核心操作实战筛选、列操作、分组和聚合4.1 列的选择和操作Polars的表达式哲学在Polars里选中某一列非常简单可以通过select方法配合pl.col()完成# 选择单列 df.select(姓名) df.select(pl.col(姓名)) # 选择多列 df.select([姓名, 薪资]) df.select([pl.col(姓名), pl.col(薪资)]) # 选择满足特定条件的列 df.select(pl.col(^薪.*$)) # 正则表达式匹配以薪开头的列这看起来和Pandas差不多但Polars更偏向于“表达式”的思维。你用pl.col(薪资)得到的并不是那一列数据本身而是一个“列表达式”它可以被放在各种操作里随意组合。比如df.select([ pl.col(姓名), (pl.col(薪资) * 1.1).alias(涨薪后薪资), ])这段代码把“薪资”这一列每个值乘以1.1然后把结果命名为“涨薪后薪资”。在Pandas里你需要先算出一个新列再通过字典的方式去重命名。Polars这种方式要直接得多。再比如字符串操作df.select([ pl.col(姓名).str.to_uppercase(), pl.col(部门).str.replace(部, ), ])列名后面用.str进入字符串命名空间用.dt进入时间处理命名空间用.arr进入数组处理命名空间。这种命名空间的划分非常规整学过一次就记住了不会出现Pandas那种同一个功能散落多处、找不到对应方法的情况。4.2 行的筛选filter的正确打开方式筛选行数据Polars用的是filter方法对应Pandas里的loc、query等一堆你根本记不全的写法。Polars的filter设计非常直观# 薪资大于等于15000的员工 df.filter(pl.col(薪资) 15000) # 同时满足多个条件 df.filter( (pl.col(薪资) 15000) (pl.col(绩效) 80) ) # 只要技术部的员工 df.filter(pl.col(部门) 技术部) # 部门为技术部或市场部的员工 df.filter(pl.col(部门).is_in([技术部, 市场部]))注意一个细节多个条件的组合用的是与、|或、~非每个条件都要用括号包起来。这是最容易犯的语法错误——忘了加括号Python会抱怨运算符优先级的问题。也可以不用pl.col()直接用字符串名字Polars支持这种简化写法df.filter((pl.col(薪资) 15000) (pl.col(绩效) 80)) # 等价于 df.filter(薪资 15000) # 不推荐还是老老实实用pl.col吧在实际操作中我更推荐统一使用pl.col()因为到了后面组合复杂逻辑、写自定义函数时这种写法的表达能力更强。还有个很常用到却容易被忽视的filter招式df.filter(pl.col(姓名).str.contains(张)) df.filter(pl.col(绩效).is_between(70, 90)) df.filter(pl.col(部门).ne(财务部)) # 不等于财务部str.contains用来模糊匹配is_between是区间匹配都能在关键时刻帮你省几行代码。4.3 新增列和修改列with_columns才是主角Pandas新增一列用的是df[新列名] 值这种原地赋值的方式。Polars中更推荐使用with_columns它强调的是“返回一个新的DataFrame”不修改原数据符合函数式编程的理念df df.with_columns([ (pl.col(薪资) * 12).alias(年薪), (pl.col(绩效) / 100).alias(绩效百分比), ])这样做的好处是代码更好维护。你每一步操作都返回新对象不会出现Pandas里常见的SettingWithCopyWarning那种让人摸不着头脑的警告。with_columns里也可以做条件逻辑比如根据绩效等级打标签df df.with_columns([ pl.when(pl.col(绩效) 85) .then(pl.lit(A)) .when(pl.col(绩效) 70) .then(pl.lit(B)) .otherwise(pl.lit(C)) .alias(绩效等级), ])这个pl.when().then().otherwise()的结构非常像SQL里的CASE WHEN表达式逻辑清晰只要你脑子里有SQL的概念这里基本零学习成本。如果你想给一列所有值做加减乘除运算直接用表达式就行df.with_columns([ (pl.col(薪资) 1000).alias(薪资加一千), (pl.col(薪资) / 10000).alias(薪资单位万), ])4.4 分组聚合group_by的真面目分组聚合是数据分析最常用的操作之一。Pandas里有groupbyPolars里是group_by用法上有些相似但Polars返回的是一个DataFrame而不是一个需要你再逐步处理的GroupBy对象。看个例子按部门统计平均薪资、最高绩效和人数result df.group_by(部门).agg([ pl.col(薪资).mean().alias(平均薪资), pl.col(绩效).max().alias(最高绩效), pl.len().alias(人数), ]) print(result)输出大概是shape: (3, 4) ┌────────┬──────────┬──────────┬──────┐ │ 部门 ┆ 平均薪资 ┆ 最高绩效 ┆ 人数 │ ╞════════╪══════════╪══════════╪══════╡ │ 技术部 ┆ 16500.0 ┆ 92 ┆ 2 │ │ 市场部 ┆ 12000.0 ┆ 76 ┆ 1 │ │ 财务部 ┆ 11000.0 ┆ 65 ┆ 1 │ └────────┴──────────┴──────────┴──────┘这里最需要留意的是agg里面接受的是一个列表列表里每一个元素都是一个聚合表达式。pl.len()算的是每个分组里的行数等效于Pandas里的size()。分组后还可以做排序比如按平均薪资降序排result.sort(平均薪资, descendingTrue)如果需要对多个列进行分组语法依然简洁df.group_by([部门, 绩效等级]).agg([ pl.col(薪资).sum().alias(薪资总和) ])4.5 排序和去重的常用细节排序在Polars里非常简单df.sort(薪资) # 默认升序 df.sort(薪资, descendingTrue) # 降序 df.sort([部门, 薪资], descending[False, True]) # 先按部门升序再按薪资降序去重方面有unique和drop_nulls两个常用方法df.unique(subset[部门]) # 按部门去重保留第一行 df.drop_nulls(subset[薪资]) # 删除薪资为空的行第5节开始前先给你扔张速查表把Pandas和Polars的关键API对应关系列出来帮助有pandas基础的朋友快速迁移功能PandasPolars读取CSVpd.read_csv(f.csv)pl.read_csv(f.csv)惰性读取CSV无直接对应pl.scan_csv(f.csv)选择列df[[a,b]]df.select([a,b])筛选行df[df[a] 1]df.filter(pl.col(a) 1)新增列df[c] ...df.with_columns(expression)分组聚合df.groupby(a).agg(...)df.group_by(a).agg(...)排序df.sort_values(a)df.sort(a)去重df.drop_duplicates(a)df.unique(subset[a])查看列名df.columnsdf.columns查看统计信息df.describe()df.describe()看完这个表你会发现绝大多数场景下不需要重头学你只需要把思维从“索引和原地操作”切换到“表达式和函数式操作”基本就畅通无阻了。5. 惰性计算实战用LazyFrame写出高性能查询5.1 理解LazyFrame的执行机制一个小例子给你讲透前面说了很多惰性计算的优势现在直接上一个可以手动玩的例子。假设我们要从一个大文件里筛选出“市场部”员工并且只需要姓名和年薪两列。用Polars的惰性模式写result ( pl.scan_csv(员工数据.csv) # 惰性读取不真正加载 .filter(pl.col(部门) 市场部) # 记录过滤条件 .select([姓名, 年薪]) # 记录选择的列 .collect() # 此刻才真正执行 )在collect()之前Polars只是把操作记录在了一个查询计划里真正执行时Polars会自动优化。比如因为只需要两列Polars在读取CSV的时候可能就不加载其他列了这样磁盘I/O和内存开销都会大大降低。你可以通过explain()来查看Polars的查询计划Query Plan这是在Pandas里完全不可能做到的( pl.scan_csv(员工数据.csv) .filter(pl.col(部门) 市场部) .select([姓名, 年薪]) .explain() )输出的是一份执行计划有点像数据库的EXPLAIN输出。虽然对初学者来说这些信息有点抽象但你能从中感受到Polars在背后做了不少优化工作。5.2 惰性模式和及时模式怎么选择我的建议我把我的使用习惯分享给各位可以作为参考如果你只是做一次性的探索性分析数据量又不大几十万行以内直接用及时模式DataFrame就够了写法更简单直接。如果你的数据量在百万行以上或者你的处理步骤超过三四步优先用惰性模式LazyFrame。它不仅能优化执行计划写起来其实也没有多麻烦就是最后多一个.collect()的事。如果数据量和复杂步骤同时存在惰性模式基本就是你唯一的选择了因为它能在有限的内存里把任务啃下来。每次用完惰性模式要记得调用.collect()。忘记了也不报错但你会看到结果始终是“懒懒的”没有任何输出。5.3 collect的几种变化特殊场景下的好帮手collect()是惰性的收口操作但Polars还提供了一些变体适配不同的场景。# 常规执行 df.lazy().filter(...).collect() # 不收集所有列只取前500行用于快速探查数据 df.lazy().filter(...).collect(n_rows500) # 原地流式处理适合内存不够但数据量巨大的场景 df.lazy().filter(...).collect(streamingTrue)collect(n_rows500)在调试时特别有用你不需要加载全部数据先快速看看前500行结果是否符合预期。collect(streamingTrue)则是处理超大数据集的利器它会用流式方式一块一块地处理数据不会一次性把所有数据压进内存。6. 用Polars做真实的数据分析一个端到端的案例6.1 场景设定和数据准备前面的知识点单独看都不难难的是把它们串起来。我在这里准备了一个贴近日常工作的小案例带你走一遍完整的分析流程。假设你手上有一份电商订单表orders.csv字段包括order_id订单编号user_id用户编号category商品类目amount订单金额status订单状态completed, pending, cancelledcreated_at下单时间我们要完成这些分析目标找出所有已完成订单中金额排名前10的订单按商品类目统计已完成订单的销售总额、订单量、平均客单价找出消费总额最高的前5位用户按月统计销售总额的变化趋势6.2 用惰性模式打包这些操作import polars as pl lazy_query ( pl.scan_csv(orders.csv) .filter(pl.col(status) completed) ) # 分析1金额排名前10的已完成订单 top10_orders ( lazy_query .sort(amount, descendingTrue) .head(10) .select([order_id, user_id, category, amount]) .collect() ) print(top10_orders)可以看到我们把lazy_query这个公共的LazyFrame定义了不止一次使用。它每次被collect()的时候都会重新执行一次查询计划。这个模式非常舒服——公共查询只写一遍后面各自接不同的下游操作。接着是分析2按类目聚合category_summary ( lazy_query .group_by(category) .agg([ pl.col(amount).sum().alias(销售总额), pl.len().alias(订单量), (pl.col(amount).sum() / pl.len()).alias(平均客单价), ]) .sort(销售总额, descendingTrue) .collect() ) print(category_summary)再来看分析3找消费前5的用户top_users ( lazy_query .group_by(user_id) .agg(pl.col(amount).sum().alias(总消费)) .sort(总消费, descendingTrue) .head(5) .collect() ) print(top_users)分析4稍微复杂一点需要提取月份字段后做月维度聚合monthly_trend ( lazy_query .with_columns([ pl.col(created_at).str.slice(0, 7).alias(month), ]) .group_by(month) .agg(pl.col(amount).sum().alias(月度销售额)) .sort(month) .collect() ) print(monthly_trend)注意这里created_at是字符串类型我用str.slice(0, 7)把前7个字符年-月提取出来作为月份字段。如果created_at是真正的日期时间类型需要用.dt.truncate(1mo)或者.dt.to_string(%Y-%m)等时间处理方法。6.3 案例总结和历史数据下的性能感受这套流程跑完你会发现整个分析过程只写了不到三十行代码但完成了四个维度的统计分析。我认为最值得表扬的是整个过程可读性非常高每一步操作都在清清楚楚地表达它的意图就像是在读一段流畅的英文句子。如果这份订单数据刚好有几百万行你可以直接感受一下Polars和Pandas的差异。我第一次跑类似案例的时候Pandas版的脚本需要约40秒才能完成全部四个分析而Polars版的脚本只用了约5秒。核心原因就是Polars把能够并行处理的聚合操作都并行化了而Pandas只能一步步排队执行。7. 你一定会遇到的几个坑以及排查技巧7.1 别把Pandas的apply习惯带到PolarsPandas的df.apply(func, axis1)可以让你按行执行自定义函数用起来非常灵活。但在Polars里如果你大量使用apply性能优势基本就没了。Polars更推荐的做法是使用map_elements或者尽可能用内置表达式# 不推荐会拖慢速度 df.with_columns([ pl.col(薪资).map_elements(lambda x: x * 1.1).alias(涨薪) ]) # 推荐用表达式一步到位 df.with_columns([ (pl.col(薪资) * 1.1).alias(涨薪) ])记住一个原则能直接用表达式做的事情绝对不要用自定义函数。表达式是矢量化的性能最高自定义函数是逐元素执行的相当于回到了Python循环的节奏。7.2 字符串列名和pl.col混用容易造成混淆Polars支持直接使用字符串列名也支持pl.col(列名)。在简单的单条件筛选里这两种写法没差别。但当表达式嵌套时字符串写法往往会导致代码可读性下降甚至出现歧义。我见过有的新手这样写df.filter(pl.col(部门) 技术部).select(姓名, 薪资)这样没问题但一旦条件复杂一点df.filter( (pl.col(部门) 技术部) | (pl.col(绩效) 90) )这时候再混合字符串写法和pl.col写法代码就会显得混乱。建议一开始就统一用pl.col等代码跑通之后再决定要不要精简成字符串形式。7.3 忘记collect导致结果一直不出来惰性模式太顺手了以至于有些朋友跑完查询发现变量明明赋值了打印出来却是空的。原因大概率是忘了在末尾加.collect()。这里有一个检查清单供你排查确认你用的是pl.scan_csv还是pl.read_csv。如果是前者就是惰性模式必须调用collect()才会执行。确认你有没有把结果正确赋值给变量。有时候你调用了collect()但忘了result 结果当然找不到。确认你是在正确的对象上调用的collect()。有人会不小心在已经collect后的DataFrame上再次调用.lazy()这样又回到惰性状态了。7.4 类型推断的坑Polars读取CSV时会自动推断每列的数据类型。这个推断大多数时候是对的但偶尔会出现意外情况。比如一列数据在文件前几千行都是整数但后面出现了一个小数Polars可能会把它推断为Int64然后遇到小数时炸掉。解决办法是在读取时手动指定schemapl.read_csv(orders.csv, schema{ order_id: pl.Utf8, amount: pl.Float64, created_at: pl.Utf8, })此外Polars里把对象列当成数值列处理或者反过来也是常见的报错来源。解决办法是用.cast()显式转换类型df.with_columns([ pl.col(amount).cast(pl.Float64) ])7.5 断言Elemwise vs Reduction还有一个隐蔽的小坑就是某些聚合函数放在错误的位置会报错。例如pl.col(amount).sum()是聚合操作它会产生一个标量。如果你把它放在with_columns里Polars会不确定你的意图——你是要一列相同的总和值还是只想要一个标量通常来说聚合操作应该放在agg里逐行操作放在with_columns里。如果你真的需要在每一行都显示汇总值可以使用over来实现窗口函数的效果df.with_columns([ pl.col(amount).sum().over(部门).alias(部门总金额) ])这样每一行都会显示它所属部门的金额总和。这个功能非常强大而且写法很直观值得记下来。8. 后续学什么以及我的学习路径建议到这里你已经掌握了Polars的常用核心操作。做入门教程我认为这些内容已经够你应对绝大多数日常工作场景。但如果想往深走我建议按下面的顺序继续推进第一优先级学习join的各种用法。数据处理中表连接太常见了Polars的join支持左连接、右连接、内连接、全连接、交叉连接以及键匹配的高级写法。我当时学Pandas时就在各种how参数上栽过跟头Polars的join逻辑更清晰估计会顺利很多。第二优先级深入研究over窗口函数。它能实现类似SQL的窗口分析能力比如排名、累计求和、环比同比等。这个技能在数据分析岗面试里几乎是必考内容也是区分操作水平的试金石。第三优先级了解Polars的I/O扩展能力。除了CSV你还可以用它读取Parquet、JSON、Excel、甚至数据库。尤其Parquet格式性能和压缩率都极其出色如果你还没接触过强烈建议尝试。第四优先级也是很多人容易忽略的学会用pl.Expr构造自定义表达式实现更复杂的数据处理逻辑。等到你能写出结构清晰、表达力强的自定义表达式时Pandas对你的吸引力就会直线下降。对我自己来说Polars最大的价值不仅仅是快而是它让我重新体会到了“写数据处理代码”的愉悦感。Pandas用久了总感觉自己在跟各种怪癖和兼容性作斗争。Polars用起来就像换了一套合身的工具每一个方法都落在它该在的位置上思路顺着代码一路写下去很少需要为了某个操作去网上搜索老半天。如果你上手过程中卡住了我建议先去翻官方文档的“API Reference”页面它的组织方式非常清晰。也可以直接在Python环境里用help()函数查看方法签名Polars的开发团队在文档字符串上下的功夫不少很多提示词直接看就能懂。入门阶段最忌讳的就是囤一堆教程然后不动手。打开终端填好一份测试数据把这篇教程中的代码敲一遍亲手跑通几个分析任务比看十篇文章都有用。相信我一旦你跨过最初的不适应你会越来越喜欢这个工具的。