diff --git a/translations/cn/README.md b/translations/cn/README.md new file mode 100644 index 000000000..a9c69544e --- /dev/null +++ b/translations/cn/README.md @@ -0,0 +1,8 @@ +# Python 数据分析(PYDA)第三版 + +> 原文:[`wesmckinney.com/book/`](https://wesmckinney.com/book/) +> +> 译者:[飞龙](https://github.com/wizardforcel) +> +> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) + diff --git a/translations/cn/SUMMARY.md b/translations/cn/SUMMARY.md new file mode 100644 index 000000000..2d56d57b4 --- /dev/null +++ b/translations/cn/SUMMARY.md @@ -0,0 +1,20 @@ ++ [Python 数据分析(PYDA)第三版](README.md) ++ [Python 数据分析第三版](pyda3e_01.md) ++ [正文](pyda3e_02.md) ++ [前言](pyda3e_03.md) ++ [一、初步](pyda3e_04.md) ++ [二、Python 语言基础,IPython 和 Jupyter 笔记本](pyda3e_05.md) ++ [三、内置数据结构、函数和文件](pyda3e_06.md) ++ [四、NumPy 基础知识:数组和向量化计算](pyda3e_07.md) ++ [五、使用 pandas 入门](pyda3e_08.md) ++ [六、数据加载、存储和文件格式](pyda3e_09.md) ++ [七、数据清理和准备](pyda3e_10.md) ++ [八、数据整理:连接、合并和重塑](pyda3e_11.md) ++ [九、绘图和可视化](pyda3e_12.md) ++ [十、数据聚合和组操作](pyda3e_13.md) ++ [十一、时间序列](pyda3e_14.md) ++ [十二、Python 建模库介绍](pyda3e_15.md) ++ [十三、数据分析示例](pyda3e_16.md) ++ [附录](pyda3e_17.md) ++ [附录 A:高级 NumPy](pyda3e_18.md) ++ [附录 B:关于 IPython 系统的更多内容](pyda3e_19.md) \ No newline at end of file diff --git a/translations/cn/img/057b871e61b23676a12ca85ac841d012.png b/translations/cn/img/057b871e61b23676a12ca85ac841d012.png new file mode 100644 index 000000000..fc2dc1a11 Binary files /dev/null and b/translations/cn/img/057b871e61b23676a12ca85ac841d012.png differ diff --git a/translations/cn/img/071a15ee104f8b18492d67565b62e3bc.png b/translations/cn/img/071a15ee104f8b18492d67565b62e3bc.png new file mode 100644 index 000000000..3022b3ae6 Binary files /dev/null and b/translations/cn/img/071a15ee104f8b18492d67565b62e3bc.png differ diff --git a/translations/cn/img/09caf3da85296866437ce8e683dc7f92.png b/translations/cn/img/09caf3da85296866437ce8e683dc7f92.png new file mode 100644 index 000000000..ca43cf28a Binary files /dev/null and b/translations/cn/img/09caf3da85296866437ce8e683dc7f92.png differ diff --git a/translations/cn/img/195715a4c5d3641a804058ec12414e68.png b/translations/cn/img/195715a4c5d3641a804058ec12414e68.png new file mode 100644 index 000000000..d2ae43e85 Binary files /dev/null and b/translations/cn/img/195715a4c5d3641a804058ec12414e68.png differ diff --git a/translations/cn/img/1acd381f491d15f26798c13e930adffc.png b/translations/cn/img/1acd381f491d15f26798c13e930adffc.png new file mode 100644 index 000000000..f20203c14 Binary files /dev/null and b/translations/cn/img/1acd381f491d15f26798c13e930adffc.png differ diff --git a/translations/cn/img/1c644fda5940269dac22103d8713dae4.png b/translations/cn/img/1c644fda5940269dac22103d8713dae4.png new file mode 100644 index 000000000..fc2937d47 Binary files /dev/null and b/translations/cn/img/1c644fda5940269dac22103d8713dae4.png differ diff --git a/translations/cn/img/1cd631e8481f11b53a8555983ea31c1e.png b/translations/cn/img/1cd631e8481f11b53a8555983ea31c1e.png new file mode 100644 index 000000000..e02800b53 Binary files /dev/null and b/translations/cn/img/1cd631e8481f11b53a8555983ea31c1e.png differ diff --git a/translations/cn/img/1e1bea5330bbf1ad2202b26ba95095b8.png b/translations/cn/img/1e1bea5330bbf1ad2202b26ba95095b8.png new file mode 100644 index 000000000..3c8a36066 Binary files /dev/null and b/translations/cn/img/1e1bea5330bbf1ad2202b26ba95095b8.png differ diff --git a/translations/cn/img/21026aa76a40a42294e16b84e2d400ed.png b/translations/cn/img/21026aa76a40a42294e16b84e2d400ed.png new file mode 100644 index 000000000..b75f51354 Binary files /dev/null and b/translations/cn/img/21026aa76a40a42294e16b84e2d400ed.png differ diff --git a/translations/cn/img/24e093fb2856ccb1ffa83d884da1713d.png b/translations/cn/img/24e093fb2856ccb1ffa83d884da1713d.png new file mode 100644 index 000000000..99894350e Binary files /dev/null and b/translations/cn/img/24e093fb2856ccb1ffa83d884da1713d.png differ diff --git a/translations/cn/img/2cdd2c64c3d5cf9a9a9300814e38d103.png b/translations/cn/img/2cdd2c64c3d5cf9a9a9300814e38d103.png new file mode 100644 index 000000000..bdcbbe9b2 Binary files /dev/null and b/translations/cn/img/2cdd2c64c3d5cf9a9a9300814e38d103.png differ diff --git a/translations/cn/img/33f467ae0ee1820d490756dfcb2a77fb.png b/translations/cn/img/33f467ae0ee1820d490756dfcb2a77fb.png new file mode 100644 index 000000000..c78a0656f Binary files /dev/null and b/translations/cn/img/33f467ae0ee1820d490756dfcb2a77fb.png differ diff --git a/translations/cn/img/3e63e6a6357b24a6c73084b155cbc27b.png b/translations/cn/img/3e63e6a6357b24a6c73084b155cbc27b.png new file mode 100644 index 000000000..9752ceb0e Binary files /dev/null and b/translations/cn/img/3e63e6a6357b24a6c73084b155cbc27b.png differ diff --git a/translations/cn/img/3ffe09420f821e4d9cb950f2e5bb6e84.png b/translations/cn/img/3ffe09420f821e4d9cb950f2e5bb6e84.png new file mode 100644 index 000000000..4650873c2 Binary files /dev/null and b/translations/cn/img/3ffe09420f821e4d9cb950f2e5bb6e84.png differ diff --git a/translations/cn/img/492765c1f52237f69c502e7807e289aa.png b/translations/cn/img/492765c1f52237f69c502e7807e289aa.png new file mode 100644 index 000000000..d59338dd1 Binary files /dev/null and b/translations/cn/img/492765c1f52237f69c502e7807e289aa.png differ diff --git a/translations/cn/img/53903a6f57aed96b5029b4bcb265ceb7.png b/translations/cn/img/53903a6f57aed96b5029b4bcb265ceb7.png new file mode 100644 index 000000000..2600297cf Binary files /dev/null and b/translations/cn/img/53903a6f57aed96b5029b4bcb265ceb7.png differ diff --git a/translations/cn/img/568aea090be607a174816297616d3b0d.png b/translations/cn/img/568aea090be607a174816297616d3b0d.png new file mode 100644 index 000000000..b1dbfa99b Binary files /dev/null and b/translations/cn/img/568aea090be607a174816297616d3b0d.png differ diff --git a/translations/cn/img/592f0eb154cb8af5292d2a938f2b873c.png b/translations/cn/img/592f0eb154cb8af5292d2a938f2b873c.png new file mode 100644 index 000000000..8a7af3497 Binary files /dev/null and b/translations/cn/img/592f0eb154cb8af5292d2a938f2b873c.png differ diff --git a/translations/cn/img/597f66552cc2eb32618fc4fb8471c04d.png b/translations/cn/img/597f66552cc2eb32618fc4fb8471c04d.png new file mode 100644 index 000000000..1c5422953 Binary files /dev/null and b/translations/cn/img/597f66552cc2eb32618fc4fb8471c04d.png differ diff --git a/translations/cn/img/5a99b09f27faef68b4b1150598d87c05.png b/translations/cn/img/5a99b09f27faef68b4b1150598d87c05.png new file mode 100644 index 000000000..f25600df5 Binary files /dev/null and b/translations/cn/img/5a99b09f27faef68b4b1150598d87c05.png differ diff --git a/translations/cn/img/5f557600f8cbeb90fd07375592d49c18.png b/translations/cn/img/5f557600f8cbeb90fd07375592d49c18.png new file mode 100644 index 000000000..576e2d17f Binary files /dev/null and b/translations/cn/img/5f557600f8cbeb90fd07375592d49c18.png differ diff --git a/translations/cn/img/61d4a67e0c93651a04b70d68220275c0.png b/translations/cn/img/61d4a67e0c93651a04b70d68220275c0.png new file mode 100644 index 000000000..6ad47a470 Binary files /dev/null and b/translations/cn/img/61d4a67e0c93651a04b70d68220275c0.png differ diff --git a/translations/cn/img/6716e4c3fc8eb1a44199a90351da2a0d.png b/translations/cn/img/6716e4c3fc8eb1a44199a90351da2a0d.png new file mode 100644 index 000000000..09c5ee7ed Binary files /dev/null and b/translations/cn/img/6716e4c3fc8eb1a44199a90351da2a0d.png differ diff --git a/translations/cn/img/77180177aee670ca8681b6eabd6ebce8.png b/translations/cn/img/77180177aee670ca8681b6eabd6ebce8.png new file mode 100644 index 000000000..ada658ac0 Binary files /dev/null and b/translations/cn/img/77180177aee670ca8681b6eabd6ebce8.png differ diff --git a/translations/cn/img/7921e3ccd70b031f36c44b85107918d7.png b/translations/cn/img/7921e3ccd70b031f36c44b85107918d7.png new file mode 100644 index 000000000..ead2c86c1 Binary files /dev/null and b/translations/cn/img/7921e3ccd70b031f36c44b85107918d7.png differ diff --git a/translations/cn/img/794c774025d4e8e044c70ae0199ae555.png b/translations/cn/img/794c774025d4e8e044c70ae0199ae555.png new file mode 100644 index 000000000..87c484e34 Binary files /dev/null and b/translations/cn/img/794c774025d4e8e044c70ae0199ae555.png differ diff --git a/translations/cn/img/7b3fc44566d76e7e8e544437c2dffbcc.png b/translations/cn/img/7b3fc44566d76e7e8e544437c2dffbcc.png new file mode 100644 index 000000000..68e2d5fbe Binary files /dev/null and b/translations/cn/img/7b3fc44566d76e7e8e544437c2dffbcc.png differ diff --git a/translations/cn/img/7dd52fd5fa321b6a9d79111326825f9b.png b/translations/cn/img/7dd52fd5fa321b6a9d79111326825f9b.png new file mode 100644 index 000000000..dbe07664f Binary files /dev/null and b/translations/cn/img/7dd52fd5fa321b6a9d79111326825f9b.png differ diff --git a/translations/cn/img/7f99b0a8aa2805183d2316e51eba5f22.png b/translations/cn/img/7f99b0a8aa2805183d2316e51eba5f22.png new file mode 100644 index 000000000..60d79aed5 Binary files /dev/null and b/translations/cn/img/7f99b0a8aa2805183d2316e51eba5f22.png differ diff --git a/translations/cn/img/80bf6ca7e96e78f9839a73591d77f472.png b/translations/cn/img/80bf6ca7e96e78f9839a73591d77f472.png new file mode 100644 index 000000000..ca157e0bc Binary files /dev/null and b/translations/cn/img/80bf6ca7e96e78f9839a73591d77f472.png differ diff --git a/translations/cn/img/844beecf4a6f3e1c98bd50a928a5d379.png b/translations/cn/img/844beecf4a6f3e1c98bd50a928a5d379.png new file mode 100644 index 000000000..c8d8ba516 Binary files /dev/null and b/translations/cn/img/844beecf4a6f3e1c98bd50a928a5d379.png differ diff --git a/translations/cn/img/8592bb9fc63f4f28333b53d403105a57.png b/translations/cn/img/8592bb9fc63f4f28333b53d403105a57.png new file mode 100644 index 000000000..456eb65df Binary files /dev/null and b/translations/cn/img/8592bb9fc63f4f28333b53d403105a57.png differ diff --git a/translations/cn/img/85dffdfb28f327f6e100f02d45b76f91.png b/translations/cn/img/85dffdfb28f327f6e100f02d45b76f91.png new file mode 100644 index 000000000..c3abd4f8b Binary files /dev/null and b/translations/cn/img/85dffdfb28f327f6e100f02d45b76f91.png differ diff --git a/translations/cn/img/898f6a6974d281ac4a87524fcabc2566.png b/translations/cn/img/898f6a6974d281ac4a87524fcabc2566.png new file mode 100644 index 000000000..8893fb197 Binary files /dev/null and b/translations/cn/img/898f6a6974d281ac4a87524fcabc2566.png differ diff --git a/translations/cn/img/94759a9f5e61afbdf151f5410cb1d6ca.png b/translations/cn/img/94759a9f5e61afbdf151f5410cb1d6ca.png new file mode 100644 index 000000000..c340d2937 Binary files /dev/null and b/translations/cn/img/94759a9f5e61afbdf151f5410cb1d6ca.png differ diff --git a/translations/cn/img/95638725670f843291db058cdae27a0b.png b/translations/cn/img/95638725670f843291db058cdae27a0b.png new file mode 100644 index 000000000..6f97c9331 Binary files /dev/null and b/translations/cn/img/95638725670f843291db058cdae27a0b.png differ diff --git a/translations/cn/img/9ddeddd0816344270288957f3493eb63.png b/translations/cn/img/9ddeddd0816344270288957f3493eb63.png new file mode 100644 index 000000000..43a123f82 Binary files /dev/null and b/translations/cn/img/9ddeddd0816344270288957f3493eb63.png differ diff --git a/translations/cn/img/a09620216e4172a26b3283ca34ce702c.png b/translations/cn/img/a09620216e4172a26b3283ca34ce702c.png new file mode 100644 index 000000000..406bd90d4 Binary files /dev/null and b/translations/cn/img/a09620216e4172a26b3283ca34ce702c.png differ diff --git a/translations/cn/img/a218b1a30914e6cbfa11d7eb83a4f1cb.png b/translations/cn/img/a218b1a30914e6cbfa11d7eb83a4f1cb.png new file mode 100644 index 000000000..8b3edb25c Binary files /dev/null and b/translations/cn/img/a218b1a30914e6cbfa11d7eb83a4f1cb.png differ diff --git a/translations/cn/img/a4c5cd6fc9e8e06951e967c00791ae0c.png b/translations/cn/img/a4c5cd6fc9e8e06951e967c00791ae0c.png new file mode 100644 index 000000000..dda43269c Binary files /dev/null and b/translations/cn/img/a4c5cd6fc9e8e06951e967c00791ae0c.png differ diff --git a/translations/cn/img/ac1010ef5553eff57302677c1ac29851.png b/translations/cn/img/ac1010ef5553eff57302677c1ac29851.png new file mode 100644 index 000000000..6f8424d1c Binary files /dev/null and b/translations/cn/img/ac1010ef5553eff57302677c1ac29851.png differ diff --git a/translations/cn/img/ae89dd22b1516b02323b63614b75f260.png b/translations/cn/img/ae89dd22b1516b02323b63614b75f260.png new file mode 100644 index 000000000..f091888f6 Binary files /dev/null and b/translations/cn/img/ae89dd22b1516b02323b63614b75f260.png differ diff --git a/translations/cn/img/b305e27a26b0ff06c2d3130576a72f42.png b/translations/cn/img/b305e27a26b0ff06c2d3130576a72f42.png new file mode 100644 index 000000000..c60fb32fd Binary files /dev/null and b/translations/cn/img/b305e27a26b0ff06c2d3130576a72f42.png differ diff --git a/translations/cn/img/b686da0f6b499292c1b338fdebcddfef.png b/translations/cn/img/b686da0f6b499292c1b338fdebcddfef.png new file mode 100644 index 000000000..dc4ebf4e3 Binary files /dev/null and b/translations/cn/img/b686da0f6b499292c1b338fdebcddfef.png differ diff --git a/translations/cn/img/b8b372e58ab7916dbb3ef4c71569c5be.png b/translations/cn/img/b8b372e58ab7916dbb3ef4c71569c5be.png new file mode 100644 index 000000000..f281c2fbd Binary files /dev/null and b/translations/cn/img/b8b372e58ab7916dbb3ef4c71569c5be.png differ diff --git a/translations/cn/img/baca2f4ab210ab7fbb1363acd5366b42.png b/translations/cn/img/baca2f4ab210ab7fbb1363acd5366b42.png new file mode 100644 index 000000000..0a07b3aa7 Binary files /dev/null and b/translations/cn/img/baca2f4ab210ab7fbb1363acd5366b42.png differ diff --git a/translations/cn/img/bbe77ca56f949e70e20ccf23975bca48.png b/translations/cn/img/bbe77ca56f949e70e20ccf23975bca48.png new file mode 100644 index 000000000..44f2c0260 Binary files /dev/null and b/translations/cn/img/bbe77ca56f949e70e20ccf23975bca48.png differ diff --git a/translations/cn/img/c29739a3af9d3f69ced8617625eb559c.png b/translations/cn/img/c29739a3af9d3f69ced8617625eb559c.png new file mode 100644 index 000000000..5576a7896 Binary files /dev/null and b/translations/cn/img/c29739a3af9d3f69ced8617625eb559c.png differ diff --git a/translations/cn/img/c61e1e89fed6ca0718719066a3fad5d6.png b/translations/cn/img/c61e1e89fed6ca0718719066a3fad5d6.png new file mode 100644 index 000000000..a02f34787 Binary files /dev/null and b/translations/cn/img/c61e1e89fed6ca0718719066a3fad5d6.png differ diff --git a/translations/cn/img/c65abbdbb1b538d16ea4f45c46581a89.png b/translations/cn/img/c65abbdbb1b538d16ea4f45c46581a89.png new file mode 100644 index 000000000..ba2c7f81c Binary files /dev/null and b/translations/cn/img/c65abbdbb1b538d16ea4f45c46581a89.png differ diff --git a/translations/cn/img/c901850a416d7a0f42bc07ffa0092543.png b/translations/cn/img/c901850a416d7a0f42bc07ffa0092543.png new file mode 100644 index 000000000..9aeb45fb2 Binary files /dev/null and b/translations/cn/img/c901850a416d7a0f42bc07ffa0092543.png differ diff --git a/translations/cn/img/cf72351c291cc639dc236d8072622fe1.png b/translations/cn/img/cf72351c291cc639dc236d8072622fe1.png new file mode 100644 index 000000000..440f479e3 Binary files /dev/null and b/translations/cn/img/cf72351c291cc639dc236d8072622fe1.png differ diff --git a/translations/cn/img/d60ef95fddd13030d45f90939bef5ca1.png b/translations/cn/img/d60ef95fddd13030d45f90939bef5ca1.png new file mode 100644 index 000000000..9b37f9ba5 Binary files /dev/null and b/translations/cn/img/d60ef95fddd13030d45f90939bef5ca1.png differ diff --git a/translations/cn/img/d8892398961900d39bbbcf3f01d8da4c.png b/translations/cn/img/d8892398961900d39bbbcf3f01d8da4c.png new file mode 100644 index 000000000..f2890e0ea Binary files /dev/null and b/translations/cn/img/d8892398961900d39bbbcf3f01d8da4c.png differ diff --git a/translations/cn/img/d8cb52110dd49d0ab8074f78afe0845d.png b/translations/cn/img/d8cb52110dd49d0ab8074f78afe0845d.png new file mode 100644 index 000000000..9b289f5c1 Binary files /dev/null and b/translations/cn/img/d8cb52110dd49d0ab8074f78afe0845d.png differ diff --git a/translations/cn/img/dff02874e6216c043e03726ae580d604.png b/translations/cn/img/dff02874e6216c043e03726ae580d604.png new file mode 100644 index 000000000..3bf6d2d94 Binary files /dev/null and b/translations/cn/img/dff02874e6216c043e03726ae580d604.png differ diff --git a/translations/cn/img/e1a3230ba5c578a21a784b09be9c0e91.png b/translations/cn/img/e1a3230ba5c578a21a784b09be9c0e91.png new file mode 100644 index 000000000..9d38c1083 Binary files /dev/null and b/translations/cn/img/e1a3230ba5c578a21a784b09be9c0e91.png differ diff --git a/translations/cn/img/e27af70a8051b0749f47f917cb9bc365.png b/translations/cn/img/e27af70a8051b0749f47f917cb9bc365.png new file mode 100644 index 000000000..7f01595db Binary files /dev/null and b/translations/cn/img/e27af70a8051b0749f47f917cb9bc365.png differ diff --git a/translations/cn/img/e7ac4057ad5de3681057e0690540ab39.png b/translations/cn/img/e7ac4057ad5de3681057e0690540ab39.png new file mode 100644 index 000000000..c34234a76 Binary files /dev/null and b/translations/cn/img/e7ac4057ad5de3681057e0690540ab39.png differ diff --git a/translations/cn/img/e84a484574c968c98c4258588ab07435.png b/translations/cn/img/e84a484574c968c98c4258588ab07435.png new file mode 100644 index 000000000..2d1e895de Binary files /dev/null and b/translations/cn/img/e84a484574c968c98c4258588ab07435.png differ diff --git a/translations/cn/img/e8f4e446ffcb7dd48b59497f44f85dd4.png b/translations/cn/img/e8f4e446ffcb7dd48b59497f44f85dd4.png new file mode 100644 index 000000000..0a485bfdc Binary files /dev/null and b/translations/cn/img/e8f4e446ffcb7dd48b59497f44f85dd4.png differ diff --git a/translations/cn/img/e930d33b55aed88698b1dbfd5bd03cbf.png b/translations/cn/img/e930d33b55aed88698b1dbfd5bd03cbf.png new file mode 100644 index 000000000..0fb5d7a1f Binary files /dev/null and b/translations/cn/img/e930d33b55aed88698b1dbfd5bd03cbf.png differ diff --git a/translations/cn/img/ebc8b1612dde160033c19bae91791567.png b/translations/cn/img/ebc8b1612dde160033c19bae91791567.png new file mode 100644 index 000000000..3deb4d2fc Binary files /dev/null and b/translations/cn/img/ebc8b1612dde160033c19bae91791567.png differ diff --git a/translations/cn/img/f00985b53f21f5e966a370573cb76885.png b/translations/cn/img/f00985b53f21f5e966a370573cb76885.png new file mode 100644 index 000000000..c577dea97 Binary files /dev/null and b/translations/cn/img/f00985b53f21f5e966a370573cb76885.png differ diff --git a/translations/cn/img/f28517ea0c78eaca45d3a1d21b743605.png b/translations/cn/img/f28517ea0c78eaca45d3a1d21b743605.png new file mode 100644 index 000000000..3de3850a7 Binary files /dev/null and b/translations/cn/img/f28517ea0c78eaca45d3a1d21b743605.png differ diff --git a/translations/cn/img/f4e8c3c83f755658e00aaadb7414d336.png b/translations/cn/img/f4e8c3c83f755658e00aaadb7414d336.png new file mode 100644 index 000000000..861aed024 Binary files /dev/null and b/translations/cn/img/f4e8c3c83f755658e00aaadb7414d336.png differ diff --git a/translations/cn/img/f69b4d795dc2eedaf3e3e02e6e0a9d87.png b/translations/cn/img/f69b4d795dc2eedaf3e3e02e6e0a9d87.png new file mode 100644 index 000000000..5bac93532 Binary files /dev/null and b/translations/cn/img/f69b4d795dc2eedaf3e3e02e6e0a9d87.png differ diff --git a/translations/cn/img/fb752abddb93c77ae991903b8cf1f98f.png b/translations/cn/img/fb752abddb93c77ae991903b8cf1f98f.png new file mode 100644 index 000000000..c4591d3f9 Binary files /dev/null and b/translations/cn/img/fb752abddb93c77ae991903b8cf1f98f.png differ diff --git a/translations/cn/img/fdaeef8acbb8ceb4025d1c1bd91d31b1.png b/translations/cn/img/fdaeef8acbb8ceb4025d1c1bd91d31b1.png new file mode 100644 index 000000000..5f774aefb Binary files /dev/null and b/translations/cn/img/fdaeef8acbb8ceb4025d1c1bd91d31b1.png differ diff --git a/translations/cn/img/fe1beb3f93e0617bb1f4800840b8dc37.png b/translations/cn/img/fe1beb3f93e0617bb1f4800840b8dc37.png new file mode 100644 index 000000000..daa88de76 Binary files /dev/null and b/translations/cn/img/fe1beb3f93e0617bb1f4800840b8dc37.png differ diff --git a/translations/cn/img/fecc24f715268acabec45f889288da9c.png b/translations/cn/img/fecc24f715268acabec45f889288da9c.png new file mode 100644 index 000000000..8c02b6539 Binary files /dev/null and b/translations/cn/img/fecc24f715268acabec45f889288da9c.png differ diff --git a/translations/cn/pyda3e_01.md b/translations/cn/pyda3e_01.md new file mode 100644 index 000000000..c5067d5eb --- /dev/null +++ b/translations/cn/pyda3e_01.md @@ -0,0 +1,40 @@ +# Python 数据分析第三版 + +> 原文:[`wesmckinney.com/book/`](https://wesmckinney.com/book/) +> +> 译者:[飞龙](https://github.com/wizardforcel) +> +> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) + + +# 关于开放版本 + +第 3 版的[《Python 数据分析》](https://amzn.to/3DyLaJc)现在作为“开放获取”HTML 版本在此网站[`wesmckinney.com/book`](https://wesmckinney.com/book)上提供,除了通常的印刷和电子书格式。该版本最初于 2022 年 8 月出版,将在未来几个月和年份内定期修正勘误。如果您发现任何勘误,请[在此处报告](https://oreilly.com/catalog/0636920519829/errata)。 + +一般来说,本网站的内容不得复制或复制。代码示例采用 MIT 许可证,可在[GitHub](https://github.com/wesm/pydata-book/tree/3rd-edition)或[Gitee](https://gitee.com/wesmckinn/pydata-book)上找到,以及支持数据集。 +> +> 如果您发现本书的在线版本有用,请考虑[订购纸质版本](https://amzn.to/3DyLaJc)或[无 DRM 的电子书](https://www.ebooks.com/en-us/book/210644288/python-for-data-analysis/wes-mckinney/?affId=WES398681F)(PDF 和 EPUB 格式)以支持作者。 + +本书的网络版本是使用[Quarto 出版系统](https://quarto.org/)创建的。 + +## 第 3 版的新内容是什么? + +本书已更新到 pandas 2.0.0 和 Python 3.10。第 2 版和第 3 版之间的变化主要集中在将内容与自 2017 年以来 pandas 的变化保持最新。 + +## 更新历史 + +本网站将定期更新,以提供新的早期发布内容,并在出版后修复勘误。 + ++ 2023 年 4 月 12 日:更新到 pandas 2.0.0 并修复一些代码示例。 + ++ 2022 年 10 月 19 日:修复表格链接并添加[eBooks.com 链接](https://www.ebooks.com/en-us/book/210644288/python-for-data-analysis/wes-mckinney/?affId=WES398681F)。 + ++ 2022 年 9 月 20 日:最终出版后的网站更新,包括修复了一些小错误。 + ++ 2022 年 7 月 22 日:在今年夏天后期出版之前,将副本编辑和其他改进纳入“QC1”生产阶段。 + ++ 2022 年 5 月 18 日:更新开放获取版本的所有章节。包括来自技术审查反馈(谢谢!)的编辑,第三版的致谢以及其他准备工作,使本书准备好在 2022 年晚些时候印刷。 + ++ 2022 年 2 月 13 日:更新开放获取版本,包括第 7 至第十章。 + ++ 2022 年 1 月 23 日:首次开放获取版本,包括第 1 至第六章。 diff --git a/translations/cn/pyda3e_02.md b/translations/cn/pyda3e_02.md new file mode 100644 index 000000000..66efe1ead --- /dev/null +++ b/translations/cn/pyda3e_02.md @@ -0,0 +1 @@ +# 正文 diff --git a/translations/cn/pyda3e_03.md b/translations/cn/pyda3e_03.md new file mode 100644 index 000000000..aaab8eb63 --- /dev/null +++ b/translations/cn/pyda3e_03.md @@ -0,0 +1,116 @@ +# 前言 + +> 原文:[`wesmckinney.com/book/preface`](https://wesmckinney.com/book/preface) +> +> 译者:[飞龙](https://github.com/wizardforcel) +> +> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) + + +> 此开放访问网络版本的《Python 数据分析第三版》现已作为[印刷版和数字版](https://amzn.to/3DyLaJc)的伴侣提供。如果您发现任何勘误,请[在此处报告](https://oreilly.com/catalog/0636920519829/errata)。请注意,由 Quarto 生成的本站点的某些方面与 O'Reilly 的印刷版和电子书版本的格式不同。 +> +> 如果您发现本书的在线版本有用,请考虑[订购纸质版](https://amzn.to/3DyLaJc)或[无 DRM 的电子书](https://www.ebooks.com/en-us/book/210644288/python-for-data-analysis/wes-mckinney/?affId=WES398681F)以支持作者。本网站的内容不得复制或再生产。代码示例采用 MIT 许可,可在 GitHub 或 Gitee 上找到。 + +本书的第一版于 2012 年出版,当时 Python 的开源数据分析库,尤其是 pandas,非常新且快速发展。到了 2016 年和 2017 年写第二版时,我不仅需要将书更新为 Python 3.6(第一版使用 Python 2.7),还需要更新 pandas 在过去五年中发生的许多变化。现在是 2022 年,Python 语言的变化较少(我们现在使用 Python 3.10,3.11 将于 2022 年底发布),但 pandas 仍在不断发展。 + +在这第三版中,我的目标是将内容与当前版本的 Python、NumPy、pandas 和其他项目保持同步,同时对于讨论近几年出现的较新的 Python 项目保持相对保守。由于这本书已成为许多大学课程和职业人士的重要资源,我将尽量避免讨论可能在一两年内过时的主题。这样,纸质副本在 2023 年、2024 年甚至更久以后也不会太难理解。 + +第三版的一个新特性是托管在我的网站上的开放访问在线版本,网址为[`wesmckinney.com/book`](https://wesmckinney.com/book),可作为印刷版和数字版的所有者的资源和便利。我打算保持那里的内容相对及时更新,因此如果您拥有纸质书并遇到某些问题,请在那里查看最新的内容更改。 + +## 本书中使用的约定 + +本书中使用以下排版约定: + +*斜体* + +指示新术语、URL、电子邮件地址、文件名和文件扩展名。 + +`等宽` + +用于程序清单,以及段落内引用程序元素,如变量或函数名、数据库、数据类型、环境变量、语句和关键字。 + +`等宽粗体` + +显示用户应按照字面意思键入的命令或其他文本。 + +<等宽斜体> + +显示应替换为用户提供的值或由上下文确定的值的文本。 + +提示: + +此元素表示提示或建议。 + +注意: + +此元素表示一般说明。 + +警告: + +此元素表示警告或注意事项。 + +## 使用代码示例 + +您可以在本书的 GitHub 存储库中找到每章的数据文件和相关材料,网址为[`github.com/wesm/pydata-book`](https://github.com/wesm/pydata-book),该存储库在 Gitee 上有镜像(供无法访问 GitHub 的用户使用),网址为[`gitee.com/wesmckinn/pydata-book`](https://gitee.com/wesmckinn/pydata-book)。 + +这本书旨在帮助您完成工作。一般来说,如果本书提供示例代码,您可以在程序和文档中使用它。除非您复制了代码的大部分内容,否则无需征得我们的许可。例如,编写一个使用本书中几个代码块的程序不需要许可。销售或分发 O'Reilly 图书中的示例需要许可。通过引用本书回答问题并引用示例代码不需要许可。将本书中大量示例代码合并到产品文档中需要许可。 + +我们感谢,但不要求署名。署名通常包括标题、作者、出版商和 ISBN。例如:“*Python for Data Analysis* by Wes McKinney(O'Reilly)。版权所有 2022 年 Wes McKinney,978-1-098-10403-0。” + +如果您觉得您对代码示例的使用超出了合理使用范围或上述许可,请随时通过 permissions@oreilly.com 与我们联系。 + +## 致谢 + +这项工作是多年来与世界各地许多人进行富有成果的讨论和合作的成果。我想感谢其中的一些人。 + +## 追悼:约翰·D·亨特(1968-2012) + +我们亲爱的朋友和同事约翰·D·亨特在 2012 年 8 月 28 日与结肠癌搏斗后去世。这发生在我完成本书第一版最终手稿后不久。 + +约翰在 Python 科学和数据社区的影响和遗产难以估量。除了在 21 世纪初开发 matplotlib(当时 Python 并不那么流行)之外,他还帮助塑造了一代关键的开源开发者文化,这些开发者已经成为我们现在经常视为理所当然的 Python 生态系统的支柱。 + +我很幸运在 2010 年 1 月早期与约翰建立了联系,就在发布 pandas 0.1 后不久。他的启发和指导帮助我在最黑暗的时刻推动前进,实现了我对 pandas 和 Python 作为一流数据分析语言的愿景。 + +John 与 Fernando Pérez 和 Brian Granger 非常亲近,他们是 IPython、Jupyter 和 Python 社区中许多其他倡议的先驱。我们曾希望一起合作写一本书,但最终我成为了拥有最多空闲时间的人。我相信他会为我们在过去九年中所取得的成就感到自豪,无论是作为个人还是作为一个社区。 + +## 致谢第三版(2022 年) + +自从我开始写这本书的第一版以来已经有十多年了,自从我最初作为 Python 程序员开始我的旅程以来已经有 15 年了。那时发生了很多变化!Python 已经从一个相对小众的数据分析语言发展成为最受欢迎、最广泛使用的语言,支持着数据科学、机器学习和人工智能工作的多数(如果不是大多数!)。 + +自 2013 年以来,我并没有积极参与 pandas 开源项目,但其全球开发者社区仍在蓬勃发展,成为以社区为中心的开源软件开发模式的典范。许多处理表格数据的“下一代”Python 项目直接模仿 pandas 的用户界面,因此该项目已经对 Python 数据科学生态系统未来的发展轨迹产生了持久的影响。 + +希望这本书能继续为想要学习如何在 Python 中处理数据的学生和个人提供宝贵的资源。 + +我特别感谢 O'Reilly 允许我在我的网站[`wesmckinney.com/book`](https://wesmckinney.com/book)上发布这本书的“开放获取”版本,希望它能触达更多人,并帮助扩大数据分析领域的机会。J.J. Allaire 在帮助我将这本书从 Docbook XML“移植”到[Quarto](https://quarto.org)时是一个救星,Quarto 是一个出色的新科学技术出版系统,适用于印刷和网络。 + +特别感谢我的技术审阅者 Paul Barry、Jean-Christophe Leyder、Abdullah Karasan 和 William Jamir,他们的详细反馈极大地提高了内容的可读性、清晰度和可理解性。 + +## 致谢第二版(2017 年) + +距离我在 2012 年 7 月完成这本书第一版手稿已经快五年了。很多事情发生了变化。Python 社区已经大幅增长,围绕它的开源软件生态系统也蓬勃发展。 + +如果不是 pandas 核心开发者们不懈的努力,这本书的新版将不会存在,他们已经将这个项目及其用户社区发展成为 Python 数据科学生态系统的支柱之一。这些人包括但不限于 Tom Augspurger、Joris van den Bossche、Chris Bartak、Phillip Cloud、gfyoung、Andy Hayden、Masaaki Horikoshi、Stephan Hoyer、Adam Klein、Wouter Overmeire、Jeff Reback、Chang She、Skipper Seabold、Jeff Tratner 和 y-p。 + +在撰写这本第二版时,我要感谢 O'Reilly 的工作人员在写作过程中耐心地帮助我。其中包括 Marie Beaugureau、Ben Lorica 和 Colleen Toporek。我再次有幸得到 Tom Augspurger、Paul Barry、Hugh Brown、Jonathan Coe 和 Andreas Müller 等杰出的技术审阅者的帮助。谢谢。 + +这本书的第一版已经被翻译成许多外语,包括中文、法语、德语、日语、韩语和俄语。翻译所有这些内容并让更广泛的受众获得是一项巨大且常常被忽视的工作。感谢您帮助更多世界上的人学习如何编程和使用数据分析工具。 + +在过去几年里,我很幸运地得到了 Cloudera 和 Two Sigma Investments 对我持续的开源开发工作的支持。随着开源软件项目相对于用户群体规模而言资源更加稀缺,企业为关键开源项目的开发提供支持变得越来越重要。这是正确的做法。 + +## 致谢第一版(2012) + +如果没有许多人的支持,我很难写出这本书。 + +在 O'Reilly 的工作人员中,我非常感激我的编辑 Meghan Blanchette 和 Julie Steele,他们在整个过程中指导我。Mike Loukides 也在提案阶段与我合作,帮助使这本书成为现实。 + +我得到了许多人的技术审查。特别是 Martin Blais 和 Hugh Brown 在改进书中的示例、清晰度和组织方面提供了极大帮助。James Long,Drew Conway,Fernando Pérez,Brian Granger,Thomas Kluyver,Adam Klein,Josh Klein,Chang She 和 Stéfan van der Walt 分别审查了一个或多个章节,从许多不同的角度提供了有针对性的反馈。 + +我从数据社区的朋友和同事那里得到了许多出色的示例和数据集的创意,其中包括:Mike Dewar,Jeff Hammerbacher,James Johndrow,Kristian Lum,Adam Klein,Hilary Mason,Chang She 和 Ashley Williams。 + +当然,我要感谢许多开源科学 Python 社区的领导者,他们为我的开发工作奠定了基础,并在我写这本书时给予了鼓励:IPython 核心团队(Fernando Pérez,Brian Granger,Min Ragan-Kelly,Thomas Kluyver 等),John Hunter,Skipper Seabold,Travis Oliphant,Peter Wang,Eric Jones,Robert Kern,Josef Perktold,Francesc Alted,Chris Fonnesbeck 等等。还有许多其他人,无法一一列举。还有一些人在这个过程中提供了大量的支持、想法和鼓励:Drew Conway,Sean Taylor,Giuseppe Paleologo,Jared Lander,David Epstein,John Krowas,Joshua Bloom,Den Pilsworth,John Myles-White 等等。 + +我还要感谢一些在我成长过程中的人。首先是我的前 AQR 同事,多年来一直在我的 pandas 工作中支持我:Alex Reyfman,Michael Wong,Tim Sargen,Oktay Kurbanov,Matthew Tschantz,Roni Israelov,Michael Katz,Ari Levine,Chris Uga,Prasad Ramanan,Ted Square 和 Hoon Kim。最后,我的学术导师 Haynes Miller(MIT)和 Mike West(Duke)。 + +2014 年,我得到了 Phillip Cloud 和 Joris van den Bossche 的重要帮助,更新了书中的代码示例,并修复了由于 pandas 变化而导致的一些不准确之处。 + +在个人方面,Casey 在写作过程中提供了宝贵的日常支持,容忍我在本已过度忙碌的日程表上拼凑出最终草稿时的起起伏伏。最后,我的父母 Bill 和 Kim 教导我始终追随梦想,永不妥协。 diff --git a/translations/cn/pyda3e_04.md b/translations/cn/pyda3e_04.md new file mode 100644 index 000000000..f6fc4f1db --- /dev/null +++ b/translations/cn/pyda3e_04.md @@ -0,0 +1,462 @@ +# 一、初步 + +> 原文:[`wesmckinney.com/book/preliminaries`](https://wesmckinney.com/book/preliminaries) +> +> 译者:[飞龙](https://github.com/wizardforcel) +> +> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) + + +> 此开放访问网络版本的《Python 数据分析第三版》现已作为[印刷版和数字版](https://amzn.to/3DyLaJc)的伴侣提供。如果您发现任何勘误,请[在此处报告](https://oreilly.com/catalog/0636920519829/errata)。请注意,由 Quarto 生成的本站点的某些方面与 O'Reilly 的印刷版和电子书版本的格式不同。 +> +> 如果您发现本书的在线版本有用,请考虑[订购纸质版](https://amzn.to/3DyLaJc)或[无 DRM 的电子书](https://www.ebooks.com/en-us/book/210644288/python-for-data-analysis/wes-mckinney/?affId=WES398681F)以支持作者。本网站的内容不得复制或再生产。代码示例采用 MIT 许可,可在 GitHub 或 Gitee 上找到。 + +这本书关注的是在 Python 中操纵、处理、清理和处理数据的基本原理。我的目标是为 Python 编程语言及其面向数据的库生态系统和工具提供指南,使您能够成为一名有效的数据分析师。虽然书名中有“数据分析”一词,但重点特别放在 Python 编程、库和工具上,而不是数据分析方法论。这是您进行数据分析所需的 Python 编程。 + +在我 2012 年首次出版这本书之后不久,人们开始使用“数据科学”这个术语作为从简单的描述性统计到更高级的统计分析和机器学习等各种内容的总称。自那时以来,用于进行数据分析(或数据科学)的 Python 开源生态系统也显著扩展。现在有许多其他专门关注这些更高级方法的书籍。我希望这本书能够作为足够的准备,使您能够转向更具领域特定性的资源。 + +注意: + +有些人可能将本书的大部分内容描述为“数据操纵”,而不是“数据分析”。我们还使用*整理*或*整理*这些术语来指代数据操纵。*### 什么样的数据? + +当我说“数据”时,我确切指的是什么?主要关注的是*结构化数据*,这是一个故意模糊的术语,包括许多不同形式的常见数据,例如: + ++ 表格或类似电子表格的数据,其中每列可能是不同类型(字符串、数字、日期或其他)。这包括通常存储在关系数据库或制表符或逗号分隔文本文件中的各种数据。 + ++ 多维数组(矩阵)。 + ++ 由关键列相互关联的多个数据表(对 SQL 用户来说可能是主键或外键)。 + ++ 均匀或不均匀间隔的时间序列。 + +这绝不是一个完整的列表。即使可能并不总是明显,大部分数据集都可以转换为更适合分析和建模的结构化形式。如果不行,可能可以从数据集中提取特征到结构化形式。例如,一组新闻文章可以处理成一个词频表,然后用于执行情感分析。 + +像 Microsoft Excel 这样的电子表格程序的大多数用户,可能是世界上最广泛使用的数据分析工具,对这些数据类型并不陌生。*## 1.2 为什么选择 Python 进行数据分析? + +对许多人来说,Python 编程语言具有很强的吸引力。自 1991 年首次亮相以来,Python 已成为最受欢迎的解释性编程语言之一,与 Perl、Ruby 等一起。自 2005 年左右以来,Python 和 Ruby 特别受欢迎,用于构建网站,使用它们众多的 Web 框架,如 Rails(Ruby)和 Django(Python)。这些语言通常被称为“脚本”语言,因为它们可以用于快速编写小程序或脚本来自动化其他任务。我不喜欢“脚本语言”这个术语,因为它带有一种暗示,即它们不能用于构建严肃的软件。出于各种历史和文化原因,在解释性语言中,Python 已经发展成一个庞大而活跃的科学计算和数据分析社区。在过去的 20 年里,Python 已经从一个前沿或“自担风险”的科学计算语言发展成为学术界和工业界数据科学、机器学习和通用软件开发中最重要的语言之一。 + +对于数据分析、交互式计算和数据可视化,Python 不可避免地会与其他广泛使用的开源和商业编程语言和工具进行比较,如 R、MATLAB、SAS、Stata 等。近年来,Python 改进的开源库(如 pandas 和 scikit-learn)使其成为数据分析任务的热门选择。结合 Python 在通用软件工程方面的整体实力,它是构建数据应用程序的主要语言的绝佳选择。 + +### Python 作为胶水 + +Python 在科学计算中的成功部分在于轻松集成 C、C++和 FORTRAN 代码。大多数现代计算环境共享一组类似的传统 FORTRAN 和 C 库,用于进行线性代数、优化、积分、快速傅里叶变换等算法。许多公司和国家实验室使用 Python 将几十年的传统软件粘合在一起的故事也是如此。 + +许多程序由小部分代码组成,其中大部分时间都花在其中,大量“胶水代码”很少运行。在许多情况下,胶水代码的执行时间微不足道;最有价值的努力是在优化计算瓶颈上,有时通过将代码移动到像 C 这样的低级语言来实现。 + +### 解决“双语言”问题 + +在许多组织中,通常使用更专门的计算语言如 SAS 或 R 进行研究、原型设计和测试新想法,然后将这些想法移植为更大的生产系统的一部分,比如 Java、C#或 C++。人们越来越发现 Python 不仅适合用于研究和原型设计,也适合用于构建生产系统。当一个开发环境足够时,为什么要维护两个呢?我相信越来越多的公司会选择这条道路,因为让研究人员和软件工程师使用相同的编程工具集通常会带来重大的组织效益。 + +在过去的十年里,一些解决“双语言”问题的新方法出现了,比如 Julia 编程语言。在许多情况下,充分利用 Python 将需要使用低级语言如 C 或 C++编程,并创建 Python 绑定到该代码。也就是说,像 Numba 这样的“即时”(JIT)编译器技术提供了一种在 Python 编程环境中实现出色性能的方法,而无需离开 Python 编程环境。 + +### 为什么不用 Python? + +虽然 Python 是构建许多种分析应用程序和通用系统的优秀环境,但也有一些用途不太适合 Python。 + +由于 Python 是一种解释性编程语言,通常大多数 Python 代码运行速度会比像 Java 或 C++这样的编译语言编写的代码慢得多。由于*程序员时间*通常比*CPU 时间*更有价值,许多人愿意做出这种权衡。然而,在具有非常低延迟或对资源利用要求苛刻的应用程序中(例如高频交易系统),花费时间以低级语言(但也低生产力)如 C++编程,以实现可能的最大性能,可能是值得的。 + +Python 可能是一个具有挑战性的语言,用于构建高度并发、多线程的应用程序,特别是具有许多 CPU 绑定线程的应用程序。造成这种情况的原因是它具有所谓的*全局解释器锁*(GIL),这是一种机制,防止解释器一次执行多个 Python 指令。GIL 存在的技术原因超出了本书的范围。虽然在许多大数据处理应用中,可能需要一组计算机集群来在合理的时间内处理数据集,但仍然存在一些情况,其中单进程、多线程系统是可取的。 + +这并不是说 Python 不能执行真正的多线程、并行代码。使用本地多线程(在 C 或 C++中)的 Python C 扩展可以在不受 GIL 影响的情况下并行运行代码,只要它们不需要经常与 Python 对象交互。 + +## 1.3 必要的 Python 库 + +对于那些对 Python 数据生态系统和本书中使用的库不太熟悉的人,我将简要介绍其中一些。 + +### NumPy + +[NumPy](https://numpy.org),简称 Numerical Python,长期以来一直是 Python 中数值计算的基石。它提供了大多数涉及 Python 中数值数据的科学应用所需的数据结构、算法和库粘合剂。NumPy 包含,除其他内容外: + ++ 快速高效的多维数组对象*ndarray* + ++ 执行数组元素计算或数组之间的数学运算的函数 + ++ 用于读取和写入基于数组的数据集到磁盘的工具 + ++ 线性代数运算、傅里叶变换和随机数生成 + ++ 成熟的 C API,用于使 Python 扩展和本地 C 或 C++代码能够访问 NumPy 的数据结构和计算功能 + +除了 NumPy 为 Python 增加的快速数组处理功能外,它在数据分析中的主要用途之一是作为数据容器,在算法和库之间传递数据。对于数值数据,NumPy 数组比其他内置 Python 数据结构更有效地存储和操作数据。此外,使用低级语言(如 C 或 FORTRAN)编写的库可以在 NumPy 数组中存储的数据上操作,而无需将数据复制到其他内存表示中。因此,许多 Python 的数值计算工具要么将 NumPy 数组作为主要数据结构,要么针对与 NumPy 的互操作性。 + +### pandas + +[pandas](https://pandas.pydata.org)提供了高级数据结构和函数,旨在使处理结构化或表格数据变得直观和灵活。自 2010 年出现以来,它已经帮助 Python 成为一个强大和高效的数据分析环境。本书中将使用的 pandas 中的主要对象是 DataFrame,这是一个表格化的、以列为导向的数据结构,具有行和列标签,以及 Series,这是一个一维带标签的数组对象。 + +pandas 将 NumPy 的数组计算思想与电子表格和关系数据库(如 SQL)中发现的数据操作能力相结合。它提供了方便的索引功能,使您能够重新塑造、切片、执行聚合操作和选择数据子集。由于数据操作、准备和清理在数据分析中是如此重要,pandas 是本书的主要关注点之一。 + +作为背景,我在 2008 年初在 AQR Capital Management 期间开始构建 pandas,这是一家量化投资管理公司。当时,我有一套明确的要求,任何单一工具都无法很好地满足: + ++ 具有带有标签轴的数据结构,支持自动或显式数据对齐——这可以防止由于数据不对齐和来自不同来源的不同索引数据而导致的常见错误 + ++ 集成的时间序列功能 + ++ 相同的数据结构处理时间序列数据和非时间序列数据 + ++ 保留元数据的算术操作和减少 + ++ 灵活处理缺失数据 + ++ 在流行数据库(例如基于 SQL 的数据库)中找到的合并和其他关系操作 + +我希望能够在一个地方完成所有这些事情,最好是在一种适合通用软件开发的语言中。Python 是这方面的一个很好的候选语言,但当时并不存在一个集成了这些功能的数据结构和工具集。由于最初构建是为了解决金融和业务分析问题,pandas 具有特别深入的时间序列功能和适用于处理由业务流程生成的时间索引数据的工具。 + +我在 2011 年和 2012 年的大部分时间里与我以前的 AQR 同事 Adam Klein 和 Chang She 一起扩展了 pandas 的功能。2013 年,我停止了日常项目开发的参与,pandas 自那时起已成为一个完全由社区拥有和维护的项目,全球范围内有超过两千名独特贡献者。 + +对于使用 R 语言进行统计计算的用户,DataFrame 这个名字将是熟悉的,因为该对象是根据类似的 R `data.frame`对象命名的。与 Python 不同,数据框内置于 R 编程语言及其标准库中。因此,pandas 中许多功能通常要么是 R 核心实现的一部分,要么是由附加包提供的。 + +pandas 这个名字本身来源于*panel data*,这是一个描述多维结构化数据集的计量经济学术语,也是对*Python 数据分析*这个短语的一种变换。 + +### matplotlib + +[matplotlib](https://matplotlib.org)是用于生成图表和其他二维数据可视化的最流行的 Python 库。最初由 John D. Hunter 创建,现在由一个庞大的开发团队维护。它专为创建适合出版的图表而设计。虽然 Python 程序员可以使用其他可视化库,但 matplotlib 仍然被广泛使用,并且与生态系统的其他部分相当好地集成。我认为它是默认可视化工具的一个安全选择。 + +### IPython 和 Jupyter + +[IPython 项目](https://ipython.org)始于 2001 年,是 Fernando Pérez 的一个副业项目,旨在打造更好的交互式 Python 解释器。在随后的 20 年里,它已成为现代 Python 数据堆栈中最重要的工具之一。虽然它本身不提供任何计算或数据分析工具,但 IPython 旨在用于交互式计算和软件开发工作。它鼓励*执行-探索*工作流程,而不是许多其他编程语言的典型*编辑-编译-运行*工作流程。它还提供了对操作系统的 shell 和文件系统的集成访问;这在许多情况下减少了在终端窗口和 Python 会话之间切换的需求。由于许多数据分析编码涉及探索、试错和迭代,IPython 可以帮助您更快地完成工作。 + +2014 年,Fernando 和 IPython 团队宣布了[Jupyter 项目](https://jupyter.org),这是一个更广泛的倡议,旨在设计与语言无关的交互式计算工具。IPython 网络笔记本变成了 Jupyter 笔记本,现在支持超过 40 种编程语言。IPython 系统现在可以作为使用 Python 与 Jupyter 的*内核*(编程语言模式)。 + +IPython 本身已成为更广泛的 Jupyter 开源项目的组成部分,为交互式和探索性计算提供了一个高效的环境。它最古老和最简单的“模式”是作为一个增强的 Python shell,旨在加速 Python 代码的编写、测试和调试。您还可以通过 Jupyter 笔记本使用 IPython 系统。 + +Jupyter 笔记本系统还允许您在 Markdown 和 HTML 中编写内容,为您提供了一种创建包含代码和文本的丰富文档的方式。 + +我个人经常在我的 Python 工作中使用 IPython 和 Jupyter,无论是运行、调试还是测试代码。 + +在[GitHub 上的附带书籍材料](https://github.com/wesm/pydata-book)中,您将找到包含每章代码示例的 Jupyter 笔记本。如果您无法访问 GitHub,您可以尝试[Gitee 上的镜像](https://gitee.com/wesmckinn/pydata-book)。 + +### SciPy + +[SciPy](https://scipy.org)是一个解决科学计算中一些基础问题的包集合。以下是它在各个模块中包含的一些工具: + +`scipy.integrate` + +数值积分例程和微分方程求解器 + +`scipy.linalg` + +线性代数例程和矩阵分解,扩展到`numpy.linalg`提供的范围之外 + +`scipy.optimize` + +函数优化器(最小化器)和根查找算法 + +`scipy.signal` + +信号处理工具 + +`scipy.sparse` + +稀疏矩阵和稀疏线性系统求解器 + +`scipy.special` + +SPECFUN 的包装器,一个实现许多常见数学函数(如`gamma`函数)的 FORTRAN 库 + +`scipy.stats` + +标准连续和离散概率分布(密度函数、采样器、连续分布函数)、各种统计检验和更多描述性统计 + +NumPy 和 SciPy 共同构成了许多传统科学计算应用的相当完整和成熟的计算基础。 + +### scikit-learn + +自 2007 年项目开始以来,[scikit-learn](https://scikit-learn.org)已成为 Python 程序员的首选通用机器学习工具包。截至撰写本文时,超过两千名不同的个人为该项目贡献了代码。它包括用于以下模型的子模块: + ++ 分类:SVM、最近邻、随机森林、逻辑回归等 + ++ 回归:Lasso、岭回归等 + ++ 聚类:*k*-means、谱聚类等 + ++ 降维:PCA、特征选择、矩阵分解等 + ++ 模型选择:网格搜索、交叉验证、度量 + ++ 预处理:特征提取、归一化 + +除了 pandas、statsmodels 和 IPython 之外,scikit-learn 对于使 Python 成为一种高效的数据科学编程语言至关重要。虽然我无法在本书中包含对 scikit-learn 的全面指南,但我将简要介绍一些其模型以及如何将其与本书中提供的其他工具一起使用。 + +### statsmodels + +[statsmodels](https://statsmodels.org)是一个统计分析包,由斯坦福大学统计学教授 Jonathan Taylor 的工作启发而来,他实现了 R 编程语言中流行的一些回归分析模型。Skipper Seabold 和 Josef Perktold 于 2010 年正式创建了新的 statsmodels 项目,自那时以来,该项目已经发展成为一群积极参与的用户和贡献者。Nathaniel Smith 开发了 Patsy 项目,该项目提供了一个受 R 公式系统启发的用于 statsmodels 的公式或模型规范框架。 + +与 scikit-learn 相比,statsmodels 包含用于经典(主要是频率主义)统计和计量经济学的算法。这包括诸如: + ++ 回归模型:线性回归、广义线性模型、鲁棒线性模型、线性混合效应模型等 + ++ 方差分析(ANOVA) + ++ 时间序列分析:AR、ARMA、ARIMA、VAR 和其他模型 + ++ 非参数方法:核密度估计、核回归 + ++ 统计模型结果的可视化 + +statsmodels 更专注于统计推断,为参数提供不确定性估计和*p*-值。相比之下,scikit-learn 更注重预测。 + +与 scikit-learn 一样,我将简要介绍 statsmodels 以及如何与 NumPy 和 pandas 一起使用它。 + +### 其他包 + +在 2022 年,有许多其他 Python 库可能会在关于数据科学的书中讨论。这包括一些较新的项目,如 TensorFlow 或 PyTorch,这些项目已经成为机器学习或人工智能工作中流行的工具。现在有其他更专注于这些项目的书籍,我建议使用本书来建立通用 Python 数据处理的基础。然后,您应该准备好转向更高级的资源,这些资源可能假定一定水平的专业知识。 + +## 1.4 安装和设置 + +由于每个人都在不同的应用中使用 Python,因此设置 Python 并获取必要的附加包没有单一的解决方案。许多读者可能没有完整的 Python 开发环境,适合跟随本书,因此我将在每个操作系统上提供详细的设置说明。我将使用 Miniconda,这是 conda 软件包管理器的最小安装,以及[conda-forge](https://conda-forge.org),这是一个基于 conda 的社区维护的软件分发。本书始终使用 Python 3.10,但如果您是在未来阅读,欢迎安装更新版本的 Python。 + +如果由于某种原因,这些说明在您阅读时已过时,您可以查看[我的书籍网站](https://wesmckinney.com/book),我将努力保持最新安装说明的更新。 + +### Windows 上的 Miniconda + +要在 Windows 上开始,请从[*https://conda.io*](https://conda.io)下载最新 Python 版本(目前为 3.9)的 Miniconda 安装程序。我建议按照 conda 网站上提供的 Windows 安装说明进行安装,这些说明可能在本书出版时和您阅读时之间发生了变化。大多数人会想要 64 位版本,但如果这在您的 Windows 机器上无法运行,您可以安装 32 位版本。 + +当提示是否仅为自己安装还是为系统上的所有用户安装时,请选择最适合您的选项。仅为自己安装将足以跟随本书。它还会询问您是否要将 Miniconda 添加到系统 PATH 环境变量中。如果选择此选项(我通常会这样做),则此 Miniconda 安装可能会覆盖您已安装的其他 Python 版本。如果不这样做,那么您将需要使用安装的 Window 开始菜单快捷方式才能使用此 Miniconda。此开始菜单条目可能称为“Anaconda3 (64 位)”。 + +我假设您还没有将 Miniconda 添加到系统路径中。要验证配置是否正确,请在“开始”菜单下的“Anaconda3 (64 位)”中打开“Anaconda Prompt (Miniconda3)”条目。然后尝试通过输入`python`来启动 Python 解释器。您应该会看到类似以下的消息: + +```py +(base) C:\Users\Wes>python +Python 3.9 [MSC v.1916 64 bit (AMD64)] :: Anaconda, Inc. on win32 +Type "help", "copyright", "credits" or "license" for more information. +>>> +``` + +要退出 Python shell,请输入`exit()`并按 Enter 键。 + +### GNU/Linux + +Linux 的详细信息会根据您的 Linux 发行版类型有所不同,但在这里我提供了 Debian、Ubuntu、CentOS 和 Fedora 等发行版的详细信息。设置与 macOS 类似,唯一的区别是 Miniconda 的安装方式。大多数读者会想要下载默认的 64 位安装程序文件,这是针对 x86 架构的(但未来可能会有更多用户使用基于 aarch64 的 Linux 机器)。安装程序是一个必须在终端中执行的 shell 脚本。然后您将会得到一个类似*Miniconda3-latest-Linux-x86_64.sh*的文件。要安装它,请使用`bash`执行此脚本: + +```py +$ bash Miniconda3-latest-Linux-x86_64.sh +``` + +注意 + +一些 Linux 发行版在其软件包管理器中具有所有所需的 Python 软件包(在某些情况下是过时版本),可以使用类似 apt 的工具进行安装。这里描述的设置使用 Miniconda,因为它在各种发行版中都很容易重现,并且更简单地升级软件包到最新版本。 + +您可以选择将 Miniconda 文件放在哪里。我建议将文件安装在您的主目录中的默认位置;例如,*/home/$USER/miniconda*(自然包括您的用户名)。 + +安装程序会询问您是否希望修改您的 shell 脚本以自动激活 Miniconda。我建议这样做(选择“是”)以方便起见。 + +安装完成后,启动一个新的终端进程并验证您是否已经安装了新的 Miniconda: + +```py +(base) $ python +Python 3.9 | (main) [GCC 10.3.0] on linux +Type "help", "copyright", "credits" or "license" for more information. +>>> +``` + +要退出 Python shell,请输入`exit()`并按 Enter 键或按 Ctrl-D。*### macOS 上的 Miniconda + +下载 macOS Miniconda 安装程序,应该命名为*Miniconda3-latest-MacOSX-arm64.sh*,适用于 2020 年以后发布的基于 Apple Silicon 的 macOS 计算机,或者*Miniconda3-latest-MacOSX-x86_64.sh*,适用于 2020 年之前发布的基于 Intel 的 Mac。在 macOS 中打开终端应用程序,并通过使用`bash`执行安装程序(很可能在您的`Downloads`目录中)来安装: + +```py +$ bash $HOME/Downloads/Miniconda3-latest-MacOSX-arm64.sh +``` + +当安装程序运行时,默认情况下会自动在默认 shell 环境和默认 shell 配置文件中配置 Miniconda。这可能位于*/Users/$USER/.zshrc*。我建议让它这样做;如果您不想让安装程序修改默认的 shell 环境,您需要查阅 Miniconda 文档以便继续。 + +要验证一切是否正常工作,请尝试在系统 shell 中启动 Python(打开终端应用程序以获取命令提示符): + +```py +$ python +Python 3.9 (main) [Clang 12.0.1 ] on darwin +Type "help", "copyright", "credits" or "license" for more information. +>>> +``` + +要退出 shell,请按 Ctrl-D 或输入`exit()`并按 Enter 键。 + +### 安装必要的软件包 + +现在我们已经在您的系统上设置了 Miniconda,是时候安装本书中将要使用的主要软件包了。第一步是通过在 shell 中运行以下命令将 conda-forge 配置为您的默认软件包渠道: + +```py +(base) $ conda config --add channels conda-forge +(base) $ conda config --set channel_priority strict +``` + +现在,我们将使用 Python 3.10 使用`conda create`命令创建一个新的 conda“环境”: + +```py +(base) $ conda create -y -n pydata-book python=3.10 +``` + +安装完成后,请使用`conda activate`激活环境: + +```py +(base) $ conda activate pydata-book +(pydata-book) $ +``` + +注意 + +每次打开新终端时,都需要使用`conda activate`来激活您的环境。您可以随时通过在终端中运行`conda info`来查看有关活动 conda 环境的信息。 + +现在,我们将使用`conda install`安装整本书中使用的基本软件包(以及它们的依赖项): + +```py +(pydata-book) $ conda install -y pandas jupyter matplotlib +``` + +我们还将使用其他软件包,但这些软件包可以在需要时稍后安装。有两种安装软件包的方法:使用`conda install`和`pip install`。在使用 Miniconda 时,应始终优先使用`conda install`,但某些软件包无法通过 conda 获得,因此如果`conda install $package_name`失败,请尝试`pip install $package_name`。 + +注意 + +如果您想安装本书其余部分使用的所有软件包,现在可以通过运行: + +```py +conda install lxml beautifulsoup4 html5lib openpyxl \ + requests sqlalchemy seaborn scipy statsmodels \ + patsy scikit-learn pyarrow pytables numba +``` + +在 Windows 上,将`^`替换为 Linux 和 macOS 上使用的行继续符`\`。 + +您可以使用`conda` `update`命令更新软件包: + +```py +conda update package_name +``` + +pip 还支持使用`--upgrade`标志进行升级: + +```py +pip install --upgrade package_name +``` + +您将有机会在整本书中尝试这些命令。 + +注意 + +虽然您可以使用 conda 和 pip 来安装软件包,但应避免使用 pip 更新最初使用 conda 安装的软件包(反之亦然),因为这样做可能会导致环境问题。我建议尽可能使用 conda,并仅在无法使用`conda install`安装软件包时才回退到 pip。 + +### 集成开发环境和文本编辑器 + +当被问及我的标准开发环境时,我几乎总是说“IPython 加上文本编辑器”。我通常会在 IPython 或 Jupyter 笔记本中编写程序,并逐步测试和调试每个部分。交互式地玩弄数据并直观验证特定数据操作是否正确也是很有用的。像 pandas 和 NumPy 这样的库旨在在 shell 中使用时提高生产力。 + +然而,在构建软件时,一些用户可能更喜欢使用功能更丰富的集成开发环境(IDE),而不是像 Emacs 或 Vim 这样的编辑器,后者在开箱即用时提供了更简洁的环境。以下是一些您可以探索的内容: + ++ PyDev(免费),基于 Eclipse 平台构建的 IDE + ++ 来自 JetBrains 的 PyCharm(面向商业用户的订阅制,对于开源开发者免费) + ++ Visual Studio 的 Python 工具(适用于 Windows 用户) + ++ Spyder(免费),目前与 Anaconda 捆绑的 IDE + ++ Komodo IDE(商业版) + +由于 Python 的流行,大多数文本编辑器,如 VS Code 和 Sublime Text 2,都具有出色的 Python 支持。 + +## 1.5 社区和会议 + +除了通过互联网搜索外,各种科学和数据相关的 Python 邮件列表通常对问题有帮助并且响应迅速。一些可以参考的包括: + ++ pydata:一个 Google Group 列表,用于与 Python 数据分析和 pandas 相关的问题 + ++ pystatsmodels:用于 statsmodels 或与 pandas 相关的问题 + ++ scikit-learn 邮件列表(*scikit-learn@python.org*)和 Python 中的机器学习,一般 + ++ numpy-discussion:用于与 NumPy 相关的问题 + ++ scipy-user:用于一般 SciPy 或科学 Python 问题 + +我故意没有发布这些 URL,以防它们发生变化。它们可以通过互联网搜索轻松找到。 + +每年举办许多全球各地的 Python 程序员会议。如果您想与其他分享您兴趣的 Python 程序员联系,我鼓励您尽可能参加其中一个。许多会议为那些无法支付入场费或旅行费的人提供财政支持。以下是一些可以考虑的会议: + ++ PyCon 和 EuroPython:分别是在北美和欧洲举办的两个主要的一般 Python 会议 + ++ SciPy 和 EuroSciPy:分别是在北美和欧洲举办的面向科学计算的会议 + ++ PyData:面向数据科学和数据分析用例的全球系列区域会议 + ++ 国际和地区 PyCon 会议(请参阅 [`pycon.org`](https://pycon.org) 获取完整列表) + +## 1.6 浏览本书 + +如果您以前从未在 Python 中编程过,您可能需要花一些时间阅读 第二章:Python 语言基础、IPython 和 Jupyter Notebooks 和 第三章:内置数据结构、函数和文件,我在这里放置了有关 Python 语言特性、IPython shell 和 Jupyter notebooks 的简明教程。这些内容是本书其余部分的先决知识。如果您已经有 Python 经验,您可以选择略读或跳过这些章节。 + +接下来,我简要介绍了 NumPy 的关键特性,将更高级的 NumPy 使用留给 附录 A:高级 NumPy。然后,我介绍了 pandas,并将本书的其余部分专注于应用 pandas、NumPy 和 matplotlib 进行数据分析主题(用于可视化)。我以递增的方式组织了材料,尽管在章节之间偶尔会有一些轻微的交叉,有些概念可能尚未介绍。 + +尽管读者可能对他们的工作有许多不同的最终目标,但通常所需的任务大致可以分为许多不同的广泛组别: + +与外部世界互动 + +使用各种文件格式和数据存储进行读写 + +准备 + +清理、整理、合并、规范化、重塑、切片和切块以及转换数据以进行分析 + +转换 + +对数据集组应用数学和统计操作以派生新数据集(例如,通过组变量对大表进行聚合) + +建模和计算 + +将您的数据连接到统计模型、机器学习算法或其他计算工具 + +演示 + +创建交互式或静态图形可视化或文本摘要 + +### 代码示例 + +本书中的大多数代码示例都显示了输入和输出,就像在 IPython shell 或 Jupyter notebooks 中执行时一样: + +```py +In [5]: CODE EXAMPLE +Out[5]: OUTPUT +``` + +当您看到像这样的代码示例时,意图是让您在编码环境中的 `In` 区块中键入示例代码,并通过按 Enter 键(或在 Jupyter 中按 Shift-Enter)执行它。您应该看到类似于 `Out` 区块中显示的输出。 + +我已更改了 NumPy 和 pandas 的默认控制台输出设置,以提高本书的可读性和简洁性。例如,您可能会看到在数字数据中打印更多位数的精度。要完全匹配书中显示的输出,您可以在运行代码示例之前执行以下 Python 代码: + +```py +import numpy as np +import pandas as pd +pd.options.display.max_columns = 20 +pd.options.display.max_rows = 20 +pd.options.display.max_colwidth = 80 +np.set_printoptions(precision=4, suppress=True) +``` + +### 示例数据 + +每一章的示例数据集都托管在 [GitHub 仓库](https://github.com/wesm/pydata-book) 中(如果无法访问 GitHub,则可以在 [Gitee 上的镜像](https://gitee.com/wesmckinn/pydata-book))。您可以通过使用 Git 版本控制系统在命令行上下载这些数据,或者通过从网站下载仓库的 zip 文件来获取数据。如果遇到问题,请转到 [书籍网站](https://wesmckinney.com/book) 获取有关获取书籍材料的最新说明。 + +如果您下载包含示例数据集的 zip 文件,则必须完全提取 zip 文件的内容到一个目录,并在终端中导航到该目录,然后才能继续运行本书的代码示例: + +```py +$ pwd +/home/wesm/book-materials + +$ ls +appa.ipynb ch05.ipynb ch09.ipynb ch13.ipynb README.md +ch02.ipynb ch06.ipynb ch10.ipynb COPYING requirements.txt +ch03.ipynb ch07.ipynb ch11.ipynb datasets +ch04.ipynb ch08.ipynb ch12.ipynb examples +``` + +我已尽一切努力确保 GitHub 仓库包含重现示例所需的一切,但可能会出现一些错误或遗漏。如果有的话,请发送邮件至:*book@wesmckinney.com*。报告书中错误的最佳方式是在 [O'Reilly 网站上的勘误页面](https://oreil.ly/kmhmQ)上。 + +### 导入约定 + +Python 社区已经采用了许多常用模块的命名约定: + +```py +import numpy as np +import matplotlib.pyplot as plt +import pandas as pd +import seaborn as sns +import statsmodels as sm +``` + +这意味着当你看到`np.arange`时,这是对 NumPy 中`arange`函数的引用。这样做是因为在 Python 软件开发中,从像 NumPy 这样的大型包中导入所有内容(`from numpy import *`)被认为是不良实践。 diff --git a/translations/cn/pyda3e_05.md b/translations/cn/pyda3e_05.md new file mode 100644 index 000000000..7a864a6c8 --- /dev/null +++ b/translations/cn/pyda3e_05.md @@ -0,0 +1,1303 @@ +# 二、Python 语言基础,IPython 和 Jupyter 笔记本 + +> 原文:[`wesmckinney.com/book/python-basics`](https://wesmckinney.com/book/python-basics) +> +> 译者:[飞龙](https://github.com/wizardforcel) +> +> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) + + +> 此开放访问网络版本的《Python 数据分析第三版》现已作为[印刷版和数字版](https://amzn.to/3DyLaJc)的伴侣提供。如果您发现任何勘误,请[在此处报告](https://oreilly.com/catalog/0636920519829/errata)。请注意,由 Quarto 生成的本站点的某些方面与 O'Reilly 的印刷版和电子书版本的格式不同。 +> +> 如果您发现本书的在线版本有用,请考虑[订购纸质版](https://amzn.to/3DyLaJc)或[无 DRM 的电子书](https://www.ebooks.com/en-us/book/210644288/python-for-data-analysis/wes-mckinney/?affId=WES398681F)以支持作者。本网站的内容不得复制或再生产。代码示例采用 MIT 许可,可在 GitHub 或 Gitee 上找到。 + +当我在 2011 年和 2012 年编写本书的第一版时,关于在 Python 中进行数据分析的学习资源较少。这在一定程度上是一个鸡生蛋的问题;许多我们现在认为理所当然的库,如 pandas、scikit-learn 和 statsmodels,在那时相对不成熟。现在是 2022 年,现在有越来越多关于数据科学、数据分析和机器学习的文献,补充了关于面向计算科学家、物理学家和其他研究领域专业人士的通用科学计算的先前作品。还有关于学习 Python 编程语言本身并成为有效软件工程师的优秀书籍。 + +由于本书旨在作为在 Python 中处理数据的入门文本,我认为从数据操作的角度对 Python 的内置数据结构和库的一些最重要特性进行自包含概述是有价值的。因此,我将在本章和第三章:内置数据结构、函数和文件中提供大致足够的信息,以便您能够跟随本书的其余部分。 + +本书的很大一部分关注于基于表格的分析和数据准备工具,用于处理足够小以适合个人计算机的数据集。要使用这些工具,有时您必须对混乱的数据进行一些整理,将其整理成更整洁的表格(或*结构化*)形式。幸运的是,Python 是做这些事情的理想语言。您对 Python 语言及其内置数据类型的熟练程度越高,准备新数据集进行分析就会变得更容易。 + +本书中的一些工具最好在实时的 IPython 或 Jupyter 会话中进行探索。一旦您学会如何启动 IPython 和 Jupyter,我建议您跟着示例进行实验和尝试不同的东西。与任何基于键盘的类似控制台的环境一样,熟悉常用命令也是学习曲线的一部分。 + +注意:本章未涵盖一些入门级 Python 概念,如类和面向对象编程,这些概念在您进入 Python 数据分析领域时可能会有用。 + +为了加深您对 Python 语言的了解,我建议您将本章与[官方 Python 教程](http://docs.python.org)以及可能是许多优秀的通用 Python 编程书籍结合起来阅读。一些建议让您开始包括: + ++ 《Python Cookbook》,第三版,作者 David Beazley 和 Brian K. Jones(O'Reilly) + ++ 《流畅的 Python》,作者 Luciano Ramalho(O'Reilly) + ++ 《Effective Python》,第二版,作者 Brett Slatkin(Addison-Wesley)## 2.1 Python 解释器 + +Python 是一种解释性语言。Python 解释器通过逐条执行程序来运行程序。标准的交互式 Python 解释器可以通过命令行使用`python`命令调用: + +```py +$ python +Python 3.10.4 | packaged by conda-forge | (main, Mar 24 2022, 17:38:57) +[GCC 10.3.0] on linux +Type "help", "copyright", "credits" or "license" for more information. +>>> a = 5 +>>> print(a) +5 +``` + +您看到的`>>>`是您将输入代码表达式的*提示*。要退出 Python 解释器,您可以输入`exit()`或按 Ctrl-D(仅适用于 Linux 和 macOS)。 + +运行 Python 程序就像调用`python`并将*.py*文件作为第一个参数一样简单。假设我们已经创建了包含以下内容的*hello_world.py*文件: + +```py +print("Hello world") +``` + +您可以通过执行以下命令来运行它(*hello_world.py*文件必须在您当前的工作终端目录中): + +```py +$ python hello_world.py +Hello world +``` + +虽然一些 Python 程序员以这种方式执行他们的所有 Python 代码,但进行数据分析或科学计算的人使用 IPython,这是一个增强的 Python 解释器,或者使用 Jupyter 笔记本,这是最初在 IPython 项目中创建的基于 Web 的代码笔记本。我在本章中介绍了如何使用 IPython 和 Jupyter,并在附录 A:高级 NumPy 中深入探讨了 IPython 功能。当您使用`%run`命令时,IPython 会在同一进程中执行指定文件中的代码,使您能够在完成时交互地探索结果: + +```py +$ ipython +Python 3.10.4 | packaged by conda-forge | (main, Mar 24 2022, 17:38:57) +Type 'copyright', 'credits' or 'license' for more information +IPython 7.31.1 -- An enhanced Interactive Python. Type '?' for help. + +In [1]: %run hello_world.py +Hello world + +In [2]: +``` + +默认的 IPython 提示采用带编号的`In [2]:`样式,与标准的`>>>`提示相比。 + +## 2.2 IPython 基础知识 + +在本节中,我将带您快速了解 IPython shell 和 Jupyter 笔记本,并介绍一些基本概念。 + +### 运行 IPython Shell + +您可以像启动常规 Python 解释器一样在命令行上启动 IPython shell,只是使用`ipython`命令: + +```py +$ ipython +Python 3.10.4 | packaged by conda-forge | (main, Mar 24 2022, 17:38:57) +Type 'copyright', 'credits' or 'license' for more information +IPython 7.31.1 -- An enhanced Interactive Python. Type '?' for help. + +In [1]: a = 5 + +In [2]: a +Out[2]: 5 +``` + +您可以通过键入 Python 语句并按回车键(或 Enter 键)来执行任意 Python 语句。当您只输入一个变量时,IPython 会呈现对象的字符串表示: + +```py +In [5]: import numpy as np + +In [6]: data = [np.random.standard_normal() for i in range(7)] + +In [7]: data +Out[7]: +[-0.20470765948471295, + 0.47894333805754824, + -0.5194387150567381, + -0.55573030434749, + 1.9657805725027142, + 1.3934058329729904, + 0.09290787674371767] +``` + +前两行是 Python 代码语句;第二个语句创建了一个名为`data`的变量,指向一个新创建的列表。最后一行在控制台中打印了`data`的值。 + +许多种类的 Python 对象都被格式化为更易读或*漂亮打印*,这与使用`print`进行正常打印不同。如果您在标准 Python 解释器中打印上述`data`变量,它将不太易读: + +```py +>>> import numpy as np +>>> data = [np.random.standard_normal() for i in range(7)] +>>> print(data) +>>> data +[-0.5767699931966723, -0.1010317773535111, -1.7841005313329152, +-1.524392126408841, 0.22191374220117385, -1.9835710588082562, +-1.6081963964963528] +``` + +IPython 还提供了执行任意代码块(通过一种略微夸张的复制粘贴方法)和整个 Python 脚本的功能。您还可以使用 Jupyter 笔记本来处理更大的代码块,我们很快就会看到。 + +### 在终端中运行 Jupyter Notebook + +Jupyter 项目的一个主要组件是*笔记本*,一种用于代码、文本(包括 Markdown)、数据可视化和其他输出的交互式文档。Jupyter 笔记本与*内核*交互,这些内核是针对不同编程语言的 Jupyter 交互式计算协议的实现。Python Jupyter 内核使用 IPython 系统作为其基础行为。 + +要启动 Jupyter,请在终端中运行命令`jupyter` `notebook`: + +```py +$ jupyter notebook +[I 15:20:52.739 NotebookApp] Serving notebooks from local directory: +/home/wesm/code/pydata-book +[I 15:20:52.739 NotebookApp] 0 active kernels +[I 15:20:52.739 NotebookApp] The Jupyter Notebook is running at: +http://localhost:8888/?token=0a77b52fefe52ab83e3c35dff8de121e4bb443a63f2d... +[I 15:20:52.740 NotebookApp] Use Control-C to stop this server and shut down +all kernels (twice to skip confirmation). +Created new window in existing browser session. + To access the notebook, open this file in a browser: + file:///home/wesm/.local/share/jupyter/runtime/nbserver-185259-open.html + Or copy and paste one of these URLs: + http://localhost:8888/?token=0a77b52fefe52ab83e3c35dff8de121e4... + or http://127.0.0.1:8888/?token=0a77b52fefe52ab83e3c35dff8de121e4... +``` + +在许多平台上,Jupyter 会自动在默认的 Web 浏览器中打开(除非您使用`--no-browser`启动)。否则,您可以导航到启动笔记本时打印的 HTTP 地址,例如`http://localhost:8888/?token=0a77b52fefe52ab83e3c35dff8de121e4bb443a63f2d3055`。在 Google Chrome 中查看图 2.1。 + +注意 + +许多人将 Jupyter 用作本地计算环境,但它也可以部署在服务器上并远程访问。我不会在这里涵盖这些细节,但如果这与您的需求相关,我鼓励您在互联网上探索这个主题。 + +![](img/1cd631e8481f11b53a8555983ea31c1e.png) + +图 2.1:Jupyter 笔记本首页 + +要创建一个新笔记本,点击“New”按钮并选择“Python 3”选项。您应该看到类似于图 2.2 的内容。如果这是您第一次尝试,请尝试点击空的代码“单元格”并输入一行 Python 代码。然后按 Shift-Enter 执行它。 + +![](img/7f99b0a8aa2805183d2316e51eba5f22.png) + +图 2.2:Jupyter 新笔记本视图 + +当您保存笔记本(请参见笔记本文件菜单下的“保存和检查点”)时,它将创建一个扩展名为*.ipynb*的文件。这是一种自包含的文件格式,包含当前笔记本中的所有内容(包括任何已评估的代码输出)。其他 Jupyter 用户可以加载和编辑这些文件。 + +要重命名打开的笔记本,请单击页面顶部的笔记本标题,然后键入新标题,完成后按 Enter。 + +要加载现有笔记本,请将文件放在启动笔记本进程的同一目录中(或其中的子文件夹),然后从登录页面点击名称。您可以尝试使用 GitHub 上我的*wesm/pydata-book*存储库中的笔记本。请参见图 2.3。 + +当您想要关闭笔记本时,请单击文件菜单,然后选择“关闭并停止”。如果您只是关闭浏览器选项卡,则与笔记本相关联的 Python 进程将继续在后台运行。 + +虽然 Jupyter 笔记本可能感觉与 IPython shell 有所不同,但本章中的几乎所有命令和工具都可以在任何环境中使用。 + +![](img/85dffdfb28f327f6e100f02d45b76f91.png) + +图 2.3:现有笔记本的 Jupyter 示例视图 + +### Tab Completion + +从表面上看,IPython shell 看起来像标准终端 Python 解释器的外观不同版本(使用`python`调用)。与标准 Python shell 相比,IPython shell 的一个主要改进是*制表完成*,在许多 IDE 或其他交互式计算分析环境中都可以找到。在 shell 中输入表达式时,按 Tab 键将搜索命名空间以查找与您迄今为止键入的字符匹配的任何变量(对象、函数等),并在方便的下拉菜单中显示结果: + +```py +In [1]: an_apple = 27 + +In [2]: an_example = 42 + +In [3]: an +an_apple an_example any +``` + +在此示例中,请注意 IPython 显示了我定义的两个变量以及内置函数`any`。此外,在键入句点后,您还可以完成任何对象的方法和属性: + +```py +In [3]: b = [1, 2, 3] + +In [4]: b. +append() count() insert() reverse() +clear() extend() pop() sort() +copy() index() remove() +``` + +模块也是如此: + +```py +In [1]: import datetime + +In [2]: datetime. +date MAXYEAR timedelta +datetime MINYEAR timezone +datetime_CAPI time tzinfo +``` + +注意 + +请注意,默认情况下,IPython 隐藏以下划线开头的方法和属性,例如魔术方法和内部“私有”方法和属性,以避免显示混乱(并使初学者感到困惑!)。这些也可以通过制表完成,但您必须首先键入下划线才能看到它们。如果您希望始终在制表完成中看到此类方法,请更改 IPython 配置中的此设置。请参阅[IPython 文档](https://ipython.readthedocs.io)以了解如何执行此操作。 + +制表完成在许多上下文中起作用,不仅限于搜索交互式命名空间并完成对象或模块属性。在键入任何看起来像文件路径的内容(即使在 Python 字符串中),按 Tab 键将完成与您键入的内容匹配的计算机文件系统上的任何内容。 + +结合`%run`命令(请参见附录 B.2.1:`%run`命令),此功能可以为您节省许多按键。 + +制表完成还可以节省函数关键字参数(包括`=`符号!)的完成时间。请参见图 2.4。 + +![](img/9ddeddd0816344270288957f3493eb63.png) + +图 2.4:在 Jupyter 笔记本中自动完成函数关键字 + +我们稍后将更仔细地查看函数。 + +### 内省 + +在变量前或后使用问号(`?`)将显示有关对象的一些常规信息: + +```py +In [1]: b = [1, 2, 3] + +In [2]: b? +Type: list +String form: [1, 2, 3] +Length: 3 +Docstring: +Built-in mutable sequence. + +If no argument is given, the constructor creates a new empty list. +The argument must be an iterable if specified. + +In [3]: print? +Docstring: +print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False) + +Prints the values to a stream, or to sys.stdout by default. +Optional keyword arguments: +file: a file-like object (stream); defaults to the current sys.stdout. +sep: string inserted between values, default a space. +end: string appended after the last value, default a newline. +flush: whether to forcibly flush the stream. +Type: builtin_function_or_method +``` + +这被称为*对象内省*。如果对象是函数或实例方法,则如果定义了文档字符串,它也将显示出来。假设我们编写了以下函数(您可以在 IPython 或 Jupyter 中重现): + +```py +def add_numbers(a, b): + """ + Add two numbers together + + Returns + ------- + the_sum : type of arguments + """ + return a + b +``` + +然后使用`?`显示文档字符串: + +```py +In [6]: add_numbers? +Signature: add_numbers(a, b) +Docstring: +Add two numbers together +Returns +------- +the_sum : type of arguments +File: +Type: function +``` + +`?`还有一个最终的用途,就是在 IPython 命名空间中进行搜索,类似于标准的 Unix 或 Windows 命令行。与通配符(`*`)结合的一系列字符将显示所有与通配符表达式匹配的名称。例如,我们可以获取包含`load`的顶级 NumPy 命名空间中的所有函数列表: + +```py +In [9]: import numpy as np + +In [10]: np.*load*? +np.__loader__ +np.load +np.loads +np.loadtxt +``` + +## 2.3 Python 语言基础 + +在本节中,我将为您概述基本的 Python 编程概念和语言机制。在下一章中,我将更详细地介绍 Python 数据结构、函数和其他内置工具。 + +### 语言语义 + +Python 语言设计的一个重要特点是其对可读性、简单性和明确性的强调。有些人甚至将其比作“可执行的伪代码”。 + +#### 缩进,而不是大括号 + +Python 使用空格(制表符或空格)来结构化代码,而不是像 R、C++、Java 和 Perl 等许多其他语言那样使用大括号。考虑一个排序算法中的`for`循环: + +```py +for x in array: + if x < pivot: + less.append(x) + else: + greater.append(x) +``` + +冒号表示缩进代码块的开始,之后所有代码都必须缩进相同的量,直到块的结束。 + +无论你喜欢还是讨厌,对于 Python 程序员来说,有意义的空白是一个事实。虽然一开始可能会感到陌生,但希望你能逐渐习惯它。 + +注意 + +我强烈建议使用*四个空格*作为默认缩进,并用四个空格替换制表符。许多文本编辑器都有一个设置,可以自动将制表符替换为空格(请这样做!)。IPython 和 Jupyter 笔记本会在冒号后的新行自动插入四个空格,并用四个空格替换制表符。 + +正如你现在所看到的,Python 语句也不需要以分号结尾。但是,分号可以用来在单行上分隔多个语句: + +```py +a = 5; b = 6; c = 7 +``` + +在一行上放置多个语句通常在 Python 中是不鼓励的,因为这可能会使代码变得不太可读。 + +#### 一切都是对象 + +Python 语言的一个重要特点是其*对象模型*的一致性。每个数字、字符串、数据结构、函数、类、模块等都存在于 Python 解释器中的自己的“盒子”中,这被称为*Python 对象*。每个对象都有一个关联的*类型*(例如*整数*、*字符串*或*函数*)和内部数据。实际上,这使得语言非常灵活,因为即使函数也可以像任何其他对象一样对待。 + +#### 注释 + +由井号(井号)`#`引导的任何文本都会被 Python 解释器忽略。这通常用于向代码添加注释。有时您可能还想排除某些代码块而不删除它们。一种解决方案是*注释掉*代码: + +```py +results = [] +for line in file_handle: + # keep the empty lines for now + # if len(line) == 0: + # continue + results.append(line.replace("foo", "bar")) +``` + +注释也可以出现在执行代码的行之后。虽然一些程序员更喜欢将注释放在特定代码行之前的行中,但有时这样做也是有用的: + +```py +print("Reached this line") # Simple status report +``` + +#### 函数和对象方法调用 + +使用括号调用函数并传递零个或多个参数,可选地将返回的值赋给一个变量: + +```py +result = f(x, y, z) +g() +``` + +Python 中几乎每个对象都有附加的函数,称为*方法*,这些函数可以访问对象的内部内容。您可以使用以下语法调用它们: + +```py +obj.some_method(x, y, z) +``` + +函数可以接受*位置*和*关键字*参数: + +```py +result = f(a, b, c, d=5, e="foo") +``` + +我们稍后会更详细地看一下这个。 + +#### 变量和参数传递 + +在 Python 中赋值变量(或*名称*)时,您正在创建对等号右侧显示的对象的*引用*。在实际操作中,考虑一个整数列表: + +```py +In [8]: a = [1, 2, 3] +``` + +假设我们将`a`赋给一个新变量`b`: + +```py +In [9]: b = a + +In [10]: b +Out[10]: [1, 2, 3] +``` + +在一些语言中,对`b`的赋值将导致数据`[1, 2, 3]`被复制。在 Python 中,`a`和`b`实际上现在指向同一个对象,即原始列表`[1, 2, 3]`(请参见图 2.5 的模拟)。您可以通过向`a`附加一个元素,然后检查`b`来证明这一点: + +```py +In [11]: a.append(4) + +In [12]: b +Out[12]: [1, 2, 3, 4] +``` + +![](img/844beecf4a6f3e1c98bd50a928a5d379.png) + +图 2.5:同一对象的两个引用 + +了解 Python 中引用的语义以及何时、如何以及为什么数据被复制,在处理 Python 中的大型数据集时尤为重要。 + +注意 + +赋值也被称为*绑定*,因为我们正在将一个名称绑定到一个对象。已经分配的变量名称有时可能被称为绑定变量。 + +当您将对象作为参数传递给函数时,将创建新的本地变量引用原始对象,而不进行任何复制。如果在函数内部将一个新对象绑定到一个变量,那么它不会覆盖函数外部(“父范围”)具有相同名称的变量。因此,可以更改可变参数的内部。假设我们有以下函数: + +```py +In [13]: def append_element(some_list, element): + ....: some_list.append(element) +``` + +然后我们有: + +```py +In [14]: data = [1, 2, 3] + +In [15]: append_element(data, 4) + +In [16]: data +Out[16]: [1, 2, 3, 4] +``` + +#### 动态引用,强类型 + +Python 中的变量没有与之关联的固有类型;通过赋值,变量可以引用不同类型的对象。以下情况没有问题: + +```py +In [17]: a = 5 + +In [18]: type(a) +Out[18]: int + +In [19]: a = "foo" + +In [20]: type(a) +Out[20]: str +``` + +变量是特定命名空间内对象的名称;类型信息存储在对象本身中。一些观察者可能匆忙得出结论,认为 Python 不是一种“类型化语言”。这是不正确的;考虑这个例子: + +```py +In [21]: "5" + 5 +--------------------------------------------------------------------------- +TypeError Traceback (most recent call last) + in +----> 1 "5" + 5 +TypeError: can only concatenate str (not "int") to str +``` + +在某些语言中,字符串`'5'`可能会被隐式转换(或*转换*)为整数,从而得到 10。在其他语言中,整数`5`可能会被转换为字符串,从而得到连接的字符串`'55'`。在 Python 中,不允许这种隐式转换。在这方面,我们说 Python 是一种*强类型*语言,这意味着每个对象都有一个特定的类型(或*类*),隐式转换只会在某些允许的情况下发生,例如: + +```py +In [22]: a = 4.5 + +In [23]: b = 2 + +# String formatting, to be visited later +In [24]: print(f"a is {type(a)}, b is {type(b)}") +a is , b is + +In [25]: a / b +Out[25]: 2.25 +``` + +在这里,即使`b`是一个整数,它也会被隐式转换为浮点数进行除法运算。 + +了解对象的类型很重要,能够编写能够处理许多不同类型输入的函数也很有用。您可以使用`isinstance`函数检查对象是否是特定类型的实例: + +```py +In [26]: a = 5 + +In [27]: isinstance(a, int) +Out[27]: True +``` + +如果要检查对象的类型是否在元组中存在,`isinstance`可以接受一个类型元组: + +```py +In [28]: a = 5; b = 4.5 + +In [29]: isinstance(a, (int, float)) +Out[29]: True + +In [30]: isinstance(b, (int, float)) +Out[30]: True +``` + +#### 属性和方法 + +Python 中的对象通常具有属性(存储在对象“内部”的其他 Python 对象)和方法(与对象关联的函数,可以访问对象的内部数据)。它们都可以通过语法访问: + +```py +In [1]: a = "foo" + +In [2]: a. +capitalize() index() isspace() removesuffix() startswith() +casefold() isprintable() istitle() replace() strip() +center() isalnum() isupper() rfind() swapcase() +count() isalpha() join() rindex() title() +encode() isascii() ljust() rjust() translate() +endswith() isdecimal() lower() rpartition() +expandtabs() isdigit() lstrip() rsplit() +find() isidentifier() maketrans() rstrip() +format() islower() partition() split() +format_map() isnumeric() removeprefix() splitlines() +``` + +属性和方法也可以通过`getattr`函数按名称访问: + +```py +In [32]: getattr(a, "split") +Out[32]: +``` + +虽然我们在本书中不会广泛使用`getattr`函数和相关函数`hasattr`和`setattr`,但它们可以非常有效地用于编写通用的可重用代码。 + +#### 鸭子类型 + +通常,您可能不关心对象的类型,而只关心它是否具有某些方法或行为。这有时被称为*鸭子类型*,源自谚语“如果它走起来像鸭子,叫起来像鸭子,那么它就是鸭子。”例如,如果对象实现了*迭代器协议*,则可以验证该对象是否可迭代。对于许多对象,这意味着它具有一个`__iter__`“魔术方法”,尽管检查的另一种更好的方法是尝试使用`iter`函数: + +```py +In [33]: def isiterable(obj): + ....: try: + ....: iter(obj) + ....: return True + ....: except TypeError: # not iterable + ....: return False +``` + +对于字符串以及大多数 Python 集合类型,此函数将返回`True`: + +```py +In [34]: isiterable("a string") +Out[34]: True + +In [35]: isiterable([1, 2, 3]) +Out[35]: True + +In [36]: isiterable(5) +Out[36]: False +``` + +#### 导入 + +在 Python 中,*模块*只是一个包含 Python 代码的扩展名为*.py*的文件。假设我们有以下模块: + +```py +# some_module.py +PI = 3.14159 + +def f(x): + return x + 2 + +def g(a, b): + return a + b +``` + +如果我们想要从同一目录中的另一个文件中访问*some_module.py*中定义的变量和函数,我们可以这样做: + +```py +import some_module +result = some_module.f(5) +pi = some_module.PI +``` + +或者: + +```py +from some_module import g, PI +result = g(5, PI) +``` + +通过使用`as`关键字,您可以为导入指定不同的变量名称: + +```py +import some_module as sm +from some_module import PI as pi, g as gf + +r1 = sm.f(pi) +r2 = gf(6, pi) +``` + +#### 二进制运算符和比较 + +大多数二进制数学运算和比较使用其他编程语言中常用的数学语法: + +```py +In [37]: 5 - 7 +Out[37]: -2 + +In [38]: 12 + 21.5 +Out[38]: 33.5 + +In [39]: 5 <= 2 +Out[39]: False +``` + +查看表 2.1 以获取所有可用的二进制运算符。 + +表 2.1:二进制运算符 + +| 操作 | 描述 | +| --- | --- | +| `a + b` | 将`a`和`b`相加 | +| `a - b` | 从`a`中减去`b` | +| `a * b` | 将`a`乘以`b` | +| `a / b` | 将`a`除以`b` | +| `a // b` | 通过`b`进行地板除法,去除任何小数余数 | +| `a ** b` | 将`a`提升到`b`次方 | +| `a & b` | 如果`a`和`b`都为`True`,则为`True`;对于整数,取位`AND` | +| `a | b` | 如果`a`或`b`中的任何一个为`True`,则为`True`;对于整数,取位`OR` | +| `a ^ b` | 对于布尔值,如果`a`或`b`为`True`,但不是两者都为`True`;对于整数,取位`异或` | +| `a == b` | 如果`a`等于`b`,则为`True` | +| `a != b` | 如果`a`不等于`b`,则为`True` | +| `a < b`,a <= b | 如果`a`小于(小于或等于)`b`,则为`True` | +| `a > b, a >= b` | 如果`a`大于(大于或等于)`b`,则为`True` | +| `a is b` | 如果`a`和`b`引用相同的 Python 对象,则为`True` | +| `a is not b` | 如果`a`和`b`引用不同的 Python 对象,则为`True` | + +要检查两个变量是否引用同一对象,请使用`is`关键字。使用`is not`来检查两个对象是否不相同: + +```py +In [40]: a = [1, 2, 3] + +In [41]: b = a + +In [42]: c = list(a) + +In [43]: a is b +Out[43]: True + +In [44]: a is not c +Out[44]: True +``` + +由于`list`函数始终创建一个新的 Python 列表(即一个副本),我们可以确保`c`与`a`不同。与`==`运算符不同,使用`is`不同,因为在这种情况下我们有: + +```py +In [45]: a == c +Out[45]: True +``` + +`is`和`is not`的常见用法是检查变量是否为`None`,因为`None`只有一个实例: + +```py +In [46]: a = None + +In [47]: a is None +Out[47]: True +``` + +#### 可变和不可变对象 + +Python 中的许多对象,如列表、字典、NumPy 数组和大多数用户定义的类型(类),都是*可变*的。这意味着它们包含的对象或值可以被修改: + +```py +In [48]: a_list = ["foo", 2, [4, 5]] + +In [49]: a_list[2] = (3, 4) + +In [50]: a_list +Out[50]: ['foo', 2, (3, 4)] +``` + +其他,如字符串和元组,是不可变的,这意味着它们的内部数据不能被更改: + +```py +In [51]: a_tuple = (3, 5, (4, 5)) + +In [52]: a_tuple[1] = "four" +--------------------------------------------------------------------------- +TypeError Traceback (most recent call last) + in +----> 1 a_tuple[1] = "four" +TypeError: 'tuple' object does not support item assignment +``` + +请记住,仅因为您*可以*改变对象并不意味着您总是*应该*这样做。这些操作被称为*副作用*。例如,在编写函数时,任何副作用都应明确地在函数的文档或注释中向用户传达。如果可能的话,我建议尽量避免副作用并*偏爱不可变性*,即使可能涉及可变对象。 + +### 标量类型 + +Python 具有一小组内置类型,用于处理数字数据、字符串、布尔(`True`或`False`)值以及日期和时间。这些“单值”类型有时被称为*标量类型*,我们在本书中将它们称为*标量*。请参阅表 2.2 以获取主要标量类型的列表。日期和时间处理将单独讨论,因为这些由标准库中的`datetime`模块提供。 + +表 2.2:标准 Python 标量类型 + +| 类型 | 描述 | +| --- | --- | +| `None` | Python 的“null”值(只存在一个`None`对象的实例) | +| `str` | 字符串类型;保存 Unicode 字符串 | +| `bytes` | 原始二进制数据 | +| `float` | 双精度浮点数(请注意没有单独的`double`类型) | +| `bool` | 布尔值`True`或`False` | +| `int` | 任意精度整数 | + +#### 数字类型 + +数字的主要 Python 类型是`int`和`float`。`int`可以存储任意大的数字: + +```py +In [53]: ival = 17239871 + +In [54]: ival ** 6 +Out[54]: 26254519291092456596965462913230729701102721 +``` + +浮点数用 Python 的`float`类型表示。在底层,每个都是双精度值。它们也可以用科学计数法表示: + +```py +In [55]: fval = 7.243 + +In [56]: fval2 = 6.78e-5 +``` + +整数除法如果结果不是整数,将始终产生一个浮点数: + +```py +In [57]: 3 / 2 +Out[57]: 1.5 +``` + +要获得 C 风格的整数除法(如果结果不是整数,则丢弃小数部分),请使用地板除法运算符`//`: + +```py +In [58]: 3 // 2 +Out[58]: 1 +``` + +#### 字符串 + +许多人使用 Python 是因为其内置的字符串处理功能。您可以使用单引号`'`或双引号`"`(通常更喜欢双引号)编写*字符串字面值*: + +```py +a = 'one way of writing a string' +b = "another way" +``` + +Python 字符串类型是`str`。 + +对于带有换行符的多行字符串,可以使用三引号,即`'''`或`"""`: + +```py +c = """ +This is a longer string that +spans multiple lines +""" +``` + +这个字符串`c`实际上包含四行文本可能会让您感到惊讶;在`"""`之后和`lines`之后的换行符包含在字符串中。我们可以使用`c`上的`count`方法来计算换行符的数量: + +```py +In [60]: c.count("\n") +Out[60]: 3 +``` + +Python 字符串是不可变的;您不能修改一个字符串: + +```py +In [61]: a = "this is a string" + +In [62]: a[10] = "f" +--------------------------------------------------------------------------- +TypeError Traceback (most recent call last) + in +----> 1 a[10] = "f" +TypeError: 'str' object does not support item assignment +``` + +要解释此错误消息,请从下往上阅读。我们尝试用字母`"f"`替换位置 10 处的字符(“项”),但对于字符串对象来说,这是不允许的。如果我们需要修改一个字符串,我们必须使用一个创建新字符串的函数或方法,比如字符串`replace`方法: + +```py +In [63]: b = a.replace("string", "longer string") + +In [64]: b +Out[64]: 'this is a longer string' +``` + +此操作后,变量`a`保持不变: + +```py +In [65]: a +Out[65]: 'this is a string' +``` + +许多 Python 对象可以使用`str`函数转换为字符串: + +```py +In [66]: a = 5.6 + +In [67]: s = str(a) + +In [68]: print(s) +5.6 +``` + +字符串是 Unicode 字符序列,因此可以像其他序列(如列表和元组)一样对待: + +```py +In [69]: s = "python" + +In [70]: list(s) +Out[70]: ['p', 'y', 't', 'h', 'o', 'n'] + +In [71]: s[:3] +Out[71]: 'pyt' +``` + +`s[:3]`语法称为*切片*,对于许多种类的 Python 序列都有实现。稍后将更详细地解释这一点,因为它在本书中被广泛使用。 + +反斜杠字符`\`是一个*转义字符*,意味着它用于指定特殊字符,如换行符`\n`或 Unicode 字符。要编写带有反斜杠的字符串字面值,您需要对其进行转义: + +```py +In [72]: s = "12\\34" + +In [73]: print(s) +12\34 +``` + +如果您有一个带有许多反斜杠且没有特殊字符的字符串,您可能会觉得有点烦人。幸运的是,您可以在字符串的前导引号前加上`r`,这意味着应该按原样解释字符: + +```py +In [74]: s = r"this\has\no\special\characters" + +In [75]: s +Out[75]: 'this\\has\\no\\special\\characters' +``` + +`r`代表*原始*。 + +将两个字符串相加会将它们连接在一起并生成一个新字符串: + +```py +In [76]: a = "this is the first half " + +In [77]: b = "and this is the second half" + +In [78]: a + b +Out[78]: 'this is the first half and this is the second half' +``` + +字符串模板或格式化是另一个重要主题。随着 Python 3 的出现,进行此操作的方式数量已经扩展,这里我将简要描述其中一个主要接口的机制。字符串对象具有一个`format`方法,可用于将格式化参数替换为字符串中,生成一个新字符串: + +```py +In [79]: template = "{0:.2f} {1:s} are worth US${2:d}" +``` + +在这个字符串中: + ++ `{0:.2f}`表示将第一个参数格式化为带有两位小数的浮点数。 + ++ `{1:s}`表示将第二个参数格式化为字符串。 + ++ `{2:d}`表示将第三个参数格式化为精确整数。 + +要为这些格式参数替换参数,我们将一系列参数传递给`format`方法: + +```py +In [80]: template.format(88.46, "Argentine Pesos", 1) +Out[80]: '88.46 Argentine Pesos are worth US$1' +``` + +Python 3.6 引入了一个名为*f-strings*(即*格式化字符串字面值*)的新功能,可以使创建格式化字符串更加方便。要创建 f-string,只需在字符串字面值之前立即写入字符`f`。在字符串中,用大括号括起 Python 表达式,以将表达式的值替换为格式化字符串中的值: + +```py +In [81]: amount = 10 + +In [82]: rate = 88.46 + +In [83]: currency = "Pesos" + +In [84]: result = f"{amount} {currency} is worth US${amount / rate}" +``` + +格式说明符可以在每个表达式后添加,使用与上面字符串模板相同的语法: + +```py +In [85]: f"{amount} {currency} is worth US${amount / rate:.2f}" +Out[85]: '10 Pesos is worth US$0.11' +``` + +字符串格式化是一个深入的主题;有多种方法和大量选项和调整可用于控制结果字符串中的值的格式。要了解更多,请参阅[官方 Python 文档](https://docs.python.org/3/library/string.html)。 + +#### 字节和 Unicode + +在现代 Python(即 Python 3.0 及更高版本)中,Unicode 已成为一流的字符串类型,以实现更一致地处理 ASCII 和非 ASCII 文本。在旧版本的 Python 中,字符串都是字节,没有任何明确的 Unicode 编码。您可以假设您知道字符编码来转换为 Unicode。这里是一个带有非 ASCII 字符的示例 Unicode 字符串: + +```py +In [86]: val = "español" + +In [87]: val +Out[87]: 'español' +``` + +我们可以使用`encode`方法将此 Unicode 字符串转换为其 UTF-8 字节表示: + +```py +In [88]: val_utf8 = val.encode("utf-8") + +In [89]: val_utf8 +Out[89]: b'espa\xc3\xb1ol' + +In [90]: type(val_utf8) +Out[90]: bytes +``` + +假设您知道`bytes`对象的 Unicode 编码,您可以使用`decode`方法返回: + +```py +In [91]: val_utf8.decode("utf-8") +Out[91]: 'español' +``` + +现在最好使用 UTF-8 进行任何编码,但出于历史原因,您可能会遇到各种不同编码的数据: + +```py +In [92]: val.encode("latin1") +Out[92]: b'espa\xf1ol' + +In [93]: val.encode("utf-16") +Out[93]: b'\xff\xfee\x00s\x00p\x00a\x00\xf1\x00o\x00l\x00' + +In [94]: val.encode("utf-16le") +Out[94]: b'e\x00s\x00p\x00a\x00\xf1\x00o\x00l\x00' +``` + +在处理文件时,最常见的是遇到`bytes`对象,其中不希望将所有数据隐式解码为 Unicode 字符串。 + +#### 布尔值 + +Python 中的两个布尔值分别写为`True`和`False`。比较和其他条件表达式的结果要么为`True`,要么为`False`。布尔值可以使用`and`和`or`关键字组合: + +```py +In [95]: True and True +Out[95]: True + +In [96]: False or True +Out[96]: True +``` + +当转换为数字时,`False`变为`0`,`True`变为`1`: + +```py +In [97]: int(False) +Out[97]: 0 + +In [98]: int(True) +Out[98]: 1 +``` + +关键字`not`可以将布尔值从`True`翻转为`False`,反之亦然: + +```py +In [99]: a = True + +In [100]: b = False + +In [101]: not a +Out[101]: False + +In [102]: not b +Out[102]: True +``` + +#### 类型转换 + +`str`、`bool`、`int`和`float`类型也是可以用来将值转换为这些类型的函数: + +```py +In [103]: s = "3.14159" + +In [104]: fval = float(s) + +In [105]: type(fval) +Out[105]: float + +In [106]: int(fval) +Out[106]: 3 + +In [107]: bool(fval) +Out[107]: True + +In [108]: bool(0) +Out[108]: False +``` + +请注意,大多数非零值在转换为`bool`时会变为`True`。 + +#### None + +`None`是 Python 的空值类型: + +```py +In [109]: a = None + +In [110]: a is None +Out[110]: True + +In [111]: b = 5 + +In [112]: b is not None +Out[112]: True +``` + +`None`也是函数参数的常见默认值: + +```py +def add_and_maybe_multiply(a, b, c=None): + result = a + b + + if c is not None: + result = result * c + + return result +``` + +#### 日期和时间 + +内置的 Python `datetime`模块提供了`datetime`、`date`和`time`类型。`datetime`类型结合了`date`和`time`中存储的信息,是最常用的类型: + +```py +In [113]: from datetime import datetime, date, time + +In [114]: dt = datetime(2011, 10, 29, 20, 30, 21) + +In [115]: dt.day +Out[115]: 29 + +In [116]: dt.minute +Out[116]: 30 +``` + +给定一个`datetime`实例,您可以通过在具有相同名称的`datetime`上调用方法来提取等效的`date`和`time`对象: + +```py +In [117]: dt.date() +Out[117]: datetime.date(2011, 10, 29) + +In [118]: dt.time() +Out[118]: datetime.time(20, 30, 21) +``` + +`strftime`方法将`datetime`格式化为字符串: + +```py +In [119]: dt.strftime("%Y-%m-%d %H:%M") +Out[119]: '2011-10-29 20:30' +``` + +字符串可以使用`strptime`函数转换(解析)为`datetime`对象: + +```py +In [120]: datetime.strptime("20091031", "%Y%m%d") +Out[120]: datetime.datetime(2009, 10, 31, 0, 0) +``` + +查看表 11.2 以获取完整的格式规范列表。 + +当您聚合或以其他方式对时间序列数据进行分组时,偶尔会有必要替换一系列`datetime`的时间字段,例如,将`minute`和`second`字段替换为零: + +```py +In [121]: dt_hour = dt.replace(minute=0, second=0) + +In [122]: dt_hour +Out[122]: datetime.datetime(2011, 10, 29, 20, 0) +``` + +由于`datetime.datetime`是不可变类型,这些方法总是会产生新对象。因此,在前面的例子中,`dt`不会被`replace`修改: + +```py +In [123]: dt +Out[123]: datetime.datetime(2011, 10, 29, 20, 30, 21) +``` + +两个`datetime`对象的差产生一个`datetime.timedelta`类型: + +```py +In [124]: dt2 = datetime(2011, 11, 15, 22, 30) + +In [125]: delta = dt2 - dt + +In [126]: delta +Out[126]: datetime.timedelta(days=17, seconds=7179) + +In [127]: type(delta) +Out[127]: datetime.timedelta +``` + +输出`timedelta(17, 7179)`表示`timedelta`编码了 17 天和 7179 秒的偏移量。 + +将`timedelta`添加到`datetime`会产生一个新的偏移`datetime`: + +```py +In [128]: dt +Out[128]: datetime.datetime(2011, 10, 29, 20, 30, 21) + +In [129]: dt + delta +Out[129]: datetime.datetime(2011, 11, 15, 22, 30) +``` + +### 控制流 + +Python 有几个内置关键字用于条件逻辑、循环和其他标准*控制流*概念,这些概念在其他编程语言中也可以找到。 + +#### if、elif 和 else + +`if`语句是最为人熟知的控制流语句类型之一。它检查一个条件,如果为`True`,则评估后面的代码块: + +```py +x = -5 +if x < 0: + print("It's negative") +``` + +`if`语句后面可以选择跟随一个或多个`elif`代码块和一个全捕获的`else`代码块,如果所有条件都为`False`: + +```py +if x < 0: + print("It's negative") +elif x == 0: + print("Equal to zero") +elif 0 < x < 5: + print("Positive but smaller than 5") +else: + print("Positive and larger than or equal to 5") +``` + +如果任何条件为`True`,则不会继续执行任何`elif`或`else`代码块。使用`and`或`or`的复合条件,条件从左到右进行评估并会短路: + +```py +In [130]: a = 5; b = 7 + +In [131]: c = 8; d = 4 + +In [132]: if a < b or c > d: + .....: print("Made it") +Made it +``` + +在这个例子中,比较`c > d`永远不会被评估,因为第一个比较是`True`。 + +也可以链接比较: + +```py +In [133]: 4 > 3 > 2 > 1 +Out[133]: True +``` + +#### for 循环 + +`for`循环用于遍历集合(如列表或元组)或迭代器。`for`循环的标准语法是: + +```py +for value in collection: + # do something with value +``` + +您可以使用`continue`关键字将`for`循环推进到下一个迭代,跳过代码块的其余部分。考虑这段代码,它对列表中的整数求和并跳过`None`值: + +```py +sequence = [1, 2, None, 4, None, 5] +total = 0 +for value in sequence: + if value is None: + continue + total += value +``` + +可以使用`break`关键字完全退出`for`循环。这段代码将列表元素求和,直到达到 5 为止: + +```py +sequence = [1, 2, 0, 4, 6, 5, 2, 1] +total_until_5 = 0 +for value in sequence: + if value == 5: + break + total_until_5 += value +``` + +`break`关键字仅终止最内层的`for`循环;任何外部的`for`循环将继续运行: + +```py +In [134]: for i in range(4): + .....: for j in range(4): + .....: if j > i: + .....: break + .....: print((i, j)) + .....: +(0, 0) +(1, 0) +(1, 1) +(2, 0) +(2, 1) +(2, 2) +(3, 0) +(3, 1) +(3, 2) +(3, 3) +``` + +正如我们将在更详细地看到的,如果集合或迭代器中的元素是序列(例如元组或列表),它们可以方便地在`for`循环语句中*解包*为变量: + +```py +for a, b, c in iterator: + # do something +``` + +#### while 循环 + +`while`循环指定一个条件和一个要执行的代码块,直到条件评估为`False`或循环被显式地使用`break`结束为止: + +```py +x = 256 +total = 0 +while x > 0: + if total > 500: + break + total += x + x = x // 2 +``` + +#### pass + +`pass`是 Python 中的“空操作”(或“什么也不做”)语句。它可以在不需要执行任何操作的代码块中使用(或作为尚未实现的代码的占位符);它仅仅是因为 Python 使用空格来分隔代码块: + +```py +if x < 0: + print("negative!") +elif x == 0: + # TODO: put something smart here + pass +else: + print("positive!") +``` + +#### 范围 + +`range`函数生成一系列均匀间隔的整数: + +```py +In [135]: range(10) +Out[135]: range(0, 10) + +In [136]: list(range(10)) +Out[136]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] +``` + +可以给定起始点、终点和步长(可以是负数): + +```py +In [137]: list(range(0, 20, 2)) +Out[137]: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18] + +In [138]: list(range(5, 0, -1)) +Out[138]: [5, 4, 3, 2, 1] +``` + +正如您所看到的,`range` 生成的整数是直到但不包括终点的。`range` 的一个常见用途是通过索引迭代序列: + +```py +In [139]: seq = [1, 2, 3, 4] + +In [140]: for i in range(len(seq)): + .....: print(f"element {i}: {seq[i]}") +element 0: 1 +element 1: 2 +element 2: 3 +element 3: 4 +``` + +虽然您可以使用`list`等函数将`range`生成的所有整数存储在其他数据结构中,但通常默认的迭代器形式会是您想要的。这段代码将从 0 到 99,999 之间是 3 或 5 的倍数的所有数字相加: + +```py +In [141]: total = 0 + +In [142]: for i in range(100_000): + .....: # % is the modulo operator + .....: if i % 3 == 0 or i % 5 == 0: + .....: total += i + +In [143]: print(total) +2333316668 +``` + +虽然生成的范围可以任意大,但在任何给定时间内的内存使用可能非常小。 + +## 2.4 结论 + +本章简要介绍了一些基本的 Python 语言概念以及 IPython 和 Jupyter 编程环境。在下一章中,我将讨论许多内置数据类型、函数和输入输出工具,这些内容将在本书的其余部分中持续使用。 diff --git a/translations/cn/pyda3e_06.md b/translations/cn/pyda3e_06.md new file mode 100644 index 000000000..b7f8af5ac --- /dev/null +++ b/translations/cn/pyda3e_06.md @@ -0,0 +1,1791 @@ +# 三、内置数据结构、函数和文件 + +> 原文:[`wesmckinney.com/book/python-builtin`](https://wesmckinney.com/book/python-builtin) +> +> 译者:[飞龙](https://github.com/wizardforcel) +> +> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) + + +> 此开放访问网络版本的《Python 数据分析第三版》现已作为[印刷版和数字版](https://amzn.to/3DyLaJc)的伴侣提供。如果您发现任何勘误,请[在此处报告](https://oreilly.com/catalog/0636920519829/errata)。请注意,由 Quarto 生成的本站点的某些方面与 O'Reilly 的印刷版和电子书版本的格式不同。 +> +> 如果您发现本书的在线版本有用,请考虑[订购纸质版](https://amzn.to/3DyLaJc)或[无 DRM 的电子书](https://www.ebooks.com/en-us/book/210644288/python-for-data-analysis/wes-mckinney/?affId=WES398681F)以支持作者。本网站的内容不得复制或再生产。代码示例采用 MIT 许可,可在 GitHub 或 Gitee 上找到。 + +本章讨论了内置到 Python 语言中的功能,这些功能将在整本书中被广泛使用。虽然像 pandas 和 NumPy 这样的附加库为更大的数据集添加了高级计算功能,但它们旨在与 Python 的内置数据操作工具一起使用。 + +我们将从 Python 的主要数据结构开始:元组、列表、字典和集合。然后,我们将讨论如何创建自己可重用的 Python 函数。最后,我们将看看 Python 文件对象的机制以及如何与本地硬盘交互。 + +## 3.1 数据结构和序列 + +Python 的数据结构简单而强大。掌握它们的使用是成为熟练的 Python 程序员的关键部分。我们从元组、列表和字典开始,它们是一些最常用的*序列*类型。 + +### 元组 + +*元组*是 Python 对象的固定长度、不可变序列,一旦分配,就无法更改。创建元组的最简单方法是使用括号括起的逗号分隔的值序列: + +```py +In [2]: tup = (4, 5, 6) + +In [3]: tup +Out[3]: (4, 5, 6) +``` + +在许多情况下,括号可以省略,所以这里我们也可以这样写: + +```py +In [4]: tup = 4, 5, 6 + +In [5]: tup +Out[5]: (4, 5, 6) +``` + +您可以通过调用`tuple`将任何序列或迭代器转换为元组: + +```py +In [6]: tuple([4, 0, 2]) +Out[6]: (4, 0, 2) + +In [7]: tup = tuple('string') + +In [8]: tup +Out[8]: ('s', 't', 'r', 'i', 'n', 'g') +``` + +元素可以使用方括号`[]`访问,就像大多数其他序列类型一样。与 C、C++、Java 和许多其他语言一样,在 Python 中,序列是从 0 开始索引的: + +```py +In [9]: tup[0] +Out[9]: 's' +``` + +当您在更复杂的表达式中定义元组时,通常需要将值括在括号中,就像在创建元组的示例中一样: + +```py +In [10]: nested_tup = (4, 5, 6), (7, 8) + +In [11]: nested_tup +Out[11]: ((4, 5, 6), (7, 8)) + +In [12]: nested_tup[0] +Out[12]: (4, 5, 6) + +In [13]: nested_tup[1] +Out[13]: (7, 8) +``` + +虽然存储在元组中的对象本身可能是可变的,但一旦创建了元组,就无法修改存储在每个槽中的对象: + +```py +In [14]: tup = tuple(['foo', [1, 2], True]) + +In [15]: tup[2] = False +--------------------------------------------------------------------------- +TypeError Traceback (most recent call last) + in +----> 1 tup[2] = False +TypeError: 'tuple' object does not support item assignment +``` + +如果元组中的对象是可变的,比如列表,您可以就地修改它: + +```py +In [16]: tup[1].append(3) + +In [17]: tup +Out[17]: ('foo', [1, 2, 3], True) +``` + +您可以使用`+`运算符连接元组以生成更长的元组: + +```py +In [18]: (4, None, 'foo') + (6, 0) + ('bar',) +Out[18]: (4, None, 'foo', 6, 0, 'bar') +``` + +将元组乘以一个整数,与列表一样,会产生该元组的多个副本的效果: + +```py +In [19]: ('foo', 'bar') * 4 +Out[19]: ('foo', 'bar', 'foo', 'bar', 'foo', 'bar', 'foo', 'bar') +``` + +请注意,对象本身并没有被复制,只有对它们的引用。 + +#### 解包元组 + +如果您尝试对类似元组的变量表达式进行*赋值*,Python 将尝试在等号右侧*解包*值: + +```py +In [20]: tup = (4, 5, 6) + +In [21]: a, b, c = tup + +In [22]: b +Out[22]: 5 +``` + +即使包含嵌套元组的序列也可以解包: + +```py +In [23]: tup = 4, 5, (6, 7) + +In [24]: a, b, (c, d) = tup + +In [25]: d +Out[25]: 7 +``` + +使用这个功能,您可以轻松交换变量名,这在许多语言中可能看起来像: + +```py +tmp = a +a = b +b = tmp +``` + +但是,在 Python 中,交换可以这样做: + +```py +In [26]: a, b = 1, 2 + +In [27]: a +Out[27]: 1 + +In [28]: b +Out[28]: 2 + +In [29]: b, a = a, b + +In [30]: a +Out[30]: 2 + +In [31]: b +Out[31]: 1 +``` + +变量解包的常见用途是迭代元组或列表的序列: + +```py +In [32]: seq = [(1, 2, 3), (4, 5, 6), (7, 8, 9)] + +In [33]: for a, b, c in seq: + ....: print(f'a={a}, b={b}, c={c}') +a=1, b=2, c=3 +a=4, b=5, c=6 +a=7, b=8, c=9 +``` + +另一个常见用途是从函数返回多个值。我稍后会更详细地介绍这个问题。 + +有一些情况下,您可能希望从元组的开头“摘取”一些元素。有一种特殊的语法可以做到这一点,`*rest`,这也用于函数签名中捕获任意长的位置参数: + +```py +In [34]: values = 1, 2, 3, 4, 5 + +In [35]: a, b, *rest = values + +In [36]: a +Out[36]: 1 + +In [37]: b +Out[37]: 2 + +In [38]: rest +Out[38]: [3, 4, 5] +``` + +这个`rest`位有时是您想要丢弃的内容;`rest`名称没有特殊之处。作为一种惯例,许多 Python 程序员会使用下划线(`_`)表示不需要的变量: + +```py +In [39]: a, b, *_ = values +``` + +#### 元组方法 + +由于元组的大小和内容不能被修改,因此实例方法非常少。一个特别有用的方法(也适用于列表)是`count`,它计算值的出现次数: + +```py +In [40]: a = (1, 2, 2, 2, 3, 4, 2) + +In [41]: a.count(2) +Out[41]: 4 +``` + +### 列表 + +与元组相反,列表是可变长度的,其内容可以就地修改。列表是可变的。您可以使用方括号`[]`定义它们,也可以使用`list`类型函数: + +```py +In [42]: a_list = [2, 3, 7, None] + +In [43]: tup = ("foo", "bar", "baz") + +In [44]: b_list = list(tup) + +In [45]: b_list +Out[45]: ['foo', 'bar', 'baz'] + +In [46]: b_list[1] = "peekaboo" + +In [47]: b_list +Out[47]: ['foo', 'peekaboo', 'baz'] +``` + +列表和元组在语义上是相似的(尽管元组不能被修改),并且可以在许多函数中互换使用。 + +`list`内置函数在数据处理中经常用作实例化迭代器或生成器表达式的方法: + +```py +In [48]: gen = range(10) + +In [49]: gen +Out[49]: range(0, 10) + +In [50]: list(gen) +Out[50]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] +``` + +#### 添加和删除元素 + +元素可以使用`append`方法附加到列表的末尾: + +```py +In [51]: b_list.append("dwarf") + +In [52]: b_list +Out[52]: ['foo', 'peekaboo', 'baz', 'dwarf'] +``` + +使用`insert`可以在列表中的特定位置插入元素: + +```py +In [53]: b_list.insert(1, "red") + +In [54]: b_list +Out[54]: ['foo', 'red', 'peekaboo', 'baz', 'dwarf'] +``` + +插入索引必须在列表的长度之间,包括 0 和长度。 + +警告: + +与`append`相比,`insert`的计算成本较高,因为必须在内部移动后续元素的引用以为新元素腾出空间。如果需要在序列的开头和结尾插入元素,您可能希望探索`collections.deque`,这是一个双端队列,专为此目的进行了优化,并且包含在 Python 标准库中。 + +`insert`的反向操作是`pop`,它会删除并返回特定索引处的元素: + +```py +In [55]: b_list.pop(2) +Out[55]: 'peekaboo' + +In [56]: b_list +Out[56]: ['foo', 'red', 'baz', 'dwarf'] +``` + +可以使用`remove`按值删除元素,它会定位第一个这样的值并将其从列表中删除: + +```py +In [57]: b_list.append("foo") + +In [58]: b_list +Out[58]: ['foo', 'red', 'baz', 'dwarf', 'foo'] + +In [59]: b_list.remove("foo") + +In [60]: b_list +Out[60]: ['red', 'baz', 'dwarf', 'foo'] +``` + +如果不关心性能,通过使用`append`和`remove`,可以使用 Python 列表作为类似集合的数据结构(尽管 Python 有实际的集合对象,稍后讨论)。 + +使用`in`关键字检查列表是否包含一个值: + +```py +In [61]: "dwarf" in b_list +Out[61]: True +``` + +关键字`not`可以用来否定`in`: + +```py +In [62]: "dwarf" not in b_list +Out[62]: False +``` + +检查列表是否包含一个值比使用字典和集合慢得多(即将介绍),因为 Python 会在线性扫描列表的值,而可以在常量时间内检查其他值(基于哈希表)。 + +#### 连接和组合列表 + +与元组类似,使用`+`将两个列表相加会将它们连接起来: + +```py +In [63]: [4, None, "foo"] + [7, 8, (2, 3)] +Out[63]: [4, None, 'foo', 7, 8, (2, 3)] +``` + +如果已经定义了一个列表,可以使用`extend`方法将多个元素附加到其中: + +```py +In [64]: x = [4, None, "foo"] + +In [65]: x.extend([7, 8, (2, 3)]) + +In [66]: x +Out[66]: [4, None, 'foo', 7, 8, (2, 3)] +``` + +请注意,通过加法进行列表连接是一种相对昂贵的操作,因为必须创建一个新列表并复制对象。通常最好使用`extend`将元素附加到现有列表中,特别是如果您正在构建一个大列表。因此: + +```py +everything = [] +for chunk in list_of_lists: + everything.extend(chunk) +``` + +比连接替代方案更快: + +```py +everything = [] +for chunk in list_of_lists: + everything = everything + chunk +``` + +#### 排序 + +您可以通过调用其`sort`函数就地对列表进行排序(而不创建新对象): + +```py +In [67]: a = [7, 2, 5, 1, 3] + +In [68]: a.sort() + +In [69]: a +Out[69]: [1, 2, 3, 5, 7] +``` + +`sort`有一些选项,偶尔会派上用场。其中之一是能够传递一个次要*排序键*——即生成用于对对象进行排序的值的函数。例如,我们可以按字符串的长度对字符串集合进行排序: + +```py +In [70]: b = ["saw", "small", "He", "foxes", "six"] + +In [71]: b.sort(key=len) + +In [72]: b +Out[72]: ['He', 'saw', 'six', 'small', 'foxes'] +``` + +很快,我们将看一下`sorted`函数,它可以生成一份排序后的一般序列的副本。 + +#### 切片 + +您可以使用切片表示法选择大多数序列类型的部分,其基本形式是将`start:stop`传递给索引运算符`[]`: + +```py +In [73]: seq = [7, 2, 3, 7, 5, 6, 0, 1] + +In [74]: seq[1:5] +Out[74]: [2, 3, 7, 5] +``` + +切片也可以用序列赋值: + +```py +In [75]: seq[3:5] = [6, 3] + +In [76]: seq +Out[76]: [7, 2, 3, 6, 3, 6, 0, 1] +``` + +虽然`start`索引处的元素被包括在内,但`stop`索引*不包括*在内,因此结果中的元素数量为`stop - start`。 + +`start`或`stop`可以省略,此时它们分别默认为序列的开头和序列的结尾: + +```py +In [77]: seq[:5] +Out[77]: [7, 2, 3, 6, 3] + +In [78]: seq[3:] +Out[78]: [6, 3, 6, 0, 1] +``` + +负索引相对于末尾切片序列: + +```py +In [79]: seq[-4:] +Out[79]: [3, 6, 0, 1] + +In [80]: seq[-6:-2] +Out[80]: [3, 6, 3, 6] +``` + +切片语义需要一点时间来适应,特别是如果你是从 R 或 MATLAB 过来的。参见图 3.1 以了解使用正整数和负整数进行切片的有用示例。在图中,索引显示在“箱边缘”,以帮助显示使用正整数或负整数索引开始和停止的切片选择。 + +![](img/fe1beb3f93e0617bb1f4800840b8dc37.png) + +图 3.1:Python 切片约定的示例 + +第二个冒号后也可以使用`step`,比如,每隔一个元素取一个: + +```py +In [81]: seq[::2] +Out[81]: [7, 3, 3, 0] +``` + +这种方法的一个巧妙用法是传递`-1`,这样可以有效地反转列表或元组: + +```py +In [82]: seq[::-1] +Out[82]: [1, 0, 6, 3, 6, 3, 2, 7] +``` + +### 字典 + +字典或`dict`可能是 Python 中最重要的内置数据结构。在其他编程语言中,字典有时被称为*哈希映射*或*关联数组*。字典存储一组*键-值*对,其中*键*和*值*是 Python 对象。每个键都与一个值关联,以便可以方便地检索、插入、修改或删除给定特定键的值。创建字典的一种方法是使用大括号`{}`和冒号来分隔键和值: + +```py +In [83]: empty_dict = {} + +In [84]: d1 = {"a": "some value", "b": [1, 2, 3, 4]} + +In [85]: d1 +Out[85]: {'a': 'some value', 'b': [1, 2, 3, 4]} +``` + +可以使用与访问列表或元组元素相同的语法来访问、插入或设置元素: + +```py +In [86]: d1[7] = "an integer" + +In [87]: d1 +Out[87]: {'a': 'some value', 'b': [1, 2, 3, 4], 7: 'an integer'} + +In [88]: d1["b"] +Out[88]: [1, 2, 3, 4] +``` + +你可以使用与检查列表或元组是否包含值相同的语法来检查字典是否包含键: + +```py +In [89]: "b" in d1 +Out[89]: True +``` + +可以使用`del`关键字或`pop`方法(同时返回值并删除键)来删除值: + +```py +In [90]: d1[5] = "some value" + +In [91]: d1 +Out[91]: +{'a': 'some value', + 'b': [1, 2, 3, 4], + 7: 'an integer', + 5: 'some value'} + +In [92]: d1["dummy"] = "another value" + +In [93]: d1 +Out[93]: +{'a': 'some value', + 'b': [1, 2, 3, 4], + 7: 'an integer', + 5: 'some value', + 'dummy': 'another value'} + +In [94]: del d1[5] + +In [95]: d1 +Out[95]: +{'a': 'some value', + 'b': [1, 2, 3, 4], + 7: 'an integer', + 'dummy': 'another value'} + +In [96]: ret = d1.pop("dummy") + +In [97]: ret +Out[97]: 'another value' + +In [98]: d1 +Out[98]: {'a': 'some value', 'b': [1, 2, 3, 4], 7: 'an integer'} +``` + +`keys`和`values`方法分别为你提供字典的键和值的迭代器。键的顺序取决于它们插入的顺序,这些函数以相同的顺序输出键和值: + +```py +In [99]: list(d1.keys()) +Out[99]: ['a', 'b', 7] + +In [100]: list(d1.values()) +Out[100]: ['some value', [1, 2, 3, 4], 'an integer'] +``` + +如果需要同时迭代键和值,可以使用`items`方法以 2 元组的形式迭代键和值: + +```py +In [101]: list(d1.items()) +Out[101]: [('a', 'some value'), ('b', [1, 2, 3, 4]), (7, 'an integer')] +``` + +可以使用`update`方法将一个字典合并到另一个字典中: + +```py +In [102]: d1.update({"b": "foo", "c": 12}) + +In [103]: d1 +Out[103]: {'a': 'some value', 'b': 'foo', 7: 'an integer', 'c': 12} +``` + +`update`方法会直接更改字典,因此传递给`update`的数据中的任何现有键都将丢弃其旧值。 + +#### 从序列创建字典 + +通常会偶尔出现两个你想要逐个元素配对的序列。作为第一步,你可能会编写这样的代码: + +```py +mapping = {} +for key, value in zip(key_list, value_list): + mapping[key] = value +``` + +由于字典本质上是 2 元组的集合,`dict`函数接受一个 2 元组的列表: + +```py +In [104]: tuples = zip(range(5), reversed(range(5))) + +In [105]: tuples +Out[105]: + +In [106]: mapping = dict(tuples) + +In [107]: mapping +Out[107]: {0: 4, 1: 3, 2: 2, 3: 1, 4: 0} +``` + +稍后我们将讨论*字典推导*,这是构建字典的另一种方法。 + +#### 默认值 + +通常会有类似以下逻辑: + +```py +if key in some_dict: + value = some_dict[key] +else: + value = default_value +``` + +因此,字典方法`get`和`pop`可以接受要返回的默认值,因此上述`if-else`块可以简单地写为: + +```py +value = some_dict.get(key, default_value) +``` + +`get`默认情况下会返回`None`,如果键不存在,而`pop`会引发异常。在*设置*值时,可能字典中的值是另一种集合,比如列表。例如,你可以想象将单词列表按照它们的首字母分类为列表的字典: + +```py +In [108]: words = ["apple", "bat", "bar", "atom", "book"] + +In [109]: by_letter = {} + +In [110]: for word in words: + .....: letter = word[0] + .....: if letter not in by_letter: + .....: by_letter[letter] = [word] + .....: else: + .....: by_letter[letter].append(word) + .....: + +In [111]: by_letter +Out[111]: {'a': ['apple', 'atom'], 'b': ['bat', 'bar', 'book']} +``` + +`setdefault`字典方法可用于简化此工作流程。前面的`for`循环可以重写为: + +```py +In [112]: by_letter = {} + +In [113]: for word in words: + .....: letter = word[0] + .....: by_letter.setdefault(letter, []).append(word) + .....: + +In [114]: by_letter +Out[114]: {'a': ['apple', 'atom'], 'b': ['bat', 'bar', 'book']} +``` + +内置的`collections`模块有一个有用的类`defaultdict`,使这更加容易。要创建一个,你需要传递一个类型或函数,用于为字典中的每个槽生成默认值: + +```py +In [115]: from collections import defaultdict + +In [116]: by_letter = defaultdict(list) + +In [117]: for word in words: + .....: by_letter[word[0]].append(word) +``` + +#### 有效的字典键类型 + +虽然字典的值可以是任何 Python 对象,但键通常必须是不可变对象,如标量类型(int、float、string)或元组(元组中的所有对象也必须是不可变的)。这里的技术术语是*可哈希性*。你可以使用`hash`函数检查对象是否可哈希(可以用作字典中的键): + +```py +In [118]: hash("string") +Out[118]: 4022908869268713487 + +In [119]: hash((1, 2, (2, 3))) +Out[119]: -9209053662355515447 + +In [120]: hash((1, 2, [2, 3])) # fails because lists are mutable +--------------------------------------------------------------------------- +TypeError Traceback (most recent call last) + in +----> 1 hash((1, 2, [2, 3])) # fails because lists are mutable +TypeError: unhashable type: 'list' +``` + +通常情况下,使用`hash`函数时看到的哈希值将取决于你使用的 Python 版本。 + +要将列表用作键,一种选择是将其转换为元组,只要其元素也可以被散列: + +```py +In [121]: d = {} + +In [122]: d[tuple([1, 2, 3])] = 5 + +In [123]: d +Out[123]: {(1, 2, 3): 5} +``` + +### 集合 + +*集合*是一个无序的唯一元素集合。可以通过`set`函数或使用花括号的*集合字面值*来创建集合: + +```py +In [124]: set([2, 2, 2, 1, 3, 3]) +Out[124]: {1, 2, 3} + +In [125]: {2, 2, 2, 1, 3, 3} +Out[125]: {1, 2, 3} +``` + +集合支持数学*集合操作*,如并集、交集、差集和对称差集。考虑这两个示例集合: + +```py +In [126]: a = {1, 2, 3, 4, 5} + +In [127]: b = {3, 4, 5, 6, 7, 8} +``` + +这两个集合的并集是两个集合中出现的不同元素的集合。可以使用`union`方法或`|`二进制运算符来计算: + +```py +In [128]: a.union(b) +Out[128]: {1, 2, 3, 4, 5, 6, 7, 8} + +In [129]: a | b +Out[129]: {1, 2, 3, 4, 5, 6, 7, 8} +``` + +交集包含两个集合中都出现的元素。可以使用`&`运算符或`intersection`方法: + +```py +In [130]: a.intersection(b) +Out[130]: {3, 4, 5} + +In [131]: a & b +Out[131]: {3, 4, 5} +``` + +请参见表 3.1 以获取常用集合方法的列表。 + +表 3.1:Python 集合操作 + +| 函数 | 替代语法 | 描述 | +| --- | --- | --- | +| `a.add(x)` | N/A | 将元素`x`添加到集合`a`中 | +| `a.clear()` | N/A | 将集合`a`重置为空状态,丢弃所有元素 | +| `a.remove(x)` | N/A | 从集合`a`中删除元素`x` | +| `a.pop()` | N/A | 从集合`a`中删除一个任意元素,如果集合为空则引发`KeyError` | +| `a.union(b)` | `a | b` | `a`和`b`中所有唯一的元素 | +| `a.update(b)` | `a |= b` | 将`a`的内容设置为`a`和`b`中元素的并集 | +| `a.intersection(b)` | `a & b` | `a`和`b`中*都*存在的所有元素 | +| `a.intersection_update(b)` | `a &= b` | 将`a`的内容设置为`a`和`b`中元素的交集 | +| `a.difference(b)` | `a - b` | `a`中不在`b`中的元素 | +| `a.difference_update(b)` | `a -= b` | 将`a`设置为`a`中不在`b`中的元素 | +| `a.symmetric_difference(b)` | `a ^ b` | `a`或`b`中的所有元素,但*不是*两者都有的 | +| `a.symmetric_difference_update(b)` | `a ^= b` | 将`a`设置为`a`或`b`中的元素,但*不是*两者都有的 | +| `a.issubset(b)` | `<=` | 如果`a`的元素都包含在`b`中,则为`True` | +| `a.issuperset(b)` | `>=` | 如果`b`的元素都包含在`a`中,则为`True` | +| `a.isdisjoint(b)` | N/A | 如果`a`和`b`没有共同元素,则为`True` | + +注意 + +如果将不是集合的输入传递给`union`和`intersection`等方法,Python 将在执行操作之前将输入转换为集合。在使用二进制运算符时,两个对象必须已经是集合。 + +所有逻辑集合操作都有原地对应物,这使您可以用结果替换操作左侧集合的内容。对于非常大的集合,这可能更有效率:* + +```py +In [132]: c = a.copy() + +In [133]: c |= b + +In [134]: c +Out[134]: {1, 2, 3, 4, 5, 6, 7, 8} + +In [135]: d = a.copy() + +In [136]: d &= b + +In [137]: d +Out[137]: {3, 4, 5} +``` + +与字典键类似,集合元素通常必须是不可变的,并且它们必须是*可散列*的(这意味着对值调用`hash`不会引发异常)。为了将类似列表的元素(或其他可变序列)存储在集合中,可以将它们转换为元组: + +```py +In [138]: my_data = [1, 2, 3, 4] + +In [139]: my_set = {tuple(my_data)} + +In [140]: my_set +Out[140]: {(1, 2, 3, 4)} +``` + +您还可以检查一个集合是否是另一个集合的子集(包含在内)或超集(包含所有元素): + +```py +In [141]: a_set = {1, 2, 3, 4, 5} + +In [142]: {1, 2, 3}.issubset(a_set) +Out[142]: True + +In [143]: a_set.issuperset({1, 2, 3}) +Out[143]: True +``` + +只有当集合的内容相等时,集合才相等: + +```py +In [144]: {1, 2, 3} == {3, 2, 1} +Out[144]: True +``` + +### 内置序列函数 + +Python 有一些有用的序列函数,您应该熟悉并在任何机会使用。 + +#### enumerate + +在迭代序列时,通常希望跟踪当前项目的索引。自己动手的方法如下: + +```py +index = 0 +for value in collection: + # do something with value + index += 1 +``` + +由于这种情况很常见,Python 有一个内置函数`enumerate`,它返回一个`(i, value)`元组序列: + +```py +for index, value in enumerate(collection): + # do something with value +``` + +#### sorted + +`sorted`函数从任何序列的元素返回一个新的排序列表: + +```py +In [145]: sorted([7, 1, 2, 6, 0, 3, 2]) +Out[145]: [0, 1, 2, 2, 3, 6, 7] + +In [146]: sorted("horse race") +Out[146]: [' ', 'a', 'c', 'e', 'e', 'h', 'o', 'r', 'r', 's'] +``` + +`sorted`函数接受与列表的`sort`方法相同的参数。 + +#### zip + +`zip`将多个列表、元组或其他序列的元素“配对”起来,以创建一个元组列表: + +```py +In [147]: seq1 = ["foo", "bar", "baz"] + +In [148]: seq2 = ["one", "two", "three"] + +In [149]: zipped = zip(seq1, seq2) + +In [150]: list(zipped) +Out[150]: [('foo', 'one'), ('bar', 'two'), ('baz', 'three')] +``` + +`zip`可以接受任意数量的序列,并且它生成的元素数量由*最短*的序列决定: + +```py +In [151]: seq3 = [False, True] + +In [152]: list(zip(seq1, seq2, seq3)) +Out[152]: [('foo', 'one', False), ('bar', 'two', True)] +``` + +`zip`的一个常见用法是同时迭代多个序列,可能还与`enumerate`结合使用: + +```py +In [153]: for index, (a, b) in enumerate(zip(seq1, seq2)): + .....: print(f"{index}: {a}, {b}") + .....: +0: foo, one +1: bar, two +2: baz, three +``` + +#### 反转 + +`reversed`以相反的顺序迭代序列的元素: + +```py +In [154]: list(reversed(range(10))) +Out[154]: [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] +``` + +请记住,`reversed`是一个生成器(稍后将更详细讨论),因此它不会创建反转的序列,直到实现(例如,使用`list`或`for`循环)。 + +### 列表、集合和字典推导 + +*列表推导*是 Python 语言中一个方便且广泛使用的特性。它们允许您通过过滤集合的元素,将通过过滤的元素转换为一个简洁的表达式来简洁地形成一个新列表。它们的基本形式如下: + +```py +[expr for value in collection if condition] +``` + +这等同于以下的`for`循环: + +```py +result = [] +for value in collection: + if condition: + result.append(expr) +``` + +过滤条件可以被省略,只留下表达式。例如,给定一个字符串列表,我们可以过滤出长度为`2`或更少的字符串,并将它们转换为大写: + +```py +In [155]: strings = ["a", "as", "bat", "car", "dove", "python"] + +In [156]: [x.upper() for x in strings if len(x) > 2] +Out[156]: ['BAT', 'CAR', 'DOVE', 'PYTHON'] +``` + +集合和字典推导是一个自然的扩展,以一种类似的方式产生集合和字典,而不是列表。 + +字典推导看起来像这样: + +```py +dict_comp = {key-expr: value-expr for value in collection + if condition} +``` + +集合推导看起来与等效的列表推导相同,只是用花括号代替方括号: + +```py +set_comp = {expr for value in collection if condition} +``` + +与列表推导类似,集合和字典推导大多是便利性的,但它们同样可以使代码更易于编写和阅读。考虑之前的字符串列表。假设我们想要一个集合,其中只包含集合中包含的字符串的长度;我们可以很容易地使用集合推导来计算: + +```py +In [157]: unique_lengths = {len(x) for x in strings} + +In [158]: unique_lengths +Out[158]: {1, 2, 3, 4, 6} +``` + +我们也可以更加功能化地使用`map`函数,稍后介绍: + +```py +In [159]: set(map(len, strings)) +Out[159]: {1, 2, 3, 4, 6} +``` + +作为一个简单的字典推导示例,我们可以创建一个查找这些字符串在列表中位置的查找映射: + +```py +In [160]: loc_mapping = {value: index for index, value in enumerate(strings)} + +In [161]: loc_mapping +Out[161]: {'a': 0, 'as': 1, 'bat': 2, 'car': 3, 'dove': 4, 'python': 5} +``` + +#### 嵌套列表推导 + +假设我们有一个包含一些英文和西班牙名字的列表列表: + +```py +In [162]: all_data = [["John", "Emily", "Michael", "Mary", "Steven"], + .....: ["Maria", "Juan", "Javier", "Natalia", "Pilar"]] +``` + +假设我们想要获得一个包含所有包含两个或更多个`a`的名称的单个列表。我们可以通过一个简单的`for`循环来实现: + +```py +In [163]: names_of_interest = [] + +In [164]: for names in all_data: + .....: enough_as = [name for name in names if name.count("a") >= 2] + .....: names_of_interest.extend(enough_as) + .....: + +In [165]: names_of_interest +Out[165]: ['Maria', 'Natalia'] +``` + +实际上,您可以将整个操作封装在一个单独的*嵌套列表推导*中,看起来像: + +```py +In [166]: result = [name for names in all_data for name in names + .....: if name.count("a") >= 2] + +In [167]: result +Out[167]: ['Maria', 'Natalia'] +``` + +起初,嵌套列表推导可能有点难以理解。列表推导的`for`部分按照嵌套的顺序排列,任何过滤条件都放在最后。这里是另一个示例,我们将整数元组的列表“展平”为一个简单的整数列表: + +```py +In [168]: some_tuples = [(1, 2, 3), (4, 5, 6), (7, 8, 9)] + +In [169]: flattened = [x for tup in some_tuples for x in tup] + +In [170]: flattened +Out[170]: [1, 2, 3, 4, 5, 6, 7, 8, 9] +``` + +请记住,如果您写一个嵌套的`for`循环而不是列表推导,`for`表达式的顺序将是相同的: + +```py +flattened = [] + +for tup in some_tuples: + for x in tup: + flattened.append(x) +``` + +您可以有任意多层的嵌套,尽管如果您有超过两三层的嵌套,您可能应该开始质疑这是否在代码可读性方面是有意义的。重要的是要区分刚刚显示的语法与列表推导内部的列表推导,后者也是完全有效的: + +```py +In [172]: [[x for x in tup] for tup in some_tuples] +Out[172]: [[1, 2, 3], [4, 5, 6], [7, 8, 9]] +``` + +这将产生一个列表的列表,而不是所有内部元素的扁平化列表。 + +## 3.2 函数 + +*函数*是 Python 中代码组织和重用的主要和最重要的方法。作为一个经验法则,如果您预计需要重复相同或非常相似的代码超过一次,编写可重用的函数可能是值得的。函数还可以通过给一组 Python 语句命名来使您的代码更易读。 + +函数使用`def`关键字声明。函数包含一个代码块,可选使用`return`关键字: + +```py +In [173]: def my_function(x, y): + .....: return x + y +``` + +当到达带有`return`的行时,`return`后的值或表达式将发送到调用函数的上下文,例如: + +```py +In [174]: my_function(1, 2) +Out[174]: 3 + +In [175]: result = my_function(1, 2) + +In [176]: result +Out[176]: 3 +``` + +有多个`return`语句是没有问题的。如果 Python 在函数结尾处没有遇到`return`语句,将自动返回`None`。例如: + +```py +In [177]: def function_without_return(x): + .....: print(x) + +In [178]: result = function_without_return("hello!") +hello! + +In [179]: print(result) +None +``` + +每个函数可以有 *位置* 参数和 *关键字* 参数。关键字参数最常用于指定默认值或可选参数。在这里,我们将定义一个带有默认值 `1.5` 的可选 `z` 参数的函数: + +```py +def my_function2(x, y, z=1.5): + if z > 1: + return z * (x + y) + else: + return z / (x + y) +``` + +虽然关键字参数是可选的,但在调用函数时必须指定所有位置参数。 + +您可以向 `z` 参数传递值,可以使用关键字也可以不使用关键字,但建议使用关键字: + +```py +In [181]: my_function2(5, 6, z=0.7) +Out[181]: 0.06363636363636363 + +In [182]: my_function2(3.14, 7, 3.5) +Out[182]: 35.49 + +In [183]: my_function2(10, 20) +Out[183]: 45.0 +``` + +对函数参数的主要限制是关键字参数 *必须* 跟在位置参数(如果有的话)后面。您可以以任何顺序指定关键字参数。这使您不必记住函数参数的指定顺序。您只需要记住它们的名称。 + +### 命名空间、作用域和本地函数 + +函数可以访问函数内部创建的变量以及函数外部在更高(甚至 *全局*)作用域中的变量。在 Python 中描述变量作用域的另一种更具描述性的名称是 *命名空间*。在函数内部分配的任何变量默认分配给本地命名空间。本地命名空间在函数调用时创建,并立即由函数的参数填充。函数完成后,本地命名空间将被销毁(有一些例外情况超出了本章的范围)。考虑以下函数: + +```py +def func(): + a = [] + for i in range(5): + a.append(i) +``` + +当调用 `func()` 时,将创建空列表 `a`,附加五个元素,然后在函数退出时销毁 `a`。假设我们改为这样声明 `a`: + +```py +In [184]: a = [] + +In [185]: def func(): + .....: for i in range(5): + .....: a.append(i) +``` + +每次调用 `func` 都会修改列表 `a`: + +```py +In [186]: func() + +In [187]: a +Out[187]: [0, 1, 2, 3, 4] + +In [188]: func() + +In [189]: a +Out[189]: [0, 1, 2, 3, 4, 0, 1, 2, 3, 4] +``` + +在函数范围之外分配变量是可能的,但这些变量必须使用 `global` 或 `nonlocal` 关键字显式声明: + +```py +In [190]: a = None + +In [191]: def bind_a_variable(): + .....: global a + .....: a = [] + .....: bind_a_variable() + .....: + +In [192]: print(a) +[] +``` + +`nonlocal` 允许函数修改在非全局高级作用域中定义的变量。由于它的使用有些神秘(我在这本书中从未使用过它),我建议您查阅 Python 文档以了解更多信息。 + +注意 + +我通常不鼓励使用 `global` 关键字。通常,全局变量用于在系统中存储某种状态。如果您发现自己使用了很多全局变量,这可能表明需要使用面向对象编程(使用类) + +### 返回多个值 + +当我在 Java 和 C++ 中编程后第一次在 Python 中编程时,我最喜欢的功能之一是能够以简单的语法从函数中返回多个值。这里有一个例子: + +```py +def f(): + a = 5 + b = 6 + c = 7 + return a, b, c + +a, b, c = f() +``` + +在数据分析和其他科学应用中,您可能经常这样做。这里发生的是函数实际上只返回一个对象,一个元组,然后将其解包为结果变量。在前面的例子中,我们可以这样做: + +```py +return_value = f() +``` + +在这种情况下,`return_value` 将是一个包含三个返回变量的 3 元组。与之前返回多个值的一个潜在有吸引力的替代方法可能是返回一个字典: + +```py +def f(): + a = 5 + b = 6 + c = 7 + return {"a" : a, "b" : b, "c" : c} +``` + +这种替代技术可以根据您尝试做什么而有用。 + +### 函数是对象 + +由于 Python 函数是对象,许多构造可以很容易地表达,而在其他语言中很难做到。假设我们正在进行一些数据清理,并需要对以下字符串列表应用一系列转换: + +```py +In [193]: states = [" Alabama ", "Georgia!", "Georgia", "georgia", "FlOrIda", + .....: "south carolina##", "West virginia?"] +``` + +任何曾经处理过用户提交的调查数据的人都会看到这样混乱的结果。需要做很多事情才能使这个字符串列表统一并准备好进行分析:去除空格、删除标点符号,并标准化适当的大写。其中一种方法是使用内置的字符串方法以及 `re` 标准库模块进行正则表达式: + +```py +import re + +def clean_strings(strings): + result = [] + for value in strings: + value = value.strip() + value = re.sub("[!#?]", "", value) + value = value.title() + result.append(value) + return result +``` + +结果如下: + +```py +In [195]: clean_strings(states) +Out[195]: +['Alabama', + 'Georgia', + 'Georgia', + 'Georgia', + 'Florida', + 'South Carolina', + 'West Virginia'] +``` + +您可能会发现有用的另一种方法是制作一个要应用于特定字符串集的操作列表: + +```py +def remove_punctuation(value): + return re.sub("[!#?]", "", value) + +clean_ops = [str.strip, remove_punctuation, str.title] + +def clean_strings(strings, ops): + result = [] + for value in strings: + for func in ops: + value = func(value) + result.append(value) + return result +``` + +然后我们有以下内容: + +```py +In [197]: clean_strings(states, clean_ops) +Out[197]: +['Alabama', + 'Georgia', + 'Georgia', + 'Georgia', + 'Florida', + 'South Carolina', + 'West Virginia'] +``` + +像这样的更*函数式*模式使您能够轻松修改字符串在非常高级别上的转换方式。`clean_strings`函数现在也更具可重用性和通用性。 + +您可以将函数用作其他函数的参数,比如内置的`map`函数,它将一个函数应用于某种序列: + +```py +In [198]: for x in map(remove_punctuation, states): + .....: print(x) +Alabama +Georgia +Georgia +georgia +FlOrIda +south carolina +West virginia +``` + +`map`可以作为替代方案用于列表推导而不需要任何过滤器。 + +### 匿名(Lambda)函数 + +Python 支持所谓的*匿名*或*lambda*函数,这是一种编写由单个语句组成的函数的方式,其结果是返回值。它们使用`lambda`关键字定义,该关键字除了“我们正在声明一个匿名函数”之外没有其他含义: + +```py +In [199]: def short_function(x): + .....: return x * 2 + +In [200]: equiv_anon = lambda x: x * 2 +``` + +我通常在本书的其余部分中将这些称为 lambda 函数。它们在数据分析中特别方便,因为正如您将看到的,有许多情况下,数据转换函数将接受函数作为参数。与编写完整函数声明或甚至将 lambda 函数分配给本地变量相比,传递 lambda 函数通常更少输入(更清晰)。考虑这个例子: + +```py +In [201]: def apply_to_list(some_list, f): + .....: return [f(x) for x in some_list] + +In [202]: ints = [4, 0, 1, 5, 6] + +In [203]: apply_to_list(ints, lambda x: x * 2) +Out[203]: [8, 0, 2, 10, 12] +``` + +您也可以写成`[x * 2 for x in ints]`,但在这里我们能够简洁地将自定义运算符传递给`apply_to_list`函数。 + +举个例子,假设你想按每个字符串中不同字母的数量对字符串集合进行排序: + +```py +In [204]: strings = ["foo", "card", "bar", "aaaa", "abab"] +``` + +在这里,我们可以将一个 lambda 函数传递给列表的`sort`方法: + +```py +In [205]: strings.sort(key=lambda x: len(set(x))) + +In [206]: strings +Out[206]: ['aaaa', 'foo', 'abab', 'bar', 'card'] +``` + +### 生成器 + +Python 中的许多对象支持迭代,例如列表中的对象或文件中的行。这是通过*迭代器协议*实现的,这是一种使对象可迭代的通用方法。例如,对字典进行迭代会产生字典键: + +```py +In [207]: some_dict = {"a": 1, "b": 2, "c": 3} + +In [208]: for key in some_dict: + .....: print(key) +a +b +c +``` + +当您写`for key in some_dict`时,Python 解释器首先尝试从`some_dict`创建一个迭代器: + +```py +In [209]: dict_iterator = iter(some_dict) + +In [210]: dict_iterator +Out[210]: +``` + +迭代器是任何对象,在上下文中像`for`循环中使用时,将向 Python 解释器产生对象。大多数期望列表或类似列表的对象的方法也将接受任何可迭代对象。这包括内置方法如`min`、`max`和`sum`,以及类构造函数如`list`和`tuple`: + +```py +In [211]: list(dict_iterator) +Out[211]: ['a', 'b', 'c'] +``` + +*生成器*是一种方便的方式,类似于编写普通函数,来构造一个新的可迭代对象。普通函数一次执行并返回一个结果,而生成器可以通过暂停和恢复执行每次使用生成器时返回多个值的序列。要创建一个生成器,请在函数中使用`yield`关键字而不是`return`: + +```py +def squares(n=10): + print(f"Generating squares from 1 to {n ** 2}") + for i in range(1, n + 1): + yield i ** 2 +``` + +当您实际调用生成器时,不会立即执行任何代码: + +```py +In [213]: gen = squares() + +In [214]: gen +Out[214]: +``` + +直到您请求生成器的元素时,它才开始执行其代码: + +```py +In [215]: for x in gen: + .....: print(x, end=" ") +Generating squares from 1 to 100 +1 4 9 16 25 36 49 64 81 100 +``` + +注意 + +由于生成器一次产生一个元素的输出,而不是一次产生整个列表,这可以帮助您的程序使用更少的内存。 + +#### 生成器表达式 + +另一种生成器的方法是使用*生成器表达式*。这是列表、字典和集合推导的生成器类比。要创建一个,将否则是列表推导的内容括在括号中而不是方括号中: + +```py +In [216]: gen = (x ** 2 for x in range(100)) + +In [217]: gen +Out[217]: at 0x17d5feff0> +``` + +这等同于以下更冗长的生成器: + +```py +def _make_gen(): + for x in range(100): + yield x ** 2 +gen = _make_gen() +``` + +生成器表达式可以在某些情况下用作函数参数,而不是列表推导: + +```py +In [218]: sum(x ** 2 for x in range(100)) +Out[218]: 328350 + +In [219]: dict((i, i ** 2) for i in range(5)) +Out[219]: {0: 0, 1: 1, 2: 4, 3: 9, 4: 16} +``` + +根据推导表达式产生的元素数量,生成器版本有时可以更有意义地更快。 + +#### itertools 模块 + +标准库`itertools`模块具有许多常见数据算法的生成器集合。例如,`groupby`接受任何序列和一个函数,通过函数的返回值对序列中的连续元素进行分组。这里是一个例子: + +```py +In [220]: import itertools + +In [221]: def first_letter(x): + .....: return x[0] + +In [222]: names = ["Alan", "Adam", "Wes", "Will", "Albert", "Steven"] + +In [223]: for letter, names in itertools.groupby(names, first_letter): + .....: print(letter, list(names)) # names is a generator +A ['Alan', 'Adam'] +W ['Wes', 'Will'] +A ['Albert'] +S ['Steven'] +``` + +查看表 3.2 以获取我经常发现有用的其他一些`itertools`函数列表。您可能想查看[官方 Python 文档](https://docs.python.org/3/library/itertools.html)以获取有关这个有用的内置实用程序模块的更多信息。 + +表 3.2:一些有用的`itertools`函数 + +| 函数 | 描述 | +| --- | --- | +| `chain(*iterables)` | 通过将迭代器链接在一起生成序列。一旦第一个迭代器的元素用尽,将返回下一个迭代器的元素,依此类推。 | +| `combinations(iterable, k)` | 生成可迭代对象中所有可能的`k`元素元组的序列,忽略顺序且不重复(另请参阅伴随函数`combinations_with_replacement`)。 | +| `permutations(iterable, k)` | 生成可迭代对象中所有可能的`k`元素元组的序列,保持顺序。 | +| `groupby(iterable[, keyfunc])` | 为每个唯一键生成`(key, sub-iterator)`。 | + +| `product(*iterables, repeat=1)` | 生成输入可迭代对象的笛卡尔积作为元组,类似于嵌套的`for`循环。 | + +### 错误和异常处理 + +处理 Python 错误或*异常*的优雅是构建健壮程序的重要部分。在数据分析应用中,许多函数只对特定类型的输入有效。例如,Python 的`float`函数能够将字符串转换为浮点数,但在不当输入时会引发`ValueError`异常: + +```py +In [224]: float("1.2345") +Out[224]: 1.2345 + +In [225]: float("something") +--------------------------------------------------------------------------- +ValueError Traceback (most recent call last) + in +----> 1 float("something") +ValueError: could not convert string to float: 'something' +``` + +假设我们想要一个版本的`float`,它能够优雅地失败,返回输入参数。我们可以通过编写一个函数,在其中将对`float`的调用封装在`try`/`except`块中来实现这一点(在 IPython 中执行此代码): + +```py +def attempt_float(x): + try: + return float(x) + except: + return x +``` + +块中的`except`部分的代码只有在`float(x)`引发异常时才会执行: + +```py +In [227]: attempt_float("1.2345") +Out[227]: 1.2345 + +In [228]: attempt_float("something") +Out[228]: 'something' +``` + +您可能会注意到`float`可能引发除`ValueError`之外的异常: + +```py +In [229]: float((1, 2)) +--------------------------------------------------------------------------- +TypeError Traceback (most recent call last) + in +----> 1 float((1, 2)) +TypeError: float() argument must be a string or a real number, not 'tuple' +``` + +您可能只想抑制`ValueError`,因为`TypeError`(输入不是字符串或数值)可能表明程序中存在合法错误。要做到这一点,请在`except`后面写上异常类型: + +```py +def attempt_float(x): + try: + return float(x) + except ValueError: + return x +``` + +然后我们有: + +```py +In [231]: attempt_float((1, 2)) +--------------------------------------------------------------------------- +TypeError Traceback (most recent call last) + in +----> 1 attempt_float((1, 2)) + in attempt_float(x) + 1 def attempt_float(x): + 2 try: +----> 3 return float(x) + 4 except ValueError: + 5 return x +TypeError: float() argument must be a string or a real number, not 'tuple' +``` + +您可以通过编写异常类型的元组来捕获多个异常类型(括号是必需的): + +```py +def attempt_float(x): + try: + return float(x) + except (TypeError, ValueError): + return x +``` + +在某些情况下,您可能不想抑制异常,但您希望无论`try`块中的代码是否成功,都执行一些代码。要做到这一点,请使用`finally`: + +```py +f = open(path, mode="w") + +try: + write_to_file(f) +finally: + f.close() +``` + +在这里,文件对象`f`将*始终*被关闭。同样,您可以使用`else`来执行仅在`try:`块成功时执行的代码: + +```py +f = open(path, mode="w") + +try: + write_to_file(f) +except: + print("Failed") +else: + print("Succeeded") +finally: + f.close() +``` + +#### 在 IPython 中的异常 + +如果在`%run`脚本或执行任何语句时引发异常,默认情况下 IPython 将打印完整的调用堆栈跟踪(traceback),并在堆栈中的每个位置周围显示几行上下文: + +```py +In [10]: %run examples/ipython_bug.py +--------------------------------------------------------------------------- +AssertionError Traceback (most recent call last) +/home/wesm/code/pydata-book/examples/ipython_bug.py in () + 13 throws_an_exception() + 14 +---> 15 calling_things() + +/home/wesm/code/pydata-book/examples/ipython_bug.py in calling_things() + 11 def calling_things(): + 12 works_fine() +---> 13 throws_an_exception() + 14 + 15 calling_things() + +/home/wesm/code/pydata-book/examples/ipython_bug.py in throws_an_exception() + 7 a = 5 + 8 b = 6 +----> 9 assert(a + b == 10) + 10 + 11 def calling_things(): + +AssertionError: +``` + +仅仅通过附加上下文本身就是与标准 Python 解释器相比的一个巨大优势(标准 Python 解释器不提供任何额外上下文)。您可以使用`%xmode`魔术命令来控制显示的上下文量,从`Plain`(与标准 Python 解释器相同)到`Verbose`(内联函数参数值等)。正如您将在附录 B:更多关于 IPython 系统中看到的,您可以在错误发生后进行交互式事后调试,进入堆栈(使用`%debug`或`%pdb`魔术)。 + +本书的大部分内容使用高级工具如`pandas.read_csv`从磁盘读取数据文件到 Python 数据结构中。然而,了解如何在 Python 中处理文件的基础知识是很重要的。幸运的是,这相对简单,这也是 Python 在文本和文件处理方面如此受欢迎的原因。 + +要打开一个文件进行读取或写入,请使用内置的`open`函数,使用相对或绝对文件路径以及可选的文件编码: + +```py +In [233]: path = "examples/segismundo.txt" + +In [234]: f = open(path, encoding="utf-8") +``` + +在这里,我传递 `encoding="utf-8"` 作为最佳实践,因为默认的 Unicode 编码读取文件在不同平台上有所不同。 + +默认情况下,文件以只读模式 `"r"` 打开。然后我们可以像处理列表一样处理文件对象 `f` 并迭代文件行: + +```py +for line in f: + print(line) +``` + +行从文件中出来时保留了行尾(EOL)标记,因此您经常会看到代码以获取文件中无行尾的行列表,如下所示: + +```py +In [235]: lines = [x.rstrip() for x in open(path, encoding="utf-8")] + +In [236]: lines +Out[236]: +['Sueña el rico en su riqueza,', + 'que más cuidados le ofrece;', + '', + 'sueña el pobre que padece', + 'su miseria y su pobreza;', + '', + 'sueña el que a medrar empieza,', + 'sueña el que afana y pretende,', + 'sueña el que agravia y ofende,', + '', + 'y en el mundo, en conclusión,', + 'todos sueñan lo que son,', + 'aunque ninguno lo entiende.', + ''] +``` + +当使用 `open` 创建文件对象时,建议在完成后关闭文件。关闭文件会将其资源释放回操作系统: + +```py +In [237]: f.close() +``` + +使得清理打开文件更容易的一种方法是使用 `with` 语句: + +```py +In [238]: with open(path, encoding="utf-8") as f: + .....: lines = [x.rstrip() for x in f] +``` + +当退出 `with` 块时,这将自动关闭文件 `f`。确保关闭文件在许多小程序或脚本中不会导致问题,但在需要与大量文件交互的程序中可能会出现问题。 + +如果我们输入 `f = open(path, "w")`,*examples/segismundo.txt* 将会创建一个*新文件*(小心!),覆盖原来的任何文件。还有 `"x"` 文件模式,它创建一个可写文件,但如果文件路径已经存在则失败。查看 Table 3.3 获取所有有效的文件读写模式列表。 + +Table 3.3: Python 文件模式 + +| 模式 | 描述 | +| --- | --- | +| `r` | 只读模式 | +| `w` | 只写模式;创建一个新文件(擦除同名文件的数据) | +| `x` | 只写模式;创建一个新文件,但如果文件路径已经存在则失败 | +| `a` | 追加到现有文件(如果文件不存在则创建文件) | +| `r+` | 读取和写入 | +| `b` | 用于二进制文件的附加模式(即 `"rb"` 或 `"wb"`) | +| `t` | 文件的文本模式(自动将字节解码为 Unicode);如果未指定,则为默认模式 | + +对于可读文件,一些最常用的方法是 `read`、`seek` 和 `tell`。`read` 从文件返回一定数量的字符。什么构成一个“字符”取决于文件编码,或者如果文件以二进制模式打开,则是原始字节: + +```py +In [239]: f1 = open(path) + +In [240]: f1.read(10) +Out[240]: 'Sueña el r' + +In [241]: f2 = open(path, mode="rb") # Binary mode + +In [242]: f2.read(10) +Out[242]: b'Sue\xc3\xb1a el ' +``` + +`read` 方法通过读取的字节数推进文件对象位置。`tell` 给出当前位置: + +```py +In [243]: f1.tell() +Out[243]: 11 + +In [244]: f2.tell() +Out[244]: 10 +``` + +即使我们从以文本模式打开的文件 `f1` 中读取了 10 个字符,位置也是 11,因为使用默认编码解码 10 个字符需要这么多字节。您可以在 `sys` 模块中检查默认编码: + +```py +In [245]: import sys + +In [246]: sys.getdefaultencoding() +Out[246]: 'utf-8' +``` + +为了在各个平台上获得一致的行为,最好在打开文件时传递一个编码(例如 `encoding="utf-8"`,这是广泛使用的)。 + +`seek` 将文件位置更改为文件中指定的字节: + +```py +In [247]: f1.seek(3) +Out[247]: 3 + +In [248]: f1.read(1) +Out[248]: 'ñ' + +In [249]: f1.tell() +Out[249]: 5 +``` + +最后,我们记得关闭文件: + +```py +In [250]: f1.close() + +In [251]: f2.close() +``` + +要将文本写入文件,可以使用文件的 `write` 或 `writelines` 方法。例如,我们可以创建一个没有空行的 *examples/segismundo.txt* 版本如下: + +```py +In [252]: path +Out[252]: 'examples/segismundo.txt' + +In [253]: with open("tmp.txt", mode="w") as handle: + .....: handle.writelines(x for x in open(path) if len(x) > 1) + +In [254]: with open("tmp.txt") as f: + .....: lines = f.readlines() + +In [255]: lines +Out[255]: +['Sueña el rico en su riqueza,\n', + 'que más cuidados le ofrece;\n', + 'sueña el pobre que padece\n', + 'su miseria y su pobreza;\n', + 'sueña el que a medrar empieza,\n', + 'sueña el que afana y pretende,\n', + 'sueña el que agravia y ofende,\n', + 'y en el mundo, en conclusión,\n', + 'todos sueñan lo que son,\n', + 'aunque ninguno lo entiende.\n'] +``` + +查看 Table 3.4 获取许多最常用的文件方法。 + +Table 3.4: 重要的 Python 文件方法或属性 + +| 方法/属性 | 描述 | +| --- | --- | +| `read([size])` | 根据文件模式返回文件数据作为字节或字符串,可选的 `size` 参数指示要读取的字节数或字符串字符数 | +| `readable()` | 如果文件支持 `read` 操作则返回 `True` | +| `readlines([size])` | 返回文件中行的列表,带有可选的 `size` 参数 | +| `write(string)` | 将传递的字符串写入文件 | +| `writable()` | 如果文件支持 `write` 操作则返回 `True` | +| `writelines(strings)` | 将传递的字符串序列写入文件 | +| `close()` | 关闭文件对象 | +| `flush()` | 刷新内部 I/O 缓冲区到磁盘 | +| `seek(pos)` | 移动到指定的文件位置(整数) | +| `seekable()` | 如果文件对象支持寻找并且随机访问则返回 `True`(某些类似文件的对象不支持) | +| `tell()` | 返回当前文件位置作为整数 | +| `closed` | 如果文件已关闭则为`True` | +| `encoding` | 用于将文件中的字节解释为 Unicode 的编码(通常为 UTF-8) | + +### 字节和 Unicode 与文件 + +Python 文件的默认行为(无论是可读还是可写)是*文本模式*,这意味着您打算使用 Python 字符串(即 Unicode)。这与*二进制模式*相反,您可以通过在文件模式后附加`b`来获得。重新访问上一节中包含 UTF-8 编码的非 ASCII 字符的文件,我们有: + +```py +In [258]: with open(path) as f: + .....: chars = f.read(10) + +In [259]: chars +Out[259]: 'Sueña el r' + +In [260]: len(chars) +Out[260]: 10 +``` + +UTF-8 是一种可变长度的 Unicode 编码,因此当我从文件请求一些字符时,Python 会读取足够的字节(可能少至 10 个或多至 40 个字节)来解码相应数量的字符。如果我以`"rb"`模式打开文件,`read`请求确切数量的字节: + +```py +In [261]: with open(path, mode="rb") as f: + .....: data = f.read(10) + +In [262]: data +Out[262]: b'Sue\xc3\xb1a el ' +``` + +根据文本编码,您可能可以自己将字节解码为`str`对象,但前提是每个编码的 Unicode 字符都是完整形式的: + +```py +In [263]: data.decode("utf-8") +Out[263]: 'Sueña el ' + +In [264]: data[:4].decode("utf-8") +--------------------------------------------------------------------------- +UnicodeDecodeError Traceback (most recent call last) + in +----> 1 data[:4].decode("utf-8") +UnicodeDecodeError: 'utf-8' codec can't decode byte 0xc3 in position 3: unexpecte +d end of data +``` + +文本模式,结合`open`的`encoding`选项,提供了一种方便的方法来将一个 Unicode 编码转换为另一个: + +```py +In [265]: sink_path = "sink.txt" + +In [266]: with open(path) as source: + .....: with open(sink_path, "x", encoding="iso-8859-1") as sink: + .....: sink.write(source.read()) + +In [267]: with open(sink_path, encoding="iso-8859-1") as f: + .....: print(f.read(10)) +Sueña el r +``` + +在除了二进制模式之外的任何模式下打开文件时要小心使用`seek`。如果文件位置落在定义 Unicode 字符的字节中间,那么后续的读取将导致错误: + +```py +In [269]: f = open(path, encoding='utf-8') + +In [270]: f.read(5) +Out[270]: 'Sueña' + +In [271]: f.seek(4) +Out[271]: 4 + +In [272]: f.read(1) +--------------------------------------------------------------------------- +UnicodeDecodeError Traceback (most recent call last) + in +----> 1 f.read(1) +~/miniforge-x86/envs/book-env/lib/python3.10/codecs.py in decode(self, input, fin +al) + 320 # decode input (taking the buffer into account) + 321 data = self.buffer + input +--> 322 (result, consumed) = self._buffer_decode(data, self.errors, final +) + 323 # keep undecoded input until the next call + 324 self.buffer = data[consumed:] +UnicodeDecodeError: 'utf-8' codec can't decode byte 0xb1 in position 0: invalid s +tart byte + +In [273]: f.close() +``` + +如果您经常在非 ASCII 文本数据上进行数据分析,掌握 Python 的 Unicode 功能将会很有价值。查看[Python 的在线文档](https://docs.python.org)获取更多信息。 + +## 3.4 结论 + +随着 Python 环境和语言的一些基础知识现在掌握,是时候继续学习 Python 中的 NumPy 和面向数组的计算了。 diff --git a/translations/cn/pyda3e_07.md b/translations/cn/pyda3e_07.md new file mode 100644 index 000000000..64492cd14 --- /dev/null +++ b/translations/cn/pyda3e_07.md @@ -0,0 +1,1780 @@ +# 四、NumPy 基础知识:数组和向量化计算 + +> 原文:[`wesmckinney.com/book/numpy-basics`](https://wesmckinney.com/book/numpy-basics) +> +> 译者:[飞龙](https://github.com/wizardforcel) +> +> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) + + +> 此开放访问网络版本的《Python 数据分析第三版》现已作为[印刷版和数字版](https://amzn.to/3DyLaJc)的伴侣提供。如果您发现任何勘误,请[在此处报告](https://oreilly.com/catalog/0636920519829/errata)。请注意,由 Quarto 生成的本站点的某些方面与 O'Reilly 的印刷版和电子书版本的格式不同。 +> +> 如果您发现本书的在线版本有用,请考虑[订购纸质版](https://amzn.to/3DyLaJc)或[无 DRM 的电子书](https://www.ebooks.com/en-us/book/210644288/python-for-data-analysis/wes-mckinney/?affId=WES398681F)以支持作者。本网站的内容不得复制或再生产。代码示例采用 MIT 许可,可在 GitHub 或 Gitee 上找到。 + +NumPy,即 Numerical Python,是 Python 中最重要的数值计算基础包之一。许多提供科学功能的计算包使用 NumPy 的数组对象作为数据交换的标准接口之一。我涵盖的关于 NumPy 的许多知识也适用于 pandas。 + +以下是您将在 NumPy 中找到的一些内容: + ++ ndarray,一种高效的多维数组,提供快速的基于数组的算术运算和灵活的广播功能 + ++ 用于在整个数据数组上快速操作的数学函数,而无需编写循环 + ++ 用于读取/写入数组数据到磁盘和处理内存映射文件的工具 + ++ 线性代数、随机数生成和傅里叶变换功能 + ++ 用于将 NumPy 与用 C、C++或 FORTRAN 编写的库连接的 C API + +由于 NumPy 提供了全面且有文档的 C API,因此将数据传递给用低级语言编写的外部库,以及让外部库将数据作为 NumPy 数组返回给 Python 是很简单的。这个特性使 Python 成为封装传统 C、C++或 FORTRAN 代码库并为其提供动态和可访问接口的首选语言。 + +虽然 NumPy 本身并不提供建模或科学功能,但了解 NumPy 数组和面向数组的计算将帮助您更有效地使用具有数组计算语义的工具,如 pandas。由于 NumPy 是一个庞大的主题,我将在以后更深入地涵盖许多高级 NumPy 功能,比如广播(参见附录 A:高级 NumPy)。这些高级功能中的许多并不需要遵循本书的其余部分,但在您深入研究 Python 科学计算时可能会有所帮助。 + +对于大多数数据分析应用程序,我将关注的主要功能领域是: + ++ 用于数据整理和清洗、子集和过滤、转换以及任何其他类型计算的快速基于数组的操作 + ++ 常见的数组算法,如排序、唯一值和集合操作 + ++ 高效的描述统计和聚合/汇总数据 + ++ 数据对齐和关系数据操作,用于合并和连接异构数据集 + ++ 将条件逻辑表达为数组表达式,而不是使用`if-elif-else`分支循环 + ++ 分组数据操作(聚合、转换和函数应用) + +虽然 NumPy 为一般数值数据处理提供了计算基础,但许多读者将希望使用 pandas 作为大多数统计或分析的基础,尤其是在表格数据上。此外,pandas 还提供了一些更具领域特定功能,如时间序列操作,这在 NumPy 中不存在。 + +注意 + +Python 中的面向数组计算可以追溯到 1995 年,当时 Jim Hugunin 创建了 Numeric 库。在接下来的 10 年里,许多科学编程社区开始在 Python 中进行数组编程,但在 2000 年代初,库生态系统变得分散。2005 年,Travis Oliphant 能够从当时的 Numeric 和 Numarray 项目中打造出 NumPy 项目,将社区团结在一个单一的数组计算框架周围。 + +NumPy 在 Python 中进行数值计算如此重要的原因之一是因为它专为大型数据数组的效率而设计。这有几个原因:* + ++ NumPy 在内部以连续的内存块存储数据,独立于其他内置 Python 对象。NumPy 的用 C 语言编写的算法库可以在这个内存上操作,而无需进行任何类型检查或其他开销。NumPy 数组也比内置 Python 序列使用更少的内存。 + ++ NumPy 操作在整个数组上执行复杂计算,无需 Python `for`循环,对于大型序列来说,这可能会很慢。NumPy 比常规 Python 代码更快,因为它的基于 C 的算法避免了常规解释 Python 代码的开销。 + +为了让您了解性能差异,考虑一个包含一百万个整数的 NumPy 数组,以及等效的 Python 列表: + +```py +In [7]: import numpy as np + +In [8]: my_arr = np.arange(1_000_000) + +In [9]: my_list = list(range(1_000_000)) +``` + +现在让我们将每个序列乘以 2: + +```py +In [10]: %timeit my_arr2 = my_arr * 2 +309 us +- 7.48 us per loop (mean +- std. dev. of 7 runs, 1000 loops each) + +In [11]: %timeit my_list2 = [x * 2 for x in my_list] +46.4 ms +- 526 us per loop (mean +- std. dev. of 7 runs, 10 loops each) +``` + +基于 NumPy 的算法通常比纯 Python 对应算法快 10 到 100 倍(或更多),并且使用的内存明显更少。 + +## 4.1 NumPy ndarray:多维数组对象 + +NumPy 的一个关键特性是其 N 维数组对象,或者 ndarray,它是 Python 中大型数据集的快速、灵活的容器。数组使您能够使用类似标量元素之间等效操作的语法在整个数据块上执行数学运算。 + +为了让您了解 NumPy 如何使用类似标量值的语法在内置 Python 对象上进行批量计算,我首先导入 NumPy 并创建一个小数组: + +```py +In [12]: import numpy as np + +In [13]: data = np.array([[1.5, -0.1, 3], [0, -3, 6.5]]) + +In [14]: data +Out[14]: +array([[ 1.5, -0.1, 3. ], + [ 0. , -3. , 6.5]]) +``` + +然后我用`data`编写数学运算: + +```py +In [15]: data * 10 +Out[15]: +array([[ 15., -1., 30.], + [ 0., -30., 65.]]) + +In [16]: data + data +Out[16]: +array([[ 3. , -0.2, 6. ], + [ 0. , -6. , 13. ]]) +``` + +在第一个示例中,所有元素都乘以了 10。在第二个示例中,数组中每个“单元格”中的相应值已经相加。 + +注意 + +在本章和整本书中,我使用标准的 NumPy 约定,始终使用`import numpy as np`。您可以在代码中使用`from numpy import *`来避免编写`np.`,但我建议不要养成这种习惯。`numpy`命名空间很大,包含许多函数,它们的名称与内置 Python 函数(如`min`和`max`)冲突。遵循这些标准约定几乎总是一个好主意。 + +ndarray 是一个用于同质数据的通用多维容器;也就是说,所有元素必须是相同类型。每个数组都有一个`shape`,一个指示每个维度大小的元组,以及一个`dtype`,描述数组的*数据类型*的对象: + +```py +In [17]: data.shape +Out[17]: (2, 3) + +In [18]: data.dtype +Out[18]: dtype('float64') +``` + +本章将介绍使用 NumPy 数组的基础知识,这应该足以跟随本书的其余部分。虽然对于许多数据分析应用程序来说,深入了解 NumPy 并不是必需的,但精通面向数组的编程和思维是成为科学 Python 大师的关键步骤。 + +注意 + +在书中文本中,每当您看到“array”,“NumPy array”或“ndarray”时,在大多数情况下它们都指的是 ndarray 对象。 + +### 创建 ndarrays + +创建数组的最简单方法是使用`array`函数。它接受任何类似序列的对象(包括其他数组)并生成包含传递数据的新 NumPy 数组。例如,列表是一个很好的转换候选: + +```py +In [19]: data1 = [6, 7.5, 8, 0, 1] + +In [20]: arr1 = np.array(data1) + +In [21]: arr1 +Out[21]: array([6. , 7.5, 8. , 0. , 1. ]) +``` + +嵌套序列,比如等长列表的列表,将被转换为多维数组: + +```py +In [22]: data2 = [[1, 2, 3, 4], [5, 6, 7, 8]] + +In [23]: arr2 = np.array(data2) + +In [24]: arr2 +Out[24]: +array([[1, 2, 3, 4], + [5, 6, 7, 8]]) +``` + +由于`data2`是一个列表的列表,NumPy 数组`arr2`具有两个维度,形状从数据中推断出。我们可以通过检查`ndim`和`shape`属性来确认这一点: + +```py +In [25]: arr2.ndim +Out[25]: 2 + +In [26]: arr2.shape +Out[26]: (2, 4) +``` + +除非明确指定(在 ndarrays 的数据类型中讨论),`numpy.array`会尝试推断创建的数组的良好数据类型。数据类型存储在特殊的`dtype`元数据对象中;例如,在前两个示例中我们有: + +```py +In [27]: arr1.dtype +Out[27]: dtype('float64') + +In [28]: arr2.dtype +Out[28]: dtype('int64') +``` + +除了`numpy.array`之外,还有许多其他用于创建新数组的函数。例如,`numpy.zeros`和`numpy.ones`分别创建长度或形状为 0 或 1 的数组。`numpy.empty`创建一个数组,而不将其值初始化为任何特定值。要使用这些方法创建更高维度的数组,请传递一个形状的元组: + +```py +In [29]: np.zeros(10) +Out[29]: array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]) + +In [30]: np.zeros((3, 6)) +Out[30]: +array([[0., 0., 0., 0., 0., 0.], + [0., 0., 0., 0., 0., 0.], + [0., 0., 0., 0., 0., 0.]]) + +In [31]: np.empty((2, 3, 2)) +Out[31]: +array([[[0., 0.], + [0., 0.], + [0., 0.]], + [[0., 0.], + [0., 0.], + [0., 0.]]]) +``` + +注意 + +不能假设`numpy.empty`会返回一个全为零的数组。该函数返回未初始化的内存,因此可能包含非零的“垃圾”值。只有在打算用数据填充新数组时才应使用此函数。 + +`numpy.arange`是内置 Python `range`函数的数组版本: + +```py +In [32]: np.arange(15) +Out[32]: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]) +``` + +请参见表 4.1 中的一些标准数组创建函数的简要列表。由于 NumPy 专注于数值计算,如果未指定数据类型,数据类型在许多情况下将是`float64`(浮点数)。 + +表 4.1:一些重要的 NumPy 数组创建函数 + +| 函数 | 描述 | +| --- | --- | +| `array` | 将输入数据(列表、元组、数组或其他序列类型)转换为 ndarray,可以通过推断数据类型或显式指定数据类型来完成;默认情况下会复制输入数据 | +| `asarray` | 将输入转换为 ndarray,如果输入已经是 ndarray,则不复制 | +| `arange` | 类似于内置的`range`,但返回一个 ndarray 而不是列表 | +| `ones, ones_like` | 生成所有值为 1 的数组,具有给定的形状和数据类型;`ones_like`接受另一个数组,并生成相同形状和数据类型的`ones`数组 | +| `zeros, zeros_like` | 类似于`ones`和`ones_like`,但生成的是全为 0 的数组 | +| `empty, empty_like` | 通过分配新内存创建新数组,但不像`ones`和`zeros`那样填充任何值 | +| `full, full_like` | 生成具有给定形状和数据类型的数组,所有值都设置为指定的“填充值”;`full_like`接受另一个数组,并生成相同形状和数据类型的填充数组 | + +| `eye, identity` | 创建一个 N×N 的方阵单位矩阵(对角线上为 1,其他地方为 0) | + +### ndarrays 的数据类型 + +*数据类型*或`dtype`是一个特殊对象,包含 ndarray 需要将内存块解释为特定类型数据的信息(或*元数据*,关于数据的数据): + +```py +In [33]: arr1 = np.array([1, 2, 3], dtype=np.float64) + +In [34]: arr2 = np.array([1, 2, 3], dtype=np.int32) + +In [35]: arr1.dtype +Out[35]: dtype('float64') + +In [36]: arr2.dtype +Out[36]: dtype('int32') +``` + +数据类型是 NumPy 灵活性的来源,用于与来自其他系统的数据进行交互。在大多数情况下,它们直接映射到底层磁盘或内存表示,这使得可以将数据的二进制流读写到磁盘,并连接到用低级语言(如 C 或 FORTRAN)编写的代码。数值数据类型的命名方式相同:类型名称,如`float`或`int`,后跟表示每个元素的位数的数字。标准的双精度浮点值(Python 中`float`对象底层使用的)占用 8 字节或 64 位。因此,在 NumPy 中,此类型称为`float64`。请参见表 4.2 以获取 NumPy 支持的数据类型的完整列表。 + +注意 + +不要担心记住 NumPy 数据类型,特别是如果您是新用户。通常只需要关心您正在处理的数据的一般*类型*,无论是浮点数、复数、整数、布尔值、字符串还是一般的 Python 对象。当您需要更多控制数据在内存和磁盘上的存储方式,特别是对于大型数据集时,知道您可以控制存储类型是很好的。 + +表 4.2:NumPy 数据类型 + +| 类型 | 类型代码 | 描述 | +| --- | --- | --- | +| `int8, uint8` | `i1, u1` | 有符号和无符号 8 位(1 字节)整数类型 | +| `int16, uint16` | `i2, u2` | 有符号和无符号 16 位整数类型 | +| `int32, uint32` | `i4, u4` | 有符号和无符号 32 位整数类型 | +| `int64, uint64` | `i8, u8` | 有符号和无符号 64 位整数类型 | +| `float16` | `f2` | 半精度浮点数 | +| `float32` | `f4 或 f` | 标准单精度浮点数;与 C 浮点兼容 | +| `float64` | `f8 或 d` | 标准双精度浮点数;与 C 双精度和 Python `float`对象兼容 | +| `float128` | `f16 或 g` | 扩展精度浮点数 | +| `complex64`,`complex128`,`complex256` | `c8, c16, c32` | 分别由两个 32、64 或 128 个浮点数表示的复数 | +| `bool` | ? | 存储`True`和`False`值的布尔类型 | +| `object` | O | Python 对象类型;值可以是任何 Python 对象 | +| `string_` | S | 固定长度 ASCII 字符串类型(每个字符 1 字节);例如,要创建长度为 10 的字符串数据类型,请使用`'S10'` | +| `unicode_` | U | 固定长度 Unicode 类型(字节数平台特定);与`string_`(例如,`'U10'`)具有相同的规范语义 | + +注意 + +有*有符号*和*无符号*整数类型,许多读者可能不熟悉这个术语。*有符号*整数可以表示正整数和负整数,而*无符号*整数只能表示非零整数。例如,`int8`(有符号 8 位整数)可以表示从-128 到 127(包括)的整数,而`uint8`(无符号 8 位整数)可以表示 0 到 255。 + +您可以使用 ndarray 的`astype`方法显式地将数组从一种数据类型转换为另一种数据类型: + +```py +In [37]: arr = np.array([1, 2, 3, 4, 5]) + +In [38]: arr.dtype +Out[38]: dtype('int64') + +In [39]: float_arr = arr.astype(np.float64) + +In [40]: float_arr +Out[40]: array([1., 2., 3., 4., 5.]) + +In [41]: float_arr.dtype +Out[41]: dtype('float64') +``` + +在这个例子中,整数被转换为浮点数。如果我将一些浮点数转换为整数数据类型,小数部分将被截断: + +```py +In [42]: arr = np.array([3.7, -1.2, -2.6, 0.5, 12.9, 10.1]) + +In [43]: arr +Out[43]: array([ 3.7, -1.2, -2.6, 0.5, 12.9, 10.1]) + +In [44]: arr.astype(np.int32) +Out[44]: array([ 3, -1, -2, 0, 12, 10], dtype=int32) +``` + +如果您有一个表示数字的字符串数组,可以使用`astype`将它们转换为数值形式: + +```py +In [45]: numeric_strings = np.array(["1.25", "-9.6", "42"], dtype=np.string_) + +In [46]: numeric_strings.astype(float) +Out[46]: array([ 1.25, -9.6 , 42. ]) +``` + +注意 + +在使用`numpy.string_`类型时要小心,因为 NumPy 中的字符串数据是固定大小的,可能会在没有警告的情况下截断输入。pandas 对非数值数据具有更直观的开箱即用行为。 + +如果由于某种原因(例如无法将字符串转换为`float64`)而转换失败,将引发`ValueError`。以前,我有点懒,写了`float`而不是`np.float64`;NumPy 将 Python 类型别名为其自己的等效数据类型。 + +您还可以使用另一个数组的`dtype`属性: + +```py +In [47]: int_array = np.arange(10) + +In [48]: calibers = np.array([.22, .270, .357, .380, .44, .50], dtype=np.float64) + +In [49]: int_array.astype(calibers.dtype) +Out[49]: array([0., 1., 2., 3., 4., 5., 6., 7., 8., 9.]) +``` + +有简写类型代码字符串,您也可以使用它们来引用`dtype`: + +```py +In [50]: zeros_uint32 = np.zeros(8, dtype="u4") + +In [51]: zeros_uint32 +Out[51]: array([0, 0, 0, 0, 0, 0, 0, 0], dtype=uint32) +``` + +注意 + +调用`astype` *总是*会创建一个新数组(数据的副本),即使新数据类型与旧数据类型相同。 + +### NumPy 数组的算术运算 + +数组很重要,因为它们使您能够在不编写任何`for`循环的情况下对数据执行批量操作。NumPy 用户称之为*向量化*。任何等大小数组之间的算术运算都会逐元素应用该操作: + +```py +In [52]: arr = np.array([[1., 2., 3.], [4., 5., 6.]]) + +In [53]: arr +Out[53]: +array([[1., 2., 3.], + [4., 5., 6.]]) + +In [54]: arr * arr +Out[54]: +array([[ 1., 4., 9.], + [16., 25., 36.]]) + +In [55]: arr - arr +Out[55]: +array([[0., 0., 0.], + [0., 0., 0.]]) +``` + +标量的算术运算会将标量参数传播到数组中的每个元素: + +```py +In [56]: 1 / arr +Out[56]: +array([[1. , 0.5 , 0.3333], + [0.25 , 0.2 , 0.1667]]) + +In [57]: arr ** 2 +Out[57]: +array([[ 1., 4., 9.], + [16., 25., 36.]]) +``` + +相同大小的数组之间的比较会产生布尔数组: + +```py +In [58]: arr2 = np.array([[0., 4., 1.], [7., 2., 12.]]) + +In [59]: arr2 +Out[59]: +array([[ 0., 4., 1.], + [ 7., 2., 12.]]) + +In [60]: arr2 > arr +Out[60]: +array([[False, True, False], + [ True, False, True]]) +``` + +在不同大小的数组之间进行操作被称为*广播*,将在附录 A:高级 NumPy 中更详细地讨论。对广播的深入理解对本书的大部分内容并不是必要的。 + +### 基本索引和切片 + +NumPy 数组索引是一个深入的话题,因为有许多种方式可以选择数据的子集或单个元素。一维数组很简单;从表面上看,它们的行为类似于 Python 列表: + +```py +In [61]: arr = np.arange(10) + +In [62]: arr +Out[62]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + +In [63]: arr[5] +Out[63]: 5 + +In [64]: arr[5:8] +Out[64]: array([5, 6, 7]) + +In [65]: arr[5:8] = 12 + +In [66]: arr +Out[66]: array([ 0, 1, 2, 3, 4, 12, 12, 12, 8, 9]) +``` + +正如您所看到的,如果您将标量值分配给一个切片,如`arr[5:8] = 12`,该值将传播(或者*广播*)到整个选择。 + +注意 + +与 Python 内置列表的一个重要区别是,数组切片是原始数组的视图。这意味着数据没有被复制,对视图的任何修改都将反映在源数组中。 + +为了举例说明,我首先创建`arr`的一个切片: + +```py +In [67]: arr_slice = arr[5:8] + +In [68]: arr_slice +Out[68]: array([12, 12, 12]) +``` + +现在,当我在`arr_slice`中更改值时,这些变化会反映在原始数组`arr`中: + +```py +In [69]: arr_slice[1] = 12345 + +In [70]: arr +Out[70]: +array([ 0, 1, 2, 3, 4, 12, 12345, 12, 8, + 9]) +``` + +“裸”切片`[:]`将分配给数组中的所有值: + +```py +In [71]: arr_slice[:] = 64 + +In [72]: arr +Out[72]: array([ 0, 1, 2, 3, 4, 64, 64, 64, 8, 9]) +``` + +如果您是 NumPy 的新手,您可能会对此感到惊讶,特别是如果您已经使用过其他更积极复制数据的数组编程语言。由于 NumPy 被设计为能够处理非常大的数组,如果 NumPy 坚持始终复制数据,您可能会遇到性能和内存问题。 + +注意 + +如果您想要一个 ndarray 切片的副本而不是视图,您需要显式复制数组,例如`arr[5:8].copy()`。正如您将看到的,pandas 也是这样工作的。 + +对于更高维度的数组,您有更多的选择。在二维数组中,每个索引处的元素不再是标量,而是一维数组: + +```py +In [73]: arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + +In [74]: arr2d[2] +Out[74]: array([7, 8, 9]) +``` + +因此,可以递归访问单个元素。但这有点太麻烦了,所以您可以传递一个逗号分隔的索引列表来选择单个元素。因此,这些是等价的: + +```py +In [75]: arr2d[0][2] +Out[75]: 3 + +In [76]: arr2d[0, 2] +Out[76]: 3 +``` + +请参见图 4.1 以了解如何在二维数组上进行索引的说明。我发现将轴 0 视为数组的“行”而将轴 1 视为“列”是有帮助的。 + +![](img/a09620216e4172a26b3283ca34ce702c.png) + +图 4.1:索引 NumPy 数组中的元素 + +在多维数组中,如果省略后面的索引,返回的对象将是一个较低维度的 ndarray,由沿着更高维度的所有数据组成。因此,在 2×2×3 数组`arr3d`中: + +```py +In [77]: arr3d = np.array([[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]]) + +In [78]: arr3d +Out[78]: +array([[[ 1, 2, 3], + [ 4, 5, 6]], + [[ 7, 8, 9], + [10, 11, 12]]]) +``` + +`arr3d[0]`是一个 2×3 数组: + +```py +In [79]: arr3d[0] +Out[79]: +array([[1, 2, 3], + [4, 5, 6]]) +``` + +标量值和数组都可以分配给`arr3d[0]`: + +```py +In [80]: old_values = arr3d[0].copy() + +In [81]: arr3d[0] = 42 + +In [82]: arr3d +Out[82]: +array([[[42, 42, 42], + [42, 42, 42]], + [[ 7, 8, 9], + [10, 11, 12]]]) + +In [83]: arr3d[0] = old_values + +In [84]: arr3d +Out[84]: +array([[[ 1, 2, 3], + [ 4, 5, 6]], + [[ 7, 8, 9], + [10, 11, 12]]]) +``` + +类似地,`arr3d[1, 0]`会给您所有索引以`(1, 0)`开头的值,形成一个一维数组: + +```py +In [85]: arr3d[1, 0] +Out[85]: array([7, 8, 9]) +``` + +这个表达式与我们分两步索引的方式相同: + +```py +In [86]: x = arr3d[1] + +In [87]: x +Out[87]: +array([[ 7, 8, 9], + [10, 11, 12]]) + +In [88]: x[0] +Out[88]: array([7, 8, 9]) +``` + +请注意,在所有这些选择数组的子部分的情况下,返回的数组都是视图。 + +注意 + +这种用于 NumPy 数组的多维索引语法不适用于常规的 Python 对象,例如列表的列表。 + +#### 使用切片进行索引 + +像 Python 列表这样的一维对象一样,ndarrays 可以使用熟悉的语法进行切片: + +```py +In [89]: arr +Out[89]: array([ 0, 1, 2, 3, 4, 64, 64, 64, 8, 9]) + +In [90]: arr[1:6] +Out[90]: array([ 1, 2, 3, 4, 64]) +``` + +考虑之前的二维数组`arr2d`。对该数组进行切片有点不同: + +```py +In [91]: arr2d +Out[91]: +array([[1, 2, 3], + [4, 5, 6], + [7, 8, 9]]) + +In [92]: arr2d[:2] +Out[92]: +array([[1, 2, 3], + [4, 5, 6]]) +``` + +正如您所看到的,它已经沿着轴 0 切片,即第一个轴。因此,切片选择沿着一个轴的一系列元素。阅读表达式`arr2d[:2]`为“选择`arr2d`的前两行”可能会有所帮助。 + +您可以像传递多个索引一样传递多个切片: + +```py +In [93]: arr2d[:2, 1:] +Out[93]: +array([[2, 3], + [5, 6]]) +``` + +像这样切片时,您总是获得相同维数的数组视图。通过混合整数索引和切片,您可以获得较低维度的切片。 + +例如,我可以选择第二行,但只选择前两列,如下所示: + +```py +In [94]: lower_dim_slice = arr2d[1, :2] +``` + +在这里,虽然`arr2d`是二维的,`lower_dim_slice`是一维的,其形状是一个带有一个轴大小的元组: + +```py +In [95]: lower_dim_slice.shape +Out[95]: (2,) +``` + +同样,我可以选择第三列,但只选择前两行,如下所示: + +```py +In [96]: arr2d[:2, 2] +Out[96]: array([3, 6]) +``` + +请参见图 4.2 进行说明。请注意,单独的冒号表示取整个轴,因此您可以通过以下方式仅切片更高维度的轴: + +```py +In [97]: arr2d[:, :1] +Out[97]: +array([[1], + [4], + [7]]) +``` + +当然,对切片表达式的分配会分配给整个选择: + +```py +In [98]: arr2d[:2, 1:] = 0 + +In [99]: arr2d +Out[99]: +array([[1, 0, 0], + [4, 0, 0], + [7, 8, 9]]) +``` + +![](img/ae89dd22b1516b02323b63614b75f260.png) + +图 4.2:二维数组切片 + +### 布尔索引 + +让我们考虑一个例子,其中我们有一些数据在一个数组中,并且有一个包含重复名称的数组: + +```py +In [100]: names = np.array(["Bob", "Joe", "Will", "Bob", "Will", "Joe", "Joe"]) + +In [101]: data = np.array([[4, 7], [0, 2], [-5, 6], [0, 0], [1, 2], + .....: [-12, -4], [3, 4]]) + +In [102]: names +Out[102]: array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'], dtype=', >=, <, <=, ==, !=`) | +| `logical_and` | 计算逻辑与(`&`)的逐个元素真值 | +| `logical_or` | 计算逻辑或(` | `)的逐个元素真值 | +| `logical_xor` | 计算逻辑异或(`^`)的逐个元素真值 | + +## 4.4 数组导向编程与数组 + +使用 NumPy 数组使您能够将许多种类的数据处理任务表达为简洁的数组表达式,否则可能需要编写循环。用数组表达式替换显式循环的这种做法被一些人称为*向量化*。一般来说,向量化的数组操作通常比它们纯 Python 等效的要快得多,在任何类型的数值计算中影响最大。稍后,在附录 A:高级 NumPy 中,我将解释*广播*,这是一种用于向量化计算的强大方法。 + +举个简单的例子,假设我们希望在一组常规值的网格上评估函数`sqrt(x² + y²)`。`numpy.meshgrid`函数接受两个一维数组,并产生两个对应于两个数组中所有`(x, y)`对的二维矩阵: + +```py +In [169]: points = np.arange(-5, 5, 0.01) # 100 equally spaced points + +In [170]: xs, ys = np.meshgrid(points, points) + +In [171]: ys +Out[171]: +array([[-5. , -5. , -5. , ..., -5. , -5. , -5. ], + [-4.99, -4.99, -4.99, ..., -4.99, -4.99, -4.99], + [-4.98, -4.98, -4.98, ..., -4.98, -4.98, -4.98], + ..., + [ 4.97, 4.97, 4.97, ..., 4.97, 4.97, 4.97], + [ 4.98, 4.98, 4.98, ..., 4.98, 4.98, 4.98], + [ 4.99, 4.99, 4.99, ..., 4.99, 4.99, 4.99]]) +``` + +现在,评估函数只是写出您将用两个点写出的相同表达式的问题: + +```py +In [172]: z = np.sqrt(xs ** 2 + ys ** 2) + +In [173]: z +Out[173]: +array([[7.0711, 7.064 , 7.0569, ..., 7.0499, 7.0569, 7.064 ], + [7.064 , 7.0569, 7.0499, ..., 7.0428, 7.0499, 7.0569], + [7.0569, 7.0499, 7.0428, ..., 7.0357, 7.0428, 7.0499], + ..., + [7.0499, 7.0428, 7.0357, ..., 7.0286, 7.0357, 7.0428], + [7.0569, 7.0499, 7.0428, ..., 7.0357, 7.0428, 7.0499], + [7.064 , 7.0569, 7.0499, ..., 7.0428, 7.0499, 7.0569]]) +``` + +作为第九章:绘图和可视化的预览,我使用 matplotlib 来创建这个二维数组的可视化: + +```py +In [174]: import matplotlib.pyplot as plt + +In [175]: plt.imshow(z, cmap=plt.cm.gray, extent=[-5, 5, -5, 5]) +Out[175]: + +In [176]: plt.colorbar() +Out[176]: + +In [177]: plt.title("Image plot of $\sqrt{x² + y²}$ for a grid of values") +Out[177]: Text(0.5, 1.0, 'Image plot of $\\sqrt{x² + y²}$ for a grid of values' +) +``` + +在在网格上评估函数的绘图中,我使用了 matplotlib 函数`imshow`来从函数值的二维数组创建图像图。 + +![](img/c901850a416d7a0f42bc07ffa0092543.png) + +图 4.3:在网格上评估函数的绘图 + +如果您在 IPython 中工作,可以通过执行`plt.close("all")`关闭所有打开的绘图窗口: + +```py +In [179]: plt.close("all") +``` + +注意 + +术语*矢量化*用于描述其他计算机科学概念,但在本书中,我使用它来描述对整个数据数组进行操作,而不是逐个值使用 Python 的`for`循环。 + +### 将条件逻辑表达为数组操作 + +`numpy.where`函数是三元表达式`x if condition else y`的矢量化版本。假设我们有一个布尔数组和两个值数组: + +```py +In [180]: xarr = np.array([1.1, 1.2, 1.3, 1.4, 1.5]) + +In [181]: yarr = np.array([2.1, 2.2, 2.3, 2.4, 2.5]) + +In [182]: cond = np.array([True, False, True, True, False]) +``` + +假设我们想要从`cond`中对应的值为`True`时从`xarr`中取一个值,否则从`yarr`中取一个值。一个做到这一点的列表推导可能如下所示: + +```py +In [183]: result = [(x if c else y) + .....: for x, y, c in zip(xarr, yarr, cond)] + +In [184]: result +Out[184]: [1.1, 2.2, 1.3, 1.4, 2.5] +``` + +这有多个问题。首先,对于大数组来说速度不会很快(因为所有工作都是在解释的 Python 代码中完成的)。其次,它不适用于多维数组。使用`numpy.where`可以通过单个函数调用来实现这一点: + +```py +In [185]: result = np.where(cond, xarr, yarr) + +In [186]: result +Out[186]: array([1.1, 2.2, 1.3, 1.4, 2.5]) +``` + +`numpy.where`的第二个和第三个参数不需要是数组;它们中的一个或两个可以是标量。在数据分析中,`where`的典型用法是根据另一个数组生成一个新的值数组。假设你有一个随机生成数据的矩阵,并且你想用 2 替换所有正值和用-2 替换所有负值。这可以通过`numpy.where`来实现: + +```py +In [187]: arr = rng.standard_normal((4, 4)) + +In [188]: arr +Out[188]: +array([[ 2.6182, 0.7774, 0.8286, -0.959 ], + [-1.2094, -1.4123, 0.5415, 0.7519], + [-0.6588, -1.2287, 0.2576, 0.3129], + [-0.1308, 1.27 , -0.093 , -0.0662]]) + +In [189]: arr > 0 +Out[189]: +array([[ True, True, True, False], + [False, False, True, True], + [False, False, True, True], + [False, True, False, False]]) + +In [190]: np.where(arr > 0, 2, -2) +Out[190]: +array([[ 2, 2, 2, -2], + [-2, -2, 2, 2], + [-2, -2, 2, 2], + [-2, 2, -2, -2]]) +``` + +在使用`numpy.where`时,可以将标量和数组组合在一起。例如,我可以用常数 2 替换`arr`中的所有正值,如下所示: + +```py +In [191]: np.where(arr > 0, 2, arr) # set only positive values to 2 +Out[191]: +array([[ 2. , 2. , 2. , -0.959 ], + [-1.2094, -1.4123, 2. , 2. ], + [-0.6588, -1.2287, 2. , 2. ], + [-0.1308, 2. , -0.093 , -0.0662]]) +``` + +### 数学和统计方法 + +一组数学函数,用于计算整个数组或沿轴的数据的统计信息,作为数组类的方法可访问。您可以通过调用数组实例方法或使用顶级 NumPy 函数来使用聚合(有时称为*缩减*)如`sum`、`mean`和`std`(标准差)。当您使用 NumPy 函数,如`numpy.sum`时,您必须将要聚合的数组作为第一个参数传递。 + +这里我生成一些正态分布的随机数据并计算一些聚合统计数据: + +```py +In [192]: arr = rng.standard_normal((5, 4)) + +In [193]: arr +Out[193]: +array([[-1.1082, 0.136 , 1.3471, 0.0611], + [ 0.0709, 0.4337, 0.2775, 0.5303], + [ 0.5367, 0.6184, -0.795 , 0.3 ], + [-1.6027, 0.2668, -1.2616, -0.0713], + [ 0.474 , -0.4149, 0.0977, -1.6404]]) + +In [194]: arr.mean() +Out[194]: -0.08719744457434529 + +In [195]: np.mean(arr) +Out[195]: -0.08719744457434529 + +In [196]: arr.sum() +Out[196]: -1.743948891486906 +``` + +像`mean`和`sum`这样的函数接受一个可选的`axis`参数,该参数在给定轴上计算统计量,结果是一个维数少一的数组: + +```py +In [197]: arr.mean(axis=1) +Out[197]: array([ 0.109 , 0.3281, 0.165 , -0.6672, -0.3709]) + +In [198]: arr.sum(axis=0) +Out[198]: array([-1.6292, 1.0399, -0.3344, -0.8203]) +``` + +这里,`arr.mean(axis=1)`表示“计算沿着列的平均值”,而`arr.sum(axis=0)`表示“计算沿着行的总和”。 + +像`cumsum`和`cumprod`这样的其他方法不进行聚合,而是产生中间结果的数组: + +```py +In [199]: arr = np.array([0, 1, 2, 3, 4, 5, 6, 7]) + +In [200]: arr.cumsum() +Out[200]: array([ 0, 1, 3, 6, 10, 15, 21, 28]) +``` + +在多维数组中,像`cumsum`这样的累积函数返回一个相同大小的数组,但是根据每个较低维度切片沿着指定轴计算部分累积: + +```py +In [201]: arr = np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]]) + +In [202]: arr +Out[202]: +array([[0, 1, 2], + [3, 4, 5], + [6, 7, 8]]) +``` + +表达式`arr.cumsum(axis=0)`计算沿着行的累积和,而`arr.cumsum(axis=1)`计算沿着列的和: + +```py +In [203]: arr.cumsum(axis=0) +Out[203]: +array([[ 0, 1, 2], + [ 3, 5, 7], + [ 9, 12, 15]]) + +In [204]: arr.cumsum(axis=1) +Out[204]: +array([[ 0, 1, 3], + [ 3, 7, 12], + [ 6, 13, 21]]) +``` + +查看表 4.6 以获取完整列表。我们将在后面的章节中看到这些方法的许多示例。 + +表 4.6:基本数组统计方法 + +| 方法 | 描述 | +| --- | --- | +| `sum` | 数组或沿轴的所有元素的总和;长度为零的数组的总和为 0 | +| `mean` | 算术平均值;对于长度为零的数组无效(返回`NaN`) | +| `std, var` | 分别是标准差和方差 | +| `min, max` | 最小值和最大值 | +| `argmin, argmax` | 分别是最小和最大元素的索引 | +| `cumsum` | 从 0 开始的元素的累积和 | +| `cumprod` | 从 1 开始的元素的累积乘积 | + +### 布尔数组的方法 + +在前面的方法中,布尔值被强制转换为 1(`True`)和 0(`False`)。因此,`sum`经常被用作计算布尔数组中`True`值的计数的手段: + +```py +In [205]: arr = rng.standard_normal(100) + +In [206]: (arr > 0).sum() # Number of positive values +Out[206]: 48 + +In [207]: (arr <= 0).sum() # Number of non-positive values +Out[207]: 52 +``` + +这里表达式`(arr > 0).sum()`中的括号是必要的,以便能够在`arr > 0`的临时结果上调用`sum()`。 + +另外两个方法,`any`和`all`,特别适用于布尔数组。`any`测试数组中是否有一个或多个值为`True`,而`all`检查是否每个值都为`True`: + +```py +In [208]: bools = np.array([False, False, True, False]) + +In [209]: bools.any() +Out[209]: True + +In [210]: bools.all() +Out[210]: False +``` + +这些方法也适用于非布尔数组,其中非零元素被视为`True`。 + +### 排序 + +与 Python 内置的列表类型类似,NumPy 数组可以使用`sort`方法原地排序: + +```py +In [211]: arr = rng.standard_normal(6) + +In [212]: arr +Out[212]: array([ 0.0773, -0.6839, -0.7208, 1.1206, -0.0548, -0.0824]) + +In [213]: arr.sort() + +In [214]: arr +Out[214]: array([-0.7208, -0.6839, -0.0824, -0.0548, 0.0773, 1.1206]) +``` + +您可以通过将轴编号传递给`sort`方法,在多维数组中对每个一维部分的值沿着轴进行原地排序。在这个例子数据中: + +```py +In [215]: arr = rng.standard_normal((5, 3)) + +In [216]: arr +Out[216]: +array([[ 0.936 , 1.2385, 1.2728], + [ 0.4059, -0.0503, 0.2893], + [ 0.1793, 1.3975, 0.292 ], + [ 0.6384, -0.0279, 1.3711], + [-2.0528, 0.3805, 0.7554]]) +``` + +`arr.sort(axis=0)`对每列内的值进行排序,而`arr.sort(axis=1)`对每行进行排序: + +```py +In [217]: arr.sort(axis=0) + +In [218]: arr +Out[218]: +array([[-2.0528, -0.0503, 0.2893], + [ 0.1793, -0.0279, 0.292 ], + [ 0.4059, 0.3805, 0.7554], + [ 0.6384, 1.2385, 1.2728], + [ 0.936 , 1.3975, 1.3711]]) + +In [219]: arr.sort(axis=1) + +In [220]: arr +Out[220]: +array([[-2.0528, -0.0503, 0.2893], + [-0.0279, 0.1793, 0.292 ], + [ 0.3805, 0.4059, 0.7554], + [ 0.6384, 1.2385, 1.2728], + [ 0.936 , 1.3711, 1.3975]]) +``` + +顶层方法`numpy.sort`返回一个数组的排序副本(类似于 Python 内置函数`sorted`),而不是在原地修改数组。例如: + +```py +In [221]: arr2 = np.array([5, -10, 7, 1, 0, -3]) + +In [222]: sorted_arr2 = np.sort(arr2) + +In [223]: sorted_arr2 +Out[223]: array([-10, -3, 0, 1, 5, 7]) +``` + +有关使用 NumPy 的排序方法的更多详细信息,以及更高级的技术,如间接排序,请参见附录 A:高级 NumPy。还可以在 pandas 中找到与排序相关的其他数据操作(例如,按一个或多个列对数据表进行排序)。 + +### 唯一值和其他集合逻辑 + +NumPy 具有一些用于一维 ndarrays 的基本集合操作。一个常用的操作是`numpy.unique`,它返回数组中排序的唯一值: + +```py +In [224]: names = np.array(["Bob", "Will", "Joe", "Bob", "Will", "Joe", "Joe"]) + +In [225]: np.unique(names) +Out[225]: array(['Bob', 'Joe', 'Will'], dtype='= 10`给出一个布尔数组,指示漫步已经达到或超过 10,但我们想要第一个 10 或-10 的索引。事实证明,我们可以使用`argmax`来计算这个,它返回布尔数组中最大值的第一个索引(`True`是最大值): + +```py +In [263]: (np.abs(walk) >= 10).argmax() +Out[263]: 155 +``` + +请注意,在这里使用`argmax`并不总是高效的,因为它总是对数组进行完整扫描。在这种特殊情况下,一旦观察到`True`,我们就知道它是最大值。 + +### 一次模拟多个随机漫步 + +如果你的目标是模拟许多随机漫步,比如说五千次,你可以通过对前面的代码进行微小修改来生成所有的随机漫步。如果传递一个 2 元组,`numpy.random`函数将生成一个二维数组的抽样,我们可以为每一行计算累积和,以一次性计算所有五千次随机漫步: + +```py +In [264]: nwalks = 5000 + +In [265]: nsteps = 1000 + +In [266]: draws = rng.integers(0, 2, size=(nwalks, nsteps)) # 0 or 1 + +In [267]: steps = np.where(draws > 0, 1, -1) + +In [268]: walks = steps.cumsum(axis=1) + +In [269]: walks +Out[269]: +array([[ 1, 2, 3, ..., 22, 23, 22], + [ 1, 0, -1, ..., -50, -49, -48], + [ 1, 2, 3, ..., 50, 49, 48], + ..., + [ -1, -2, -1, ..., -10, -9, -10], + [ -1, -2, -3, ..., 8, 9, 8], + [ -1, 0, 1, ..., -4, -3, -2]]) +``` + +现在,我们可以计算所有漫步中获得的最大值和最小值: + +```py +In [270]: walks.max() +Out[270]: 114 + +In [271]: walks.min() +Out[271]: -120 +``` + +在这些漫步中,让我们计算到达 30 或-30 的最小穿越时间。这有点棘手,因为并非所有的 5000 次都达到 30。我们可以使用`any`方法来检查: + +```py +In [272]: hits30 = (np.abs(walks) >= 30).any(axis=1) + +In [273]: hits30 +Out[273]: array([False, True, True, ..., True, False, True]) + +In [274]: hits30.sum() # Number that hit 30 or -30 +Out[274]: 3395 +``` + +我们可以使用这个布尔数组来选择实际穿越绝对值 30 水平的`walks`的行,并在轴 1 上调用`argmax`来获取穿越时间: + +```py +In [275]: crossing_times = (np.abs(walks[hits30]) >= 30).argmax(axis=1) + +In [276]: crossing_times +Out[276]: array([201, 491, 283, ..., 219, 259, 541]) +``` + +最后,我们计算平均最小穿越时间: + +```py +In [277]: crossing_times.mean() +Out[277]: 500.5699558173785 +``` + +随意尝试使用与等大小硬币翻转不同的步骤分布。你只需要使用不同的随机生成器方法,比如`standard_normal`来生成具有一定均值和标准差的正态分布步数: + +```py +In [278]: draws = 0.25 * rng.standard_normal((nwalks, nsteps)) +``` + +注意 + +请记住,这种矢量化方法需要创建一个具有`nwalks * nsteps`元素的数组,这可能会在大型模拟中使用大量内存。如果内存更受限制,则需要采用不同的方法。 + +## 4.8 结论 + +尽管本书的大部分内容将集中在使用 pandas 构建数据整理技能上,我们将继续以类似的基于数组的风格工作。在附录 A:高级 NumPy 中,我们将深入探讨 NumPy 的特性,帮助您进一步发展数组计算技能。 diff --git a/translations/cn/pyda3e_08.md b/translations/cn/pyda3e_08.md new file mode 100644 index 000000000..ae3a81230 --- /dev/null +++ b/translations/cn/pyda3e_08.md @@ -0,0 +1,2602 @@ +# 五、使用 pandas 入门 + +> 原文:[`wesmckinney.com/book/pandas-basics`](https://wesmckinney.com/book/pandas-basics) +> +> 译者:[飞龙](https://github.com/wizardforcel) +> +> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) + +> 此开放访问网络版本的《Python 数据分析第三版》现已作为[印刷版和数字版](https://amzn.to/3DyLaJc)的伴侣提供。如果您发现任何勘误,请[在此处报告](https://oreilly.com/catalog/0636920519829/errata)。请注意,由 Quarto 生成的本站点的某些方面与 O'Reilly 的印刷版和电子书版本的格式不同。 +> +> 如果您发现本书的在线版本有用,请考虑[订购纸质版](https://amzn.to/3DyLaJc)或[无 DRM 的电子书](https://www.ebooks.com/en-us/book/210644288/python-for-data-analysis/wes-mckinney/?affId=WES398681F)以支持作者。本网站的内容不得复制或再生产。代码示例采用 MIT 许可,可在 GitHub 或 Gitee 上找到。 + +pandas 将是本书剩余部分中的一个主要工具。它包含了专为在 Python 中快速方便地进行数据清洗和分析而设计的数据结构和数据操作工具。pandas 经常与数值计算工具(如 NumPy 和 SciPy)、分析库(如 statsmodels 和 scikit-learn)以及数据可视化库(如 matplotlib)一起使用。pandas 采用了 NumPy 的很多习惯用法,特别是基于数组的计算和对数据处理的偏好,而不使用`for`循环。 + +虽然 pandas 采用了许多来自 NumPy 的编码习惯,但最大的区别在于 pandas 是为处理表格或异构数据而设计的。相比之下,NumPy 更适合处理同质类型的数值数组数据。 + +自 2010 年成为开源项目以来,pandas 已经发展成一个相当庞大的库,适用于广泛的实际用例。开发者社区已经发展到超过 2500 名不同的贡献者,他们在解决日常数据问题时一直在帮助构建这个项目。充满活力的 pandas 开发者和用户社区是其成功的关键部分。 + +注意 + +很多人不知道我自 2013 年以来并没有积极参与日常 pandas 的开发;从那时起,它一直是一个完全由社区管理的项目。请务必向核心开发人员和所有贡献者传达感谢他们的辛勤工作! + +在本书的剩余部分中,我使用以下的 NumPy 和 pandas 的导入约定: + +```py +In [1]: import numpy as np + +In [2]: import pandas as pd +``` + +因此,每当在代码中看到`pd.`时,它指的是 pandas。您可能也会发现将 Series 和 DataFrame 导入到本地命名空间中更容易,因为它们经常被使用: + +```py +In [3]: from pandas import Series, DataFrame +``` + +## 5.1 pandas 数据结构简介 + +要开始使用 pandas,您需要熟悉其两个主要数据结构:*Series*和*DataFrame*。虽然它们并非适用于每个问题的通用解决方案,但它们为各种数据任务提供了坚实的基础。 + +### Series + +Series 是一个一维数组样对象,包含一系列值(与 NumPy 类型相似的类型)和一个关联的数据标签数组,称为*索引*。最简单的 Series 是仅由数据数组形成的: + +```py +In [14]: obj = pd.Series([4, 7, -5, 3]) + +In [15]: obj +Out[15]: +0 4 +1 7 +2 -5 +3 3 +dtype: int64 +``` + +Series 的交互式显示的字符串表示在左侧显示索引,右侧显示值。由于我们没有为数据指定索引,因此会创建一个默认索引,由整数`0`到`N-1`(其中`N`是数据的长度)组成。您可以通过其`array`和`index`属性分别获取 Series 的数组表示和索引对象: + +```py +In [16]: obj.array +Out[16]: + +[4, 7, -5, 3] +Length: 4, dtype: int64 + +In [17]: obj.index +Out[17]: RangeIndex(start=0, stop=4, step=1) +``` + +`.array`属性的结果是一个`PandasArray`,通常包装了一个 NumPy 数组,但也可以包含特殊的扩展数组类型,这将在 Ch 7.3:扩展数据类型中更详细讨论。 + +通常,您会希望创建一个带有标识每个数据点的索引的 Series: + +```py +In [18]: obj2 = pd.Series([4, 7, -5, 3], index=["d", "b", "a", "c"]) + +In [19]: obj2 +Out[19]: +d 4 +b 7 +a -5 +c 3 +dtype: int64 + +In [20]: obj2.index +Out[20]: Index(['d', 'b', 'a', 'c'], dtype='object') +``` + +与 NumPy 数组相比,当选择单个值或一组值时,可以在索引中使用标签: + +```py +In [21]: obj2["a"] +Out[21]: -5 + +In [22]: obj2["d"] = 6 + +In [23]: obj2[["c", "a", "d"]] +Out[23]: +c 3 +a -5 +d 6 +dtype: int64 +``` + +这里`["c", "a", "d"]`被解释为索引列表,即使它包含字符串而不是整数。 + +使用 NumPy 函数或类似 NumPy 的操作,例如使用布尔数组进行过滤、标量乘法或应用数学函数,将保留索引值链接: + +```py +In [24]: obj2[obj2 > 0] +Out[24]: +d 6 +b 7 +c 3 +dtype: int64 + +In [25]: obj2 * 2 +Out[25]: +d 12 +b 14 +a -10 +c 6 +dtype: int64 + +In [26]: import numpy as np + +In [27]: np.exp(obj2) +Out[27]: +d 403.428793 +b 1096.633158 +a 0.006738 +c 20.085537 +dtype: float64 +``` + +将 Series 视为固定长度的有序字典的另一种方式,因为它是索引值到数据值的映射。它可以在许多上下文中使用,您可能会使用字典: + +```py +In [28]: "b" in obj2 +Out[28]: True + +In [29]: "e" in obj2 +Out[29]: False +``` + +如果您的数据包含在 Python 字典中,可以通过传递字典来创建一个 Series: + +```py +In [30]: sdata = {"Ohio": 35000, "Texas": 71000, "Oregon": 16000, "Utah": 5000} + +In [31]: obj3 = pd.Series(sdata) + +In [32]: obj3 +Out[32]: +Ohio 35000 +Texas 71000 +Oregon 16000 +Utah 5000 +dtype: int64 +``` + +Series 可以使用其`to_dict`方法转换回字典: + +```py +In [33]: obj3.to_dict() +Out[33]: {'Ohio': 35000, 'Texas': 71000, 'Oregon': 16000, 'Utah': 5000} +``` + +当您只传递一个字典时,生成的 Series 中的索引将遵循字典的`keys`方法的键的顺序,这取决于键插入顺序。您可以通过传递一个索引,其中包含字典键的顺序,以便它们出现在生成的 Series 中的顺序来覆盖这一点: + +```py +In [34]: states = ["California", "Ohio", "Oregon", "Texas"] + +In [35]: obj4 = pd.Series(sdata, index=states) + +In [36]: obj4 +Out[36]: +California NaN +Ohio 35000.0 +Oregon 16000.0 +Texas 71000.0 +dtype: float64 +``` + +在这里,`sdata`中找到的三个值被放置在适当的位置,但由于没有找到`"California"`的值,它显示为`NaN`(不是一个数字),在 pandas 中被视为标记缺失或*NA*值。由于`states`中没有包含`"Utah"`,因此它被排除在结果对象之外。 + +我将使用术语“missing”、“NA”或“null”来交替引用缺失数据。应该使用 pandas 中的`isna`和`notna`函数来检测缺失数据: + +```py +In [37]: pd.isna(obj4) +Out[37]: +California True +Ohio False +Oregon False +Texas False +dtype: bool + +In [38]: pd.notna(obj4) +Out[38]: +California False +Ohio True +Oregon True +Texas True +dtype: bool +``` + +Series 还具有这些作为实例方法: + +```py +In [39]: obj4.isna() +Out[39]: +California True +Ohio False +Oregon False +Texas False +dtype: bool +``` + +我将在第七章:数据清洗和准备中更详细地讨论处理缺失数据的工作。 + +对于许多应用程序来说,Series 的一个有用特性是它在算术运算中自动按索引标签对齐: + +```py +In [40]: obj3 +Out[40]: +Ohio 35000 +Texas 71000 +Oregon 16000 +Utah 5000 +dtype: int64 + +In [41]: obj4 +Out[41]: +California NaN +Ohio 35000.0 +Oregon 16000.0 +Texas 71000.0 +dtype: float64 + +In [42]: obj3 + obj4 +Out[42]: +California NaN +Ohio 70000.0 +Oregon 32000.0 +Texas 142000.0 +Utah NaN +dtype: float64 +``` + +数据对齐功能将在后面更详细地讨论。如果您有数据库经验,可以将其视为类似于连接操作。 + +Series 对象本身和其索引都有一个`name`属性,它与 pandas 功能的其他区域集成: + +```py +In [43]: obj4.name = "population" + +In [44]: obj4.index.name = "state" + +In [45]: obj4 +Out[45]: +state +California NaN +Ohio 35000.0 +Oregon 16000.0 +Texas 71000.0 +Name: population, dtype: float64 +``` + +Series 的索引可以通过赋值来直接更改: + +```py +In [46]: obj +Out[46]: +0 4 +1 7 +2 -5 +3 3 +dtype: int64 + +In [47]: obj.index = ["Bob", "Steve", "Jeff", "Ryan"] + +In [48]: obj +Out[48]: +Bob 4 +Steve 7 +Jeff -5 +Ryan 3 +dtype: int64 +``` + +### DataFrame + +DataFrame 表示数据的矩形表,并包含一个有序的、命名的列集合,每个列可以是不同的值类型(数值、字符串、布尔值等)。DataFrame 既有行索引又有列索引;它可以被视为共享相同索引的一系列 Series 的字典。 + +注意 + +虽然 DataFrame 在物理上是二维的,但您可以使用它来以分层索引的方式表示更高维度的数据,这是我们将在第八章:数据整理:连接、合并和重塑中讨论的一个主题,并且是 pandas 中一些更高级数据处理功能的一个组成部分。 + +有许多构建 DataFrame 的方法,尽管其中最常见的一种是从等长列表或 NumPy 数组的字典中构建: + +```py +data = {"state": ["Ohio", "Ohio", "Ohio", "Nevada", "Nevada", "Nevada"], + "year": [2000, 2001, 2002, 2001, 2002, 2003], + "pop": [1.5, 1.7, 3.6, 2.4, 2.9, 3.2]} +frame = pd.DataFrame(data) +``` + +生成的 DataFrame 将自动分配其索引,与 Series 一样,并且列根据`data`中键的顺序放置(取决于字典中的插入顺序): + +```py +In [50]: frame +Out[50]: + state year pop +0 Ohio 2000 1.5 +1 Ohio 2001 1.7 +2 Ohio 2002 3.6 +3 Nevada 2001 2.4 +4 Nevada 2002 2.9 +5 Nevada 2003 3.2 +``` + +注意 + +如果您正在使用 Jupyter 笔记本,pandas DataFrame 对象将显示为更适合浏览器的 HTML 表格。请参见图 5.1 作为示例。 + +![](img/80bf6ca7e96e78f9839a73591d77f472.png) + +图 5.1:Jupyter 中 pandas DataFrame 对象的外观 + +对于大型 DataFrame,`head`方法仅选择前五行: + +```py +In [51]: frame.head() +Out[51]: + state year pop +0 Ohio 2000 1.5 +1 Ohio 2001 1.7 +2 Ohio 2002 3.6 +3 Nevada 2001 2.4 +4 Nevada 2002 2.9 +``` + +类似地,`tail`返回最后五行: + +```py +In [52]: frame.tail() +Out[52]: + state year pop +1 Ohio 2001 1.7 +2 Ohio 2002 3.6 +3 Nevada 2001 2.4 +4 Nevada 2002 2.9 +5 Nevada 2003 3.2 +``` + +如果指定一系列列,DataFrame 的列将按照该顺序排列: + +```py +In [53]: pd.DataFrame(data, columns=["year", "state", "pop"]) +Out[53]: + year state pop +0 2000 Ohio 1.5 +1 2001 Ohio 1.7 +2 2002 Ohio 3.6 +3 2001 Nevada 2.4 +4 2002 Nevada 2.9 +5 2003 Nevada 3.2 +``` + +如果传递一个字典中不包含的列,它将以缺失值的形式出现在结果中: + +```py +In [54]: frame2 = pd.DataFrame(data, columns=["year", "state", "pop", "debt"]) + +In [55]: frame2 +Out[55]: + year state pop debt +0 2000 Ohio 1.5 NaN +1 2001 Ohio 1.7 NaN +2 2002 Ohio 3.6 NaN +3 2001 Nevada 2.4 NaN +4 2002 Nevada 2.9 NaN +5 2003 Nevada 3.2 NaN + +In [56]: frame2.columns +Out[56]: Index(['year', 'state', 'pop', 'debt'], dtype='object') +``` + +DataFrame 中的列可以通过类似字典的表示法或使用点属性表示法检索为 Series: + +```py +In [57]: frame2["state"] +Out[57]: +0 Ohio +1 Ohio +2 Ohio +3 Nevada +4 Nevada +5 Nevada +Name: state, dtype: object + +In [58]: frame2.year +Out[58]: +0 2000 +1 2001 +2 2002 +3 2001 +4 2002 +5 2003 +Name: year, dtype: int64 +``` + +注意 + +提供类似属性访问(例如,`frame2.year`)和 IPython 中列名称的制表符补全作为便利。 + +`frame2[column]`适用于任何列名,但只有当列名是有效的 Python 变量名且不与 DataFrame 中的任何方法名冲突时,`frame2.column`才适用。例如,如果列名包含空格或下划线以外的其他符号,则无法使用点属性方法访问。 + +请注意,返回的 Series 具有与 DataFrame 相同的索引,并且它们的`name`属性已经适当设置。 + +行也可以通过特殊的`iloc`和`loc`属性按位置或名称检索(稍后在使用 loc 和 iloc 在 DataFrame 上进行选择中详细介绍): + +```py +In [59]: frame2.loc[1] +Out[59]: +year 2001 +state Ohio +pop 1.7 +debt NaN +Name: 1, dtype: object + +In [60]: frame2.iloc[2] +Out[60]: +year 2002 +state Ohio +pop 3.6 +debt NaN +Name: 2, dtype: object +``` + +列可以通过赋值进行修改。例如,可以为空的`debt`列分配一个标量值或一个值数组: + +```py +In [61]: frame2["debt"] = 16.5 + +In [62]: frame2 +Out[62]: + year state pop debt +0 2000 Ohio 1.5 16.5 +1 2001 Ohio 1.7 16.5 +2 2002 Ohio 3.6 16.5 +3 2001 Nevada 2.4 16.5 +4 2002 Nevada 2.9 16.5 +5 2003 Nevada 3.2 16.5 + +In [63]: frame2["debt"] = np.arange(6.) + +In [64]: frame2 +Out[64]: + year state pop debt +0 2000 Ohio 1.5 0.0 +1 2001 Ohio 1.7 1.0 +2 2002 Ohio 3.6 2.0 +3 2001 Nevada 2.4 3.0 +4 2002 Nevada 2.9 4.0 +5 2003 Nevada 3.2 5.0 +``` + +当将列表或数组分配给列时,值的长度必须与 DataFrame 的长度相匹配。如果分配一个 Series,其标签将被重新对齐到 DataFrame 的索引,插入任何不存在的索引值的缺失值: + +```py +In [65]: val = pd.Series([-1.2, -1.5, -1.7], index=[2, 4, 5]) + +In [66]: frame2["debt"] = val + +In [67]: frame2 +Out[67]: + year state pop debt +0 2000 Ohio 1.5 NaN +1 2001 Ohio 1.7 NaN +2 2002 Ohio 3.6 -1.2 +3 2001 Nevada 2.4 NaN +4 2002 Nevada 2.9 -1.5 +5 2003 Nevada 3.2 -1.7 +``` + +分配一个不存在的列将创建一个新列。 + +`del`关键字将像字典一样删除列。例如,首先添加一个新列,其中布尔值等于`"Ohio"`的`state`列: + +```py +In [68]: frame2["eastern"] = frame2["state"] == "Ohio" + +In [69]: frame2 +Out[69]: + year state pop debt eastern +0 2000 Ohio 1.5 NaN True +1 2001 Ohio 1.7 NaN True +2 2002 Ohio 3.6 -1.2 True +3 2001 Nevada 2.4 NaN False +4 2002 Nevada 2.9 -1.5 False +5 2003 Nevada 3.2 -1.7 False +``` + +警告: + +不能使用`frame2.eastern`点属性表示法创建新列。 + +然后可以使用`del`方法删除此列: + +```py +In [70]: del frame2["eastern"] + +In [71]: frame2.columns +Out[71]: Index(['year', 'state', 'pop', 'debt'], dtype='object') +``` + +注意 + +从 DataFrame 索引返回的列是基础数据的*视图*,而不是副本。因此,对 Series 的任何原地修改都将反映在 DataFrame 中。可以使用 Series 的`copy`方法显式复制列。 + +另一种常见的数据形式是嵌套字典的字典: + +```py +In [72]: populations = {"Ohio": {2000: 1.5, 2001: 1.7, 2002: 3.6}, + ....: "Nevada": {2001: 2.4, 2002: 2.9}} +``` + +如果将嵌套字典传递给 DataFrame,pandas 将解释外部字典键为列,内部键为行索引: + +```py +In [73]: frame3 = pd.DataFrame(populations) + +In [74]: frame3 +Out[74]: + Ohio Nevada +2000 1.5 NaN +2001 1.7 2.4 +2002 3.6 2.9 +``` + +您可以使用类似于 NumPy 数组的语法转置 DataFrame(交换行和列): + +```py +In [75]: frame3.T +Out[75]: + 2000 2001 2002 +Ohio 1.5 1.7 3.6 +Nevada NaN 2.4 2.9 +``` + +警告: + +请注意,如果列的数据类型不全都相同,则转置会丢弃列数据类型,因此转置然后再次转置可能会丢失先前的类型信息。在这种情况下,列变成了纯 Python 对象的数组。 + +内部字典中的键被组合以形成结果中的索引。如果指定了显式索引,则这种情况不成立: + +```py +In [76]: pd.DataFrame(populations, index=[2001, 2002, 2003]) +Out[76]: + Ohio Nevada +2001 1.7 2.4 +2002 3.6 2.9 +2003 NaN NaN +``` + +Series 的字典以类似的方式处理: + +```py +In [77]: pdata = {"Ohio": frame3["Ohio"][:-1], + ....: "Nevada": frame3["Nevada"][:2]} + +In [78]: pd.DataFrame(pdata) +Out[78]: + Ohio Nevada +2000 1.5 NaN +2001 1.7 2.4 +``` + +有关可以传递给 DataFrame 构造函数的许多内容,请参见表 5.1。 + +表 5.1:DataFrame 构造函数的可能数据输入 + +| 类型 | 注释 | +| --- | --- | +| 2D ndarray | 一组数据的矩阵,传递可选的行和列标签 | +| 数组、列表或元组的字典 | 每个序列都变成了 DataFrame 中的一列;所有序列必须具有相同的长度 | +| NumPy 结构化/记录数组 | 被视为“数组的字典”情况 | +| Series 的字典 | 每个值都变成了一列;如果没有传递显式索引,则每个 Series 的索引被合并在一起以形成结果的行索引 | +| 字典的字典 | 每个内部字典都变成了一列;键被合并以形成行索引,就像“Series 的字典”情况一样 | +| 字典或 Series 的列表 | 每个项目都变成了 DataFrame 中的一行;字典键或 Series 索引的并集成为 DataFrame 的列标签 | +| 列表或元组的列表 | 被视为“2D ndarray”情况 | +| 另一个 DataFrame | 除非传递了不同的索引,否则将使用 DataFrame 的索引 | +| NumPy MaskedArray | 与“2D ndarray”情况类似,只是在 DataFrame 结果中缺少掩码值 | + +如果 DataFrame 的`index`和`columns`有设置它们的`name`属性,这些也会被显示出来: + +```py +In [79]: frame3.index.name = "year" + +In [80]: frame3.columns.name = "state" + +In [81]: frame3 +Out[81]: +state Ohio Nevada +year +2000 1.5 NaN +2001 1.7 2.4 +2002 3.6 2.9 +``` + +与 Series 不同,DataFrame 没有`name`属性。DataFrame 的`to_numpy`方法将 DataFrame 中包含的数据作为二维 ndarray 返回: + +```py +In [82]: frame3.to_numpy() +Out[82]: +array([[1.5, nan], + [1.7, 2.4], + [3.6, 2.9]]) +``` + +如果 DataFrame 的列是不同的数据类型,则返回的数组的数据类型将被选择以容纳所有列: + +```py +In [83]: frame2.to_numpy() +Out[83]: +array([[2000, 'Ohio', 1.5, nan], + [2001, 'Ohio', 1.7, nan], + [2002, 'Ohio', 3.6, -1.2], + [2001, 'Nevada', 2.4, nan], + [2002, 'Nevada', 2.9, -1.5], + [2003, 'Nevada', 3.2, -1.7]], dtype=object) +``` + +### 索引对象 + +pandas 的 Index 对象负责保存轴标签(包括 DataFrame 的列名)和其他元数据(如轴名称)。在构建 Series 或 DataFrame 时使用的任何数组或其他标签序列都会在内部转换为 Index: + +```py +In [84]: obj = pd.Series(np.arange(3), index=["a", "b", "c"]) + +In [85]: index = obj.index + +In [86]: index +Out[86]: Index(['a', 'b', 'c'], dtype='object') + +In [87]: index[1:] +Out[87]: Index(['b', 'c'], dtype='object') +``` + +Index 对象是不可变的,因此用户无法修改它们: + +```py +index[1] = "d" # TypeError +``` + +不可变性使得在数据结构之间共享 Index 对象更加安全: + +```py +In [88]: labels = pd.Index(np.arange(3)) + +In [89]: labels +Out[89]: Index([0, 1, 2], dtype='int64') + +In [90]: obj2 = pd.Series([1.5, -2.5, 0], index=labels) + +In [91]: obj2 +Out[91]: +0 1.5 +1 -2.5 +2 0.0 +dtype: float64 + +In [92]: obj2.index is labels +Out[92]: True +``` + +注意 + +一些用户可能不经常利用 Index 提供的功能,但由于一些操作会产生包含索引数据的结果,因此了解它们的工作原理是很重要的。 + +除了类似数组,Index 还表现得像一个固定大小的集合: + +```py +In [93]: frame3 +Out[93]: +state Ohio Nevada +year +2000 1.5 NaN +2001 1.7 2.4 +2002 3.6 2.9 + +In [94]: frame3.columns +Out[94]: Index(['Ohio', 'Nevada'], dtype='object', name='state') + +In [95]: "Ohio" in frame3.columns +Out[95]: True + +In [96]: 2003 in frame3.index +Out[96]: False +``` + +与 Python 集合不同,pandas 的 Index 可以包含重复标签: + +```py +In [97]: pd.Index(["foo", "foo", "bar", "bar"]) +Out[97]: Index(['foo', 'foo', 'bar', 'bar'], dtype='object') +``` + +具有重复标签的选择将选择该标签的所有出现。 + +每个 Index 都有一些用于集合逻辑的方法和属性,可以回答关于其包含的数据的其他常见问题。一些有用的方法总结在 Table 5.2 中。 + +Table 5.2: 一些索引方法和属性 + +| 方法/属性 | 描述 | +| --- | --- | +| `append()` | 与其他 Index 对象连接,生成一个新的 Index | +| `difference()` | 计算索引的差集 | +| `intersection()` | 计算集合交集 | +| `union()` | 计算集合并 | +| `isin()` | 计算布尔数组,指示每个值是否包含在传递的集合中 | +| `delete()` | 通过删除索引`i`处的元素来计算新的索引 | +| `drop()` | 通过删除传递的值来计算新的索引 | +| `insert()` | 通过在索引`i`处插入元素来计算新的索引 | +| `is_monotonic` | 如果每个元素大于或等于前一个元素则返回`True` | +| `is_unique` | 如果索引没有重复值则返回`True` | + +| `unique()` | 计算索引中唯一值的数组 | + +## 5.2 基本功能 + +本节将带领您了解与 Series 或 DataFrame 中包含的数据进行交互的基本机制。在接下来的章节中,我们将更深入地探讨使用 pandas 进行数据分析和操作的主题。本书不旨在作为 pandas 库的详尽文档,而是专注于让您熟悉常用功能,将不太常见的(即更神秘的)内容留给您通过阅读在线 pandas 文档来学习。 + +### 重新索引 + +pandas 对象上的一个重要方法是`reindex`,它意味着创建一个新对象,其值重新排列以与新索引对齐。考虑一个例子: + +```py +In [98]: obj = pd.Series([4.5, 7.2, -5.3, 3.6], index=["d", "b", "a", "c"]) + +In [99]: obj +Out[99]: +d 4.5 +b 7.2 +a -5.3 +c 3.6 +dtype: float64 +``` + +在这个 Series 上调用`reindex`会根据新索引重新排列数据,如果某些索引值之前不存在,则会引入缺失值: + +```py +In [100]: obj2 = obj.reindex(["a", "b", "c", "d", "e"]) + +In [101]: obj2 +Out[101]: +a -5.3 +b 7.2 +c 3.6 +d 4.5 +e NaN +dtype: float64 +``` + +对于有序数据如时间序列,当重新索引时可能需要进行一些插值或值填充。`method`选项允许我们使用`ffill`这样的方法来实现,它可以向前填充值: + +```py +In [102]: obj3 = pd.Series(["blue", "purple", "yellow"], index=[0, 2, 4]) + +In [103]: obj3 +Out[103]: +0 blue +2 purple +4 yellow +dtype: object + +In [104]: obj3.reindex(np.arange(6), method="ffill") +Out[104]: +0 blue +1 blue +2 purple +3 purple +4 yellow +5 yellow +dtype: object +``` + +对于 DataFrame,`reindex`可以改变(行)索引、列或两者。当只传递一个序列时,它会重新索引结果中的行: + +```py +In [105]: frame = pd.DataFrame(np.arange(9).reshape((3, 3)), + .....: index=["a", "c", "d"], + .....: columns=["Ohio", "Texas", "California"]) + +In [106]: frame +Out[106]: + Ohio Texas California +a 0 1 2 +c 3 4 5 +d 6 7 8 + +In [107]: frame2 = frame.reindex(index=["a", "b", "c", "d"]) + +In [108]: frame2 +Out[108]: + Ohio Texas California +a 0.0 1.0 2.0 +b NaN NaN NaN +c 3.0 4.0 5.0 +d 6.0 7.0 8.0 +``` + +可以使用`columns`关键字重新索引列: + +```py +In [109]: states = ["Texas", "Utah", "California"] + +In [110]: frame.reindex(columns=states) +Out[110]: + Texas Utah California +a 1 NaN 2 +c 4 NaN 5 +d 7 NaN 8 +``` + +因为`"Ohio"`不在`states`中,所以该列的数据被从结果中删除。 + +重新索引特定轴的另一种方法是将新的轴标签作为位置参数传递,然后使用`axis`关键字指定要重新索引的轴: + +```py +In [111]: frame.reindex(states, axis="columns") +Out[111]: + Texas Utah California +a 1 NaN 2 +c 4 NaN 5 +d 7 NaN 8 +``` + +查看 Table 5.3 以了解有关`reindex`参数的更多信息。 + +表 5.3:`reindex`函数参数 + +| 参数 | 描述 | +| --- | --- | +| `labels` | 用作索引的新序列。可以是 Index 实例或任何其他类似序列的 Python 数据结构。Index 将被完全使用,不会进行任何复制。 | +| `index` | 使用传递的序列作为新的索引标签。 | +| `columns` | 使用传递的序列作为新的列标签。 | +| `axis` | 要重新索引的轴,无论是`"index"`(行)还是`"columns"`。默认为`"index"`。您也可以使用`reindex(index=new_labels)`或`reindex(columns=new_labels)`。 | +| `method` | 插值(填充)方法;`"ffill"`向前填充,而`"bfill"`向后填充。 | +| `fill_value` | 重新索引时引入缺失数据时要使用的替代值。当您希望缺失标签在结果中具有空值时,请使用`fill_value="missing"`(默认行为)。 | +| `limit` | 在向前填充或向后填充时,要填充的最大大小间隙(元素数量)。 | +| `tolerance` | 在向前填充或向后填充时,要填充的最大大小间隙(绝对数值距离)。 | +| `level` | 在 MultiIndex 级别上匹配简单索引;否则选择子集。 | +| `copy` | 如果为`True`,即使新索引等效于旧索引,也始终复制基础数据;如果为`False`,当索引等效时不复制数据。 | + +正如我们稍后将在使用 loc 和 iloc 在 DataFrame 上进行选择中探讨的,您也可以通过使用`loc`运算符重新索引,许多用户更喜欢始终以这种方式进行操作。这仅在所有新索引标签已存在于 DataFrame 中时才有效(而`reindex`将为新标签插入缺失数据): + +```py +In [112]: frame.loc[["a", "d", "c"], ["California", "Texas"]] +Out[112]: + California Texas +a 2 1 +d 8 7 +c 5 4 +``` + +### 从轴中删除条目 + +如果您已经有一个不包含这些条目的索引数组或列表,那么从轴中删除一个或多个条目就很简单,因为您可以使用`reindex`方法或基于`.loc`的索引。由于这可能需要一些数据处理和集合逻辑,`drop`方法将返回一个新对象,其中包含从轴中删除的指定值或值: + +```py +In [113]: obj = pd.Series(np.arange(5.), index=["a", "b", "c", "d", "e"]) + +In [114]: obj +Out[114]: +a 0.0 +b 1.0 +c 2.0 +d 3.0 +e 4.0 +dtype: float64 + +In [115]: new_obj = obj.drop("c") + +In [116]: new_obj +Out[116]: +a 0.0 +b 1.0 +d 3.0 +e 4.0 +dtype: float64 + +In [117]: obj.drop(["d", "c"]) +Out[117]: +a 0.0 +b 1.0 +e 4.0 +dtype: float64 +``` + +使用 DataFrame,可以从任一轴删除索引值。为了说明这一点,我们首先创建一个示例 DataFrame: + +```py +In [118]: data = pd.DataFrame(np.arange(16).reshape((4, 4)), + .....: index=["Ohio", "Colorado", "Utah", "New York"], + .....: columns=["one", "two", "three", "four"]) + +In [119]: data +Out[119]: + one two three four +Ohio 0 1 2 3 +Colorado 4 5 6 7 +Utah 8 9 10 11 +New York 12 13 14 15 +``` + +使用一系列标签调用`drop`将从行标签(轴 0)中删除值: + +```py +In [120]: data.drop(index=["Colorado", "Ohio"]) +Out[120]: + one two three four +Utah 8 9 10 11 +New York 12 13 14 15 +``` + +要从列中删除标签,而不是使用`columns`关键字: + +```py +In [121]: data.drop(columns=["two"]) +Out[121]: + one three four +Ohio 0 2 3 +Colorado 4 6 7 +Utah 8 10 11 +New York 12 14 15 +``` + +您还可以通过传递`axis=1`(类似于 NumPy)或`axis="columns"`来从列中删除值: + +```py +In [122]: data.drop("two", axis=1) +Out[122]: + one three four +Ohio 0 2 3 +Colorado 4 6 7 +Utah 8 10 11 +New York 12 14 15 + +In [123]: data.drop(["two", "four"], axis="columns") +Out[123]: + one three +Ohio 0 2 +Colorado 4 6 +Utah 8 10 +New York 12 14 +``` + +### 索引、选择和过滤 + +Series 索引(`obj[...]`)的工作方式类似于 NumPy 数组索引,只是您可以使用 Series 的索引值而不仅仅是整数。以下是一些示例: + +```py +In [124]: obj = pd.Series(np.arange(4.), index=["a", "b", "c", "d"]) + +In [125]: obj +Out[125]: +a 0.0 +b 1.0 +c 2.0 +d 3.0 +dtype: float64 + +In [126]: obj["b"] +Out[126]: 1.0 + +In [127]: obj[1] +Out[127]: 1.0 + +In [128]: obj[2:4] +Out[128]: +c 2.0 +d 3.0 +dtype: float64 + +In [129]: obj[["b", "a", "d"]] +Out[129]: +b 1.0 +a 0.0 +d 3.0 +dtype: float64 + +In [130]: obj[[1, 3]] +Out[130]: +b 1.0 +d 3.0 +dtype: float64 + +In [131]: obj[obj < 2] +Out[131]: +a 0.0 +b 1.0 +dtype: float64 +``` + +虽然您可以通过标签这种方式选择数据,但选择索引值的首选方式是使用特殊的`loc`运算符: + +```py +In [132]: obj.loc[["b", "a", "d"]] +Out[132]: +b 1.0 +a 0.0 +d 3.0 +dtype: float64 +``` + +更喜欢`loc`的原因是因为在使用`[]`进行索引时,对整数的处理方式不同。如果索引包含整数,常规的`[]`索引将将整数视为标签,因此行为取决于索引的数据类型。例如: + +```py +In [133]: obj1 = pd.Series([1, 2, 3], index=[2, 0, 1]) + +In [134]: obj2 = pd.Series([1, 2, 3], index=["a", "b", "c"]) + +In [135]: obj1 +Out[135]: +2 1 +0 2 +1 3 +dtype: int64 + +In [136]: obj2 +Out[136]: +a 1 +b 2 +c 3 +dtype: int64 + +In [137]: obj1[[0, 1, 2]] +Out[137]: +0 2 +1 3 +2 1 +dtype: int64 + +In [138]: obj2[[0, 1, 2]] +Out[138]: +a 1 +b 2 +c 3 +dtype: int64 +``` + +在使用`loc`时,当索引不包含整数时,表达式`obj.loc[[0, 1, 2]]`将失败: + +```py +In [134]: obj2.loc[[0, 1]] +--------------------------------------------------------------------------- +KeyError Traceback (most recent call last) +/tmp/ipykernel_804589/4185657903.py in +----> 1 obj2.loc[[0, 1]] + +^ LONG EXCEPTION ABBREVIATED ^ + +KeyError: "None of [Int64Index([0, 1], dtype="int64")] are in the [index]" +``` + +由于`loc`运算符仅使用标签进行索引,因此还有一个`iloc`运算符,它仅使用整数进行索引,以便在索引包含整数或不包含整数时始终保持一致: + +```py +In [139]: obj1.iloc[[0, 1, 2]] +Out[139]: +2 1 +0 2 +1 3 +dtype: int64 + +In [140]: obj2.iloc[[0, 1, 2]] +Out[140]: +a 1 +b 2 +c 3 +dtype: int64 +``` + +注意 + +您也可以使用标签进行切片,但与正常的 Python 切片不同,终点是包含的: + +```py +In [141]: obj2.loc["b":"c"] +Out[141]: +b 2 +c 3 +dtype: int64 +``` + +使用这些方法分配值会修改 Series 的相应部分: + +```py +In [142]: obj2.loc["b":"c"] = 5 + +In [143]: obj2 +Out[143]: +a 1 +b 5 +c 5 +dtype: int64 +``` + +注意 + +尝试调用`loc`或`iloc`等函数而不是使用方括号“索引”可能是新手的常见错误。方括号表示用于启用切片操作并允许在 DataFrame 对象上的多个轴上进行索引。 + +在 DataFrame 中进行索引会检索一个或多个列,可以使用单个值或序列: + +```py +In [144]: data = pd.DataFrame(np.arange(16).reshape((4, 4)), + .....: index=["Ohio", "Colorado", "Utah", "New York"], + .....: columns=["one", "two", "three", "four"]) + +In [145]: data +Out[145]: + one two three four +Ohio 0 1 2 3 +Colorado 4 5 6 7 +Utah 8 9 10 11 +New York 12 13 14 15 + +In [146]: data["two"] +Out[146]: +Ohio 1 +Colorado 5 +Utah 9 +New York 13 +Name: two, dtype: int64 + +In [147]: data[["three", "one"]] +Out[147]: + three one +Ohio 2 0 +Colorado 6 4 +Utah 10 8 +New York 14 12 +``` + +这种索引有一些特殊情况。第一个是使用布尔数组进行切片或选择数据: + +```py +In [148]: data[:2] +Out[148]: + one two three four +Ohio 0 1 2 3 +Colorado 4 5 6 7 + +In [149]: data[data["three"] > 5] +Out[149]: + one two three four +Colorado 4 5 6 7 +Utah 8 9 10 11 +New York 12 13 14 15 +``` + +行选择语法`data[:2]`是作为一种便利提供的。将单个元素或列表传递给`[]`运算符将选择列。 + +另一个用例是使用布尔 DataFrame 进行索引,比如通过标量比较生成的 DataFrame。考虑一个通过与标量值比较生成的全布尔值的 DataFrame: + +```py +In [150]: data < 5 +Out[150]: + one two three four +Ohio True True True True +Colorado True False False False +Utah False False False False +New York False False False False +``` + +我们可以使用这个 DataFrame 将值为`True`的位置赋值为 0,就像这样: + +```py +In [151]: data[data < 5] = 0 + +In [152]: data +Out[152]: + one two three four +Ohio 0 0 0 0 +Colorado 0 5 6 7 +Utah 8 9 10 11 +New York 12 13 14 15 +``` + +#### 使用 loc 和 iloc 在 DataFrame 上进行选择 + +与 Series 一样,DataFrame 具有专门的属性`loc`和`iloc`,用于基于标签和基于整数的索引。由于 DataFrame 是二维的,您可以使用类似 NumPy 的符号使用轴标签(`loc`)或整数(`iloc`)选择行和列的子集。 + +作为第一个示例,让我们通过标签选择单行: + +```py +In [153]: data +Out[153]: + one two three four +Ohio 0 0 0 0 +Colorado 0 5 6 7 +Utah 8 9 10 11 +New York 12 13 14 15 + +In [154]: data.loc["Colorado"] +Out[154]: +one 0 +two 5 +three 6 +four 7 +Name: Colorado, dtype: int64 +``` + +选择单行的结果是一个带有包含 DataFrame 列标签的索引的 Series。要选择多个行,创建一个新的 DataFrame,传递一个标签序列: + +```py +In [155]: data.loc[["Colorado", "New York"]] +Out[155]: + one two three four +Colorado 0 5 6 7 +New York 12 13 14 15 +``` + +您可以通过用逗号分隔选择在`loc`中同时选择行和列: + +```py +In [156]: data.loc["Colorado", ["two", "three"]] +Out[156]: +two 5 +three 6 +Name: Colorado, dtype: int64 +``` + +然后我们将使用`iloc`执行一些类似的整数选择: + +```py +In [157]: data.iloc[2] +Out[157]: +one 8 +two 9 +three 10 +four 11 +Name: Utah, dtype: int64 + +In [158]: data.iloc[[2, 1]] +Out[158]: + one two three four +Utah 8 9 10 11 +Colorado 0 5 6 7 + +In [159]: data.iloc[2, [3, 0, 1]] +Out[159]: +four 11 +one 8 +two 9 +Name: Utah, dtype: int64 + +In [160]: data.iloc[[1, 2], [3, 0, 1]] +Out[160]: + four one two +Colorado 7 0 5 +Utah 11 8 9 +``` + +这两个索引函数都可以处理切片,除了单个标签或标签列表: + +```py +In [161]: data.loc[:"Utah", "two"] +Out[161]: +Ohio 0 +Colorado 5 +Utah 9 +Name: two, dtype: int64 + +In [162]: data.iloc[:, :3][data.three > 5] +Out[162]: + one two three +Colorado 0 5 6 +Utah 8 9 10 +New York 12 13 14 +``` + +布尔数组可以与`loc`一起使用,但不能与`iloc`一起使用: + +```py +In [163]: data.loc[data.three >= 2] +Out[163]: + one two three four +Colorado 0 5 6 7 +Utah 8 9 10 11 +New York 12 13 14 15 +``` + +有许多方法可以选择和重新排列 pandas 对象中包含的数据。对于 DataFrame,表 5.4 提供了许多这些方法的简要总结。正如您将在后面看到的,还有许多其他选项可用于处理分层索引。 + +表 5.4:DataFrame 的索引选项 + +| 类型 | 注释 | +| --- | --- | +| `df[column]` | 从 DataFrame 中选择单个列或列序列;特殊情况便利:布尔数组(过滤行)、切片(切片行)或布尔 DataFrame(根据某些条件设置值) | +| `df.loc[rows]` | 通过标签从 DataFrame 中选择单行或行子集 | +| `df.loc[:, cols]` | 通过标签选择单个列或列子集 | +| `df.loc[rows, cols]` | 通过标签选择行和列 | +| `df.iloc[rows]` | 通过整数位置从 DataFrame 中选择单行或行子集 | +| `df.iloc[:, cols]` | 通过整数位置选择单个列或列子集 | +| `df.iloc[rows, cols]` | 通过整数位置选择行和列 | +| `df.at[row, col]` | 通过行和列标签选择单个标量值 | +| `df.iat[row, col]` | 通过行和列位置(整数)选择单个标量值 | +| `reindex`方法 | 通过标签选择行或列 | + +#### 整数索引的陷阱 + +使用整数索引的 pandas 对象可能会成为新用户的绊脚石,因为它们与内置的 Python 数据结构(如列表和元组)的工作方式不同。例如,您可能不会期望以下代码生成错误: + +```py +In [164]: ser = pd.Series(np.arange(3.)) + +In [165]: ser +Out[165]: +0 0.0 +1 1.0 +2 2.0 +dtype: float64 + +In [166]: ser[-1] +--------------------------------------------------------------------------- +ValueError Traceback (most recent call last) +~/miniforge-x86/envs/book-env/lib/python3.10/site-packages/pandas/core/indexes/ra +nge.py in get_loc(self, key) + 344 try: +--> 345 return self._range.index(new_key) + 346 except ValueError as err: +ValueError: -1 is not in range +The above exception was the direct cause of the following exception: +KeyError Traceback (most recent call last) + in +----> 1 ser[-1] +~/miniforge-x86/envs/book-env/lib/python3.10/site-packages/pandas/core/series.py +in __getitem__(self, key) + 1010 + 1011 elif key_is_scalar: +-> 1012 return self._get_value(key) + 1013 + 1014 if is_hashable(key): +~/miniforge-x86/envs/book-env/lib/python3.10/site-packages/pandas/core/series.py +in _get_value(self, label, takeable) + 1119 + 1120 # Similar to Index.get_value, but we do not fall back to position +al +-> 1121 loc = self.index.get_loc(label) + 1122 + 1123 if is_integer(loc): +~/miniforge-x86/envs/book-env/lib/python3.10/site-packages/pandas/core/indexes/ra +nge.py in get_loc(self, key) + 345 return self._range.index(new_key) + 346 except ValueError as err: +--> 347 raise KeyError(key) from err + 348 self._check_indexing_error(key) + 349 raise KeyError(key) +KeyError: -1 +``` + +在这种情况下,pandas 可能会“回退”到整数索引,但是在不引入对用户代码中微妙错误的情况下,通常很难做到这一点。在这里,我们有一个包含`0`、`1`和`2`的索引,但 pandas 不想猜测用户想要什么(基于标签的索引还是基于位置的): + +```py +In [167]: ser +Out[167]: +0 0.0 +1 1.0 +2 2.0 +dtype: float64 +``` + +另一方面,对于非整数索引,没有这种歧义: + +```py +In [168]: ser2 = pd.Series(np.arange(3.), index=["a", "b", "c"]) + +In [169]: ser2[-1] +Out[169]: 2.0 +``` + +如果您有包含整数的轴索引,数据选择将始终是基于标签的。正如我上面所说的,如果您使用`loc`(用于标签)或`iloc`(用于整数),您将得到确切想要的结果: + +```py +In [170]: ser.iloc[-1] +Out[170]: 2.0 +``` + +另一方面,使用整数进行切片始终是基于整数的: + +```py +In [171]: ser[:2] +Out[171]: +0 0.0 +1 1.0 +dtype: float64 +``` + +由于这些陷阱,最好始终优先使用`loc`和`iloc`进行索引,以避免歧义。 + +#### 链式索引的陷阱 + +在前一节中,我们看了如何使用`loc`和`iloc`在 DataFrame 上进行灵活的选择。这些索引属性也可以用于就地修改 DataFrame 对象,但这样做需要一些小心。 + +例如,在上面的 DataFrame 示例中,我们可以按标签或整数位置分配到列或行: + +```py +In [172]: data.loc[:, "one"] = 1 + +In [173]: data +Out[173]: + one two three four +Ohio 1 0 0 0 +Colorado 1 5 6 7 +Utah 1 9 10 11 +New York 1 13 14 15 + +In [174]: data.iloc[2] = 5 + +In [175]: data +Out[175]: + one two three four +Ohio 1 0 0 0 +Colorado 1 5 6 7 +Utah 5 5 5 5 +New York 1 13 14 15 + +In [176]: data.loc[data["four"] > 5] = 3 + +In [177]: data +Out[177]: + one two three four +Ohio 1 0 0 0 +Colorado 3 3 3 3 +Utah 5 5 5 5 +New York 3 3 3 3 +``` + +对于新的 pandas 用户来说,一个常见的坑是在赋值时链接选择,就像这样: + +```py +In [177]: data.loc[data.three == 5]["three"] = 6 +:1: SettingWithCopyWarning: +A value is trying to be set on a copy of a slice from a DataFrame. +Try using .loc[row_indexer,col_indexer] = value instead +``` + +根据数据内容的不同,这可能会打印一个特殊的`SettingWithCopyWarning`,它警告您正在尝试修改一个临时值(`data.loc[data.three == 5]`的非空结果),而不是原始 DataFrame`data`,这可能是您的本意。在这里,`data`没有被修改: + +```py +In [179]: data +Out[179]: + one two three four +Ohio 1 0 0 0 +Colorado 3 3 3 3 +Utah 5 5 5 5 +New York 3 3 3 3 +``` + +在这些情况下,修复的方法是重写链接赋值,使用单个`loc`操作: + +```py +In [180]: data.loc[data.three == 5, "three"] = 6 + +In [181]: data +Out[181]: + one two three four +Ohio 1 0 0 0 +Colorado 3 3 3 3 +Utah 5 5 6 5 +New York 3 3 3 3 +``` + +一个很好的经验法则是在进行赋值时避免链接索引。还有其他情况下,pandas 会生成`SettingWithCopyWarning`,这与链接索引有关。我建议您查阅在线 pandas 文档中的这个主题。 + +### 算术和数据对齐 + +pandas 可以使处理具有不同索引的对象变得更简单。例如,当您添加对象时,如果任何索引对不相同,结果中的相应索引将是索引对的并集。让我们看一个例子: + +```py +In [182]: s1 = pd.Series([7.3, -2.5, 3.4, 1.5], index=["a", "c", "d", "e"]) + +In [183]: s2 = pd.Series([-2.1, 3.6, -1.5, 4, 3.1], + .....: index=["a", "c", "e", "f", "g"]) + +In [184]: s1 +Out[184]: +a 7.3 +c -2.5 +d 3.4 +e 1.5 +dtype: float64 + +In [185]: s2 +Out[185]: +a -2.1 +c 3.6 +e -1.5 +f 4.0 +g 3.1 +dtype: float64 +``` + +将它们相加得到: + +```py +In [186]: s1 + s2 +Out[186]: +a 5.2 +c 1.1 +d NaN +e 0.0 +f NaN +g NaN +dtype: float64 +``` + +内部数据对齐会在不重叠的标签位置引入缺失值。缺失值将在进一步的算术计算中传播。 + +对于 DataFrame,对齐是在行和列上执行的: + +```py +In [187]: df1 = pd.DataFrame(np.arange(9.).reshape((3, 3)), columns=list("bcd"), + .....: index=["Ohio", "Texas", "Colorado"]) + +In [188]: df2 = pd.DataFrame(np.arange(12.).reshape((4, 3)), columns=list("bde"), + .....: index=["Utah", "Ohio", "Texas", "Oregon"]) + +In [189]: df1 +Out[189]: + b c d +Ohio 0.0 1.0 2.0 +Texas 3.0 4.0 5.0 +Colorado 6.0 7.0 8.0 + +In [190]: df2 +Out[190]: + b d e +Utah 0.0 1.0 2.0 +Ohio 3.0 4.0 5.0 +Texas 6.0 7.0 8.0 +Oregon 9.0 10.0 11.0 +``` + +将它们相加返回一个 DataFrame,其索引和列是每个 DataFrame 中的索引的并集: + +```py +In [191]: df1 + df2 +Out[191]: + b c d e +Colorado NaN NaN NaN NaN +Ohio 3.0 NaN 6.0 NaN +Oregon NaN NaN NaN NaN +Texas 9.0 NaN 12.0 NaN +Utah NaN NaN NaN NaN +``` + +由于 DataFrame 对象中都没有找到`"c"`和`"e"`列,它们在结果中显示为缺失。对于标签不共同的行也是如此。 + +如果添加没有共同列或行标签的 DataFrame 对象,结果将包含所有空值: + +```py +In [192]: df1 = pd.DataFrame({"A": [1, 2]}) + +In [193]: df2 = pd.DataFrame({"B": [3, 4]}) + +In [194]: df1 +Out[194]: + A +0 1 +1 2 + +In [195]: df2 +Out[195]: + B +0 3 +1 4 + +In [196]: df1 + df2 +Out[196]: + A B +0 NaN NaN +1 NaN NaN +``` + +#### 带有填充值的算术方法 + +在不同索引对象之间的算术操作中,当一个对象中找到一个轴标签而另一个对象中没有时,您可能希望填充一个特殊值,比如 0。以下是一个示例,我们通过将`np.nan`赋值给它来将特定值设置为 NA(null): + +```py +In [197]: df1 = pd.DataFrame(np.arange(12.).reshape((3, 4)), + .....: columns=list("abcd")) + +In [198]: df2 = pd.DataFrame(np.arange(20.).reshape((4, 5)), + .....: columns=list("abcde")) + +In [199]: df2.loc[1, "b"] = np.nan + +In [200]: df1 +Out[200]: + a b c d +0 0.0 1.0 2.0 3.0 +1 4.0 5.0 6.0 7.0 +2 8.0 9.0 10.0 11.0 + +In [201]: df2 +Out[201]: + a b c d e +0 0.0 1.0 2.0 3.0 4.0 +1 5.0 NaN 7.0 8.0 9.0 +2 10.0 11.0 12.0 13.0 14.0 +3 15.0 16.0 17.0 18.0 19.0 +``` + +将它们相加会导致不重叠位置的缺失值: + +```py +In [202]: df1 + df2 +Out[202]: + a b c d e +0 0.0 2.0 4.0 6.0 NaN +1 9.0 NaN 13.0 15.0 NaN +2 18.0 20.0 22.0 24.0 NaN +3 NaN NaN NaN NaN NaN +``` + +在`df1`上使用`add`方法,我传递`df2`和一个参数给`fill_value`,它会用传递的值替换操作中的任何缺失值: + +```py +In [203]: df1.add(df2, fill_value=0) +Out[203]: + a b c d e +0 0.0 2.0 4.0 6.0 4.0 +1 9.0 5.0 13.0 15.0 9.0 +2 18.0 20.0 22.0 24.0 14.0 +3 15.0 16.0 17.0 18.0 19.0 +``` + +请参阅表 5.5 以获取有关算术的 Series 和 DataFrame 方法的列表。每个方法都有一个对应的方法,以字母`r`开头,参数顺序相反。因此,以下两个语句是等价的: + +```py +In [204]: 1 / df1 +Out[204]: + a b c d +0 inf 1.000000 0.500000 0.333333 +1 0.250 0.200000 0.166667 0.142857 +2 0.125 0.111111 0.100000 0.090909 + +In [205]: df1.rdiv(1) +Out[205]: + a b c d +0 inf 1.000000 0.500000 0.333333 +1 0.250 0.200000 0.166667 0.142857 +2 0.125 0.111111 0.100000 0.090909 +``` + +相关地,在重新索引 Series 或 DataFrame 时,您还可以指定不同的填充值: + +```py +In [206]: df1.reindex(columns=df2.columns, fill_value=0) +Out[206]: + a b c d e +0 0.0 1.0 2.0 3.0 0 +1 4.0 5.0 6.0 7.0 0 +2 8.0 9.0 10.0 11.0 0 +``` + +表 5.5:灵活的算术方法 + +| 方法 | 描述 | +| --- | --- | +| `add, radd` | 加法方法(+) | +| `sub, rsub` | 减法方法(-) | +| `div, rdiv` | 除法方法(/) | +| `floordiv, rfloordiv` | 地板除法方法(//) | +| `mul, rmul` | 乘法方法(*) | +| `pow, rpow` | 指数方法(**) | + +#### DataFrame 和 Series 之间的操作 + +与不同维度的 NumPy 数组一样,DataFrame 和 Series 之间的算术也是定义的。首先,作为一个激励性的例子,考虑一个二维数组和其一行之间的差异: + +```py +In [207]: arr = np.arange(12.).reshape((3, 4)) + +In [208]: arr +Out[208]: +array([[ 0., 1., 2., 3.], + [ 4., 5., 6., 7.], + [ 8., 9., 10., 11.]]) + +In [209]: arr[0] +Out[209]: array([0., 1., 2., 3.]) + +In [210]: arr - arr[0] +Out[210]: +array([[0., 0., 0., 0.], + [4., 4., 4., 4.], + [8., 8., 8., 8.]]) +``` + +当我们从`arr`中减去`arr[0]`时,减法将针对每一行执行一次。这被称为*广播*,并且在附录 A:高级 NumPy 中更详细地解释了它与一般 NumPy 数组的关系。DataFrame 和 Series 之间的操作类似: + +```py +In [211]: frame = pd.DataFrame(np.arange(12.).reshape((4, 3)), + .....: columns=list("bde"), + .....: index=["Utah", "Ohio", "Texas", "Oregon"]) + +In [212]: series = frame.iloc[0] + +In [213]: frame +Out[213]: + b d e +Utah 0.0 1.0 2.0 +Ohio 3.0 4.0 5.0 +Texas 6.0 7.0 8.0 +Oregon 9.0 10.0 11.0 + +In [214]: series +Out[214]: +b 0.0 +d 1.0 +e 2.0 +Name: Utah, dtype: float64 +``` + +默认情况下,DataFrame 和 Series 之间的算术会将 Series 的索引与 DataFrame 的列匹配,向下广播行: + +```py +In [215]: frame - series +Out[215]: + b d e +Utah 0.0 0.0 0.0 +Ohio 3.0 3.0 3.0 +Texas 6.0 6.0 6.0 +Oregon 9.0 9.0 9.0 +``` + +如果索引值既不在 DataFrame 的列中,也不在 Series 的索引中找到,那么对象将被重新索引以形成并集: + +```py +In [216]: series2 = pd.Series(np.arange(3), index=["b", "e", "f"]) + +In [217]: series2 +Out[217]: +b 0 +e 1 +f 2 +dtype: int64 + +In [218]: frame + series2 +Out[218]: + b d e f +Utah 0.0 NaN 3.0 NaN +Ohio 3.0 NaN 6.0 NaN +Texas 6.0 NaN 9.0 NaN +Oregon 9.0 NaN 12.0 NaN +``` + +如果您希望在列上进行广播,匹配行,您必须使用其中一个算术方法并指定匹配索引。例如: + +```py +In [219]: series3 = frame["d"] + +In [220]: frame +Out[220]: + b d e +Utah 0.0 1.0 2.0 +Ohio 3.0 4.0 5.0 +Texas 6.0 7.0 8.0 +Oregon 9.0 10.0 11.0 + +In [221]: series3 +Out[221]: +Utah 1.0 +Ohio 4.0 +Texas 7.0 +Oregon 10.0 +Name: d, dtype: float64 + +In [222]: frame.sub(series3, axis="index") +Out[222]: + b d e +Utah -1.0 0.0 1.0 +Ohio -1.0 0.0 1.0 +Texas -1.0 0.0 1.0 +Oregon -1.0 0.0 1.0 +``` + +您传递的轴是*要匹配的轴*。在这种情况下,我们的意思是匹配 DataFrame 的行索引(`axis="index"`)并在列之间广播。 + +### 函数应用和映射 + +NumPy ufuncs(逐元素数组方法)也适用于 pandas 对象: + +```py +In [223]: frame = pd.DataFrame(np.random.standard_normal((4, 3)), + .....: columns=list("bde"), + .....: index=["Utah", "Ohio", "Texas", "Oregon"]) + +In [224]: frame +Out[224]: + b d e +Utah -0.204708 0.478943 -0.519439 +Ohio -0.555730 1.965781 1.393406 +Texas 0.092908 0.281746 0.769023 +Oregon 1.246435 1.007189 -1.296221 + +In [225]: np.abs(frame) +Out[225]: + b d e +Utah 0.204708 0.478943 0.519439 +Ohio 0.555730 1.965781 1.393406 +Texas 0.092908 0.281746 0.769023 +Oregon 1.246435 1.007189 1.296221 +``` + +另一个频繁的操作是将一个一维数组上的函数应用于每列或每行。DataFrame 的`apply`方法正是这样做的: + +```py +In [226]: def f1(x): + .....: return x.max() - x.min() + +In [227]: frame.apply(f1) +Out[227]: +b 1.802165 +d 1.684034 +e 2.689627 +dtype: float64 +``` + +这里的函数`f`计算 Series 的最大值和最小值之间的差异,对`frame`中的每列调用一次。结果是一个具有`frame`列作为其索引的 Series。 + +如果将`axis="columns"`传递给`apply`,则该函数将每行调用一次。将其视为"跨列应用"是一种有用的方式: + +```py +In [228]: frame.apply(f1, axis="columns") +Out[228]: +Utah 0.998382 +Ohio 2.521511 +Texas 0.676115 +Oregon 2.542656 +dtype: float64 +``` + +许多最常见的数组统计(如`sum`和`mean`)都是 DataFrame 方法,因此不需要使用`apply`。 + +传递给`apply`的函数不必返回标量值;它也可以返回具有多个值的 Series: + +```py +In [229]: def f2(x): + .....: return pd.Series([x.min(), x.max()], index=["min", "max"]) + +In [230]: frame.apply(f2) +Out[230]: + b d e +min -0.555730 0.281746 -1.296221 +max 1.246435 1.965781 1.393406 +``` + +也可以使用逐元素 Python 函数。假设您想要从`frame`中的每个浮点值计算格式化字符串。您可以使用`applymap`来实现: + +```py +In [231]: def my_format(x): + .....: return f"{x:.2f}" + +In [232]: frame.applymap(my_format) +Out[232]: + b d e +Utah -0.20 0.48 -0.52 +Ohio -0.56 1.97 1.39 +Texas 0.09 0.28 0.77 +Oregon 1.25 1.01 -1.30 +``` + +`applymap`的命名原因是 Series 有一个`map`方法,用于应用逐元素函数: + +```py +In [233]: frame["e"].map(my_format) +Out[233]: +Utah -0.52 +Ohio 1.39 +Texas 0.77 +Oregon -1.30 +Name: e, dtype: object +``` + +### 排序和排名 + +按某个标准对数据集进行排序是另一个重要的内置操作。要按行或列标签的字典顺序排序,请使用`sort_index`方法,该方法返回一个新的排序对象: + +```py +In [234]: obj = pd.Series(np.arange(4), index=["d", "a", "b", "c"]) + +In [235]: obj +Out[235]: +d 0 +a 1 +b 2 +c 3 +dtype: int64 + +In [236]: obj.sort_index() +Out[236]: +a 1 +b 2 +c 3 +d 0 +dtype: int64 +``` + +对于 DataFrame,您可以在任一轴上按索引排序: + +```py +In [237]: frame = pd.DataFrame(np.arange(8).reshape((2, 4)), + .....: index=["three", "one"], + .....: columns=["d", "a", "b", "c"]) + +In [238]: frame +Out[238]: + d a b c +three 0 1 2 3 +one 4 5 6 7 + +In [239]: frame.sort_index() +Out[239]: + d a b c +one 4 5 6 7 +three 0 1 2 3 + +In [240]: frame.sort_index(axis="columns") +Out[240]: + a b c d +three 1 2 3 0 +one 5 6 7 4 +``` + +默认情况下,数据按升序排序,但也可以按降序排序: + +```py +In [241]: frame.sort_index(axis="columns", ascending=False) +Out[241]: + d c b a +three 0 3 2 1 +one 4 7 6 5 +``` + +要按值对 Series 进行排序,请使用其`sort_values`方法: + +```py +In [242]: obj = pd.Series([4, 7, -3, 2]) + +In [243]: obj.sort_values() +Out[243]: +2 -3 +3 2 +0 4 +1 7 +dtype: int64 +``` + +默认情况下,任何缺失值都按顺序排在 Series 的末尾: + +```py +In [244]: obj = pd.Series([4, np.nan, 7, np.nan, -3, 2]) + +In [245]: obj.sort_values() +Out[245]: +4 -3.0 +5 2.0 +0 4.0 +2 7.0 +1 NaN +3 NaN +dtype: float64 +``` + +缺失值也可以通过使用`na_position`选项将其排序到开头: + +```py +In [246]: obj.sort_values(na_position="first") +Out[246]: +1 NaN +3 NaN +4 -3.0 +5 2.0 +0 4.0 +2 7.0 +dtype: float64 +``` + +在对 DataFrame 进行排序时,可以使用一个或多个列中的数据作为排序键。为此,请将一个或多个列名传递给`sort_values`: + +```py +In [247]: frame = pd.DataFrame({"b": [4, 7, -3, 2], "a": [0, 1, 0, 1]}) + +In [248]: frame +Out[248]: + b a +0 4 0 +1 7 1 +2 -3 0 +3 2 1 + +In [249]: frame.sort_values("b") +Out[249]: + b a +2 -3 0 +3 2 1 +0 4 0 +1 7 1 +``` + +要按多个列排序,请传递一个名称列表: + +```py +In [250]: frame.sort_values(["a", "b"]) +Out[250]: + b a +2 -3 0 +0 4 0 +3 2 1 +1 7 1 +``` + +*排名*从数组中的最低值开始,为数组中的每个有效数据点分配从 1 到数据点数量的等级。Series 和 DataFrame 的`rank`方法是要查看的地方;默认情况下,`rank`通过为每个组分配平均等级来打破平局: + +```py +In [251]: obj = pd.Series([7, -5, 7, 4, 2, 0, 4]) + +In [252]: obj.rank() +Out[252]: +0 6.5 +1 1.0 +2 6.5 +3 4.5 +4 3.0 +5 2.0 +6 4.5 +dtype: float64 +``` + +排名也可以根据它们在数据中观察到的顺序进行分配: + +```py +In [253]: obj.rank(method="first") +Out[253]: +0 6.0 +1 1.0 +2 7.0 +3 4.0 +4 3.0 +5 2.0 +6 5.0 +dtype: float64 +``` + +在这里,与使用条目 0 和 2 的平均等级 6.5 不同,它们分别设置为 6 和 7,因为标签 0 在数据中位于标签 2 之前。 + +您也可以按降序排名: + +```py +In [254]: obj.rank(ascending=False) +Out[254]: +0 1.5 +1 7.0 +2 1.5 +3 3.5 +4 5.0 +5 6.0 +6 3.5 +dtype: float64 +``` + +请参阅表 5.6 以获取可用的平局破解方法列表。 + +DataFrame 可以在行或列上计算排名: + +```py +In [255]: frame = pd.DataFrame({"b": [4.3, 7, -3, 2], "a": [0, 1, 0, 1], + .....: "c": [-2, 5, 8, -2.5]}) + +In [256]: frame +Out[256]: + b a c +0 4.3 0 -2.0 +1 7.0 1 5.0 +2 -3.0 0 8.0 +3 2.0 1 -2.5 + +In [257]: frame.rank(axis="columns") +Out[257]: + b a c +0 3.0 2.0 1.0 +1 3.0 1.0 2.0 +2 1.0 2.0 3.0 +3 3.0 2.0 1.0 +``` + +表 5.6:排名的平局破解方法 + +| 方法 | 描述 | +| --- | --- | +| `"average"` | 默认:为相等组中的每个条目分配平均等级 | +| `"min"` | 使用整个组的最小等级 | +| `"max"` | 使用整个组的最大等级 | +| `"first"` | 按数据中值出现的顺序分配等级 | +| `"dense"` | 类似于`method="min"`,但等级总是在组之间增加 1,而不是在组中相等元素的数量之间增加 | + +### 具有重复标签的轴索引 + +到目前为止,我们看过的几乎所有示例都具有唯一的轴标签(索引值)。虽然许多 pandas 函数(如`reindex`)要求标签是唯一的,但这并非强制要求。让我们考虑一个具有重复索引的小 Series: + +```py +In [258]: obj = pd.Series(np.arange(5), index=["a", "a", "b", "b", "c"]) + +In [259]: obj +Out[259]: +a 0 +a 1 +b 2 +b 3 +c 4 +dtype: int64 +``` + +索引的`is_unique`属性可以告诉您其标签是否唯一: + +```py +In [260]: obj.index.is_unique +Out[260]: False +``` + +数据选择是与重复不同的主要行为之一。索引具有多个条目的标签返回一个 Series,而单个条目返回一个标量值: + +```py +In [261]: obj["a"] +Out[261]: +a 0 +a 1 +dtype: int64 + +In [262]: obj["c"] +Out[262]: 4 +``` + +这可能会使您的代码变得更加复杂,因为根据标签是否重复,索引的输出类型可能会有所不同。 + +相同的逻辑也适用于 DataFrame 中的行(或列)索引: + +```py +In [263]: df = pd.DataFrame(np.random.standard_normal((5, 3)), + .....: index=["a", "a", "b", "b", "c"]) + +In [264]: df +Out[264]: + 0 1 2 +a 0.274992 0.228913 1.352917 +a 0.886429 -2.001637 -0.371843 +b 1.669025 -0.438570 -0.539741 +b 0.476985 3.248944 -1.021228 +c -0.577087 0.124121 0.302614 + +In [265]: df.loc["b"] +Out[265]: + 0 1 2 +b 1.669025 -0.438570 -0.539741 +b 0.476985 3.248944 -1.021228 + +In [266]: df.loc["c"] +Out[266]: +0 -0.577087 +1 0.124121 +2 0.302614 +Name: c, dtype: float64 +``` + +## 5.3 总结和计算描述性统计 + +pandas 对象配备了一组常见的数学和统计方法。其中大多数属于*减少*或*摘要统计*的类别,这些方法从 Series 中提取单个值(如总和或均值),或者从 DataFrame 的行或列中提取一系列值。与 NumPy 数组上找到的类似方法相比,它们内置了对缺失数据的处理。考虑一个小的 DataFrame: + +```py +In [267]: df = pd.DataFrame([[1.4, np.nan], [7.1, -4.5], + .....: [np.nan, np.nan], [0.75, -1.3]], + .....: index=["a", "b", "c", "d"], + .....: columns=["one", "two"]) + +In [268]: df +Out[268]: + one two +a 1.40 NaN +b 7.10 -4.5 +c NaN NaN +d 0.75 -1.3 +``` + +调用 DataFrame 的`sum`方法会返回一个包含列和的 Series: + +```py +In [269]: df.sum() +Out[269]: +one 9.25 +two -5.80 +dtype: float64 +``` + +传递`axis="columns"`或`axis=1`会跨列求和: + +```py +In [270]: df.sum(axis="columns") +Out[270]: +a 1.40 +b 2.60 +c 0.00 +d -0.55 +dtype: float64 +``` + +当整行或整列包含所有 NA 值时,总和为 0,而如果任何值不是 NA,则结果为 NA。可以使用`skipna`选项禁用此功能,在这种情况下,行或列中的任何 NA 值都会使相应的结果为 NA: + +```py +In [271]: df.sum(axis="index", skipna=False) +Out[271]: +one NaN +two NaN +dtype: float64 + +In [272]: df.sum(axis="columns", skipna=False) +Out[272]: +a NaN +b 2.60 +c NaN +d -0.55 +dtype: float64 +``` + +一些聚合,如`mean`,需要至少一个非 NA 值才能产生一个值结果,因此我们有: + +```py +In [273]: df.mean(axis="columns") +Out[273]: +a 1.400 +b 1.300 +c NaN +d -0.275 +dtype: float64 +``` + +请参见表 5.7 以获取每种减少方法的常见选项列表。 + +表 5.7:减少方法的选项 + +| 方法 | 描述 | +| --- | --- | +| `axis` | 要减少的轴;DataFrame 的行为“index”,列为“columns” | +| `skipna` | 排除缺失值;默认为`True` | +| `level` | 如果轴是分层索引(MultiIndex),则按级别减少 | + +一些方法,如`idxmin`和`idxmax`,返回间接统计信息,如达到最小值或最大值的索引值: + +```py +In [274]: df.idxmax() +Out[274]: +one b +two d +dtype: object +``` + +其他方法是*累积*: + +```py +In [275]: df.cumsum() +Out[275]: + one two +a 1.40 NaN +b 8.50 -4.5 +c NaN NaN +d 9.25 -5.8 +``` + +一些方法既不是减少也不是累积。`describe`就是一个例子,一次生成多个摘要统计信息: + +```py +In [276]: df.describe() +Out[276]: + one two +count 3.000000 2.000000 +mean 3.083333 -2.900000 +std 3.493685 2.262742 +min 0.750000 -4.500000 +25% 1.075000 -3.700000 +50% 1.400000 -2.900000 +75% 4.250000 -2.100000 +max 7.100000 -1.300000 +``` + +对于非数字数据,`describe`会生成替代的摘要统计信息: + +```py +In [277]: obj = pd.Series(["a", "a", "b", "c"] * 4) + +In [278]: obj.describe() +Out[278]: +count 16 +unique 3 +top a +freq 8 +dtype: object +``` + +请参见表 5.8 以获取摘要统计和相关方法的完整列表。 + +表 5.8:描述性和摘要统计 + +| 方法 | 描述 | +| --- | --- | +| `count` | 非 NA 值的数量 | +| `describe` | 计算一组摘要统计信息 | +| `min, max` | 计算最小值和最大值 | +| `argmin, argmax` | 计算获得最小值或最大值的索引位置(整数),分别;在 DataFrame 对象上不可用 | +| `idxmin, idxmax` | 计算获得最小值或最大值的索引标签 | +| `quantile` | 计算从 0 到 1 范围的样本分位数(默认值:0.5) | +| `sum` | 值的总和 | +| `mean` | 值的均值 | +| `median` | 值的算术中位数(50%分位数) | +| `mad` | 与均值的平均绝对偏差 | +| `prod` | 所有值的乘积 | +| `var` | 值的样本方差 | +| `std` | 值的样本标准差 | +| `skew` | 值的样本偏度(第三时刻) | +| `kurt` | 值的样本峰度(第四时刻) | +| `cumsum` | 值的累积和 | +| `cummin, cummax` | 值的累积最小值或最大值,分别 | +| `cumprod` | 值的累积乘积 | +| `diff` | 计算第一个算术差异(对时间序列有用) | +| `pct_change` | 计算百分比变化 | + +### 相关性和协方差 + +一些摘要统计信息,如相关性和协方差,是从一对参数计算得出的。让我们考虑一些股票价格和成交量的 DataFrame,最初从 Yahoo! Finance 获取,并在本书的附带数据集中以二进制 Python pickle 文件的形式提供: + +```py +In [279]: price = pd.read_pickle("examples/yahoo_price.pkl") + +In [280]: volume = pd.read_pickle("examples/yahoo_volume.pkl") +``` + +现在我计算价格的百分比变化,这是一个时间序列操作,将在第十一章:时间序列中进一步探讨: + +```py +In [281]: returns = price.pct_change() + +In [282]: returns.tail() +Out[282]: + AAPL GOOG IBM MSFT +Date +2016-10-17 -0.000680 0.001837 0.002072 -0.003483 +2016-10-18 -0.000681 0.019616 -0.026168 0.007690 +2016-10-19 -0.002979 0.007846 0.003583 -0.002255 +2016-10-20 -0.000512 -0.005652 0.001719 -0.004867 +2016-10-21 -0.003930 0.003011 -0.012474 0.042096 +``` + +Series 的`corr`方法计算两个 Series 中重叠的、非 NA、按索引对齐的值的相关性。相关地,`cov`计算协方差: + +```py +In [283]: returns["MSFT"].corr(returns["IBM"]) +Out[283]: 0.49976361144151166 + +In [284]: returns["MSFT"].cov(returns["IBM"]) +Out[284]: 8.870655479703549e-05 +``` + +另一方面,DataFrame 的`corr`和`cov`方法分别返回完整的相关性或协方差矩阵作为 DataFrame: + +```py +In [285]: returns.corr() +Out[285]: + AAPL GOOG IBM MSFT +AAPL 1.000000 0.407919 0.386817 0.389695 +GOOG 0.407919 1.000000 0.405099 0.465919 +IBM 0.386817 0.405099 1.000000 0.499764 +MSFT 0.389695 0.465919 0.499764 1.000000 + +In [286]: returns.cov() +Out[286]: + AAPL GOOG IBM MSFT +AAPL 0.000277 0.000107 0.000078 0.000095 +GOOG 0.000107 0.000251 0.000078 0.000108 +IBM 0.000078 0.000078 0.000146 0.000089 +MSFT 0.000095 0.000108 0.000089 0.000215 +``` + +使用 DataFrame 的`corrwith`方法,您可以计算 DataFrame 的列或行与另一个 Series 或 DataFrame 之间的成对相关性。传递一个 Series 会返回一个 Series,其中计算了每列的相关值: + +```py +In [287]: returns.corrwith(returns["IBM"]) +Out[287]: +AAPL 0.386817 +GOOG 0.405099 +IBM 1.000000 +MSFT 0.499764 +dtype: float64 +``` + +传递一个 DataFrame 会计算匹配列名的相关性。在这里,我计算了百分比变化与成交量的相关性: + +```py +In [288]: returns.corrwith(volume) +Out[288]: +AAPL -0.075565 +GOOG -0.007067 +IBM -0.204849 +MSFT -0.092950 +dtype: float64 +``` + +传递`axis="columns"`会逐行执行操作。在所有情况下,在计算相关性之前,数据点都会按标签对齐。 + +### 唯一值、值计数和成员资格 + +另一类相关方法提取一维 Series 中包含的值的信息。为了说明这些方法,考虑以下示例: + +```py +In [289]: obj = pd.Series(["c", "a", "d", "a", "a", "b", "b", "c", "c"]) +``` + +第一个函数是`unique`,它为您提供 Series 中唯一值的数组: + +```py +In [290]: uniques = obj.unique() + +In [291]: uniques +Out[291]: array(['c', 'a', 'd', 'b'], dtype=object) +``` + +唯一的值不一定按它们首次出现的顺序返回,也不按排序顺序返回,但如果需要的话可以在之后排序(`uniques.sort()`)。相关地,`value_counts`计算包含值频率的 Series: + +```py +In [292]: obj.value_counts() +Out[292]: +c 3 +a 3 +b 2 +d 1 +Name: count, dtype: int64 +``` + +Series 按值降序排序以方便起见。`value_counts`也作为顶级 pandas 方法可用,可与 NumPy 数组或其他 Python 序列一起使用: + +```py +In [293]: pd.value_counts(obj.to_numpy(), sort=False) +Out[293]: +c 3 +a 3 +d 1 +b 2 +Name: count, dtype: int64 +``` + +`isin`执行矢量化的成员检查,并且在将数据集过滤到 Series 或 DataFrame 中的值子集时可能很有用: + +```py +In [294]: obj +Out[294]: +0 c +1 a +2 d +3 a +4 a +5 b +6 b +7 c +8 c +dtype: object + +In [295]: mask = obj.isin(["b", "c"]) + +In [296]: mask +Out[296]: +0 True +1 False +2 False +3 False +4 False +5 True +6 True +7 True +8 True +dtype: bool + +In [297]: obj[mask] +Out[297]: +0 c +5 b +6 b +7 c +8 c +dtype: object +``` + +与`isin`相关的是`Index.get_indexer`方法,它从可能不同的值的数组中为另一个不同值的数组提供索引数组: + +```py +In [298]: to_match = pd.Series(["c", "a", "b", "b", "c", "a"]) + +In [299]: unique_vals = pd.Series(["c", "b", "a"]) + +In [300]: indices = pd.Index(unique_vals).get_indexer(to_match) + +In [301]: indices +Out[301]: array([0, 2, 1, 1, 0, 2]) +``` + +有关这些方法的参考,请参见表 5.9。 + +表 5.9:唯一值、值计数和成员资格方法 + +| 方法 | 描述 | +| --- | --- | +| `isin` | 计算一个布尔数组,指示每个 Series 或 DataFrame 值是否包含在传递的值序列中 | +| `get_indexer` | 为数组中的每个值计算整数索引,以便将其对齐到另一个不同值的数组;有助于数据对齐和连接类型操作 | +| `unique` | 计算 Series 中唯一值的数组,按观察顺序返回 | +| `value_counts` | 返回一个 Series,其唯一值作为索引,频率作为值,按降序计数排序 | + +在某些情况下,您可能希望在 DataFrame 中的多个相关列上计算直方图。以下是一个示例: + +```py +In [302]: data = pd.DataFrame({"Qu1": [1, 3, 4, 3, 4], + .....: "Qu2": [2, 3, 1, 2, 3], + .....: "Qu3": [1, 5, 2, 4, 4]}) + +In [303]: data +Out[303]: + Qu1 Qu2 Qu3 +0 1 2 1 +1 3 3 5 +2 4 1 2 +3 3 2 4 +4 4 3 4 +``` + +我们可以计算单列的值计数,如下所示: + +```py +In [304]: data["Qu1"].value_counts().sort_index() +Out[304]: +Qu1 +1 1 +3 2 +4 2 +Name: count, dtype: int64 +``` + +要为所有列计算此值,请将`pandas.value_counts`传递给 DataFrame 的`apply`方法: + +```py +In [305]: result = data.apply(pd.value_counts).fillna(0) + +In [306]: result +Out[306]: + Qu1 Qu2 Qu3 +1 1.0 1.0 1.0 +2 0.0 2.0 1.0 +3 2.0 2.0 0.0 +4 2.0 0.0 2.0 +5 0.0 0.0 1.0 +``` + +在这里,结果中的行标签是所有列中出现的不同值。这些值是每列中这些值的相应计数。 + +还有一个`DataFrame.value_counts`方法,但它计算考虑 DataFrame 的每一行作为元组的计数,以确定每个不同行的出现次数: + +```py +In [307]: data = pd.DataFrame({"a": [1, 1, 1, 2, 2], "b": [0, 0, 1, 0, 0]}) + +In [308]: data +Out[308]: + a b +0 1 0 +1 1 0 +2 1 1 +3 2 0 +4 2 0 + +In [309]: data.value_counts() +Out[309]: +a b +1 0 2 +2 0 2 +1 1 1 +Name: count, dtype: int64 +``` + +在这种情况下,结果具有一个表示不同行的索引作为层次索引,这是我们将在第八章:数据整理:连接、合并和重塑中更详细地探讨的一个主题。 + +## 5.4 结论 + +在下一章中,我们将讨论使用 pandas 读取(或*加载*)和写入数据集的工具。之后,我们将深入探讨使用 pandas 进行数据清洗、整理、分析和可视化的工具。 diff --git a/translations/cn/pyda3e_09.md b/translations/cn/pyda3e_09.md new file mode 100644 index 000000000..b93f80d4d --- /dev/null +++ b/translations/cn/pyda3e_09.md @@ -0,0 +1,1308 @@ +# 六、数据加载、存储和文件格式 + +> 原文:[`wesmckinney.com/book/accessing-data`](https://wesmckinney.com/book/accessing-data) +> +> 译者:[飞龙](https://github.com/wizardforcel) +> +> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) + + +> 此开放访问网络版本的《Python 数据分析第三版》现已作为[印刷版和数字版](https://amzn.to/3DyLaJc)的伴侣提供。如果您发现任何勘误,请[在此处报告](https://oreilly.com/catalog/0636920519829/errata)。请注意,由 Quarto 生成的本站点的某些方面与 O'Reilly 的印刷版和电子书版本的格式不同。 +> +> 如果您发现本书的在线版本有用,请考虑[订购纸质版](https://amzn.to/3DyLaJc)或[无 DRM 的电子书](https://www.ebooks.com/en-us/book/210644288/python-for-data-analysis/wes-mckinney/?affId=WES398681F)以支持作者。本网站的内容不得复制或再生产。代码示例采用 MIT 许可,可在 GitHub 或 Gitee 上找到。 + + 读取数据并使其可访问(通常称为*数据加载*)是使用本书中大多数工具的必要第一步。术语*解析*有时也用于描述加载文本数据并将其解释为表格和不同数据类型。我将专注于使用 pandas 进行数据输入和输出,尽管其他库中有许多工具可帮助读取和写入各种格式的数据。 + +输入和输出通常分为几个主要类别:读取文本文件和其他更高效的磁盘格式、从数据库加载数据以及与网络源(如 Web API)交互。 + +## 6.1 以文本格式读取和写入数据 + +pandas 提供了许多函数,用于将表格数据读取为 DataFrame 对象。表 6.1 总结了其中一些;`pandas.read_csv`是本书中最常用的之一。我们将在二进制数据格式中稍后查看二进制数据格式。 + +表 6.1:pandas 中的文本和二进制数据加载函数 + +| 函数 | 描述 | +| --- | --- | +| `read_csv` | 从文件、URL 或类似文件的对象中加载分隔数据;使用逗号作为默认分隔符 | +| `read_fwf` | 以固定宽度列格式读取数据(即没有分隔符) | +| `read_clipboard` | 读取剪贴板中的数据的`read_csv`变体;用于将网页上的表格转换的有用工具 | +| `read_excel` | 从 Excel XLS 或 XLSX 文件中读取表格数据 | +| `read_hdf` | 读取 pandas 写入的 HDF5 文件 | +| `read_html` | 读取给定 HTML 文档中找到的所有表格 | +| `read_json` | 从 JSON(JavaScript 对象表示)字符串表示、文件、URL 或类似文件的对象中读取数据 | +| `read_feather` | 读取 Feather 二进制文件格式 | +| `read_orc` | 读取 Apache ORC 二进制文件格式 | +| `read_parquet` | 读取 Apache Parquet 二进制文件格式 | +| `read_pickle` | 使用 Python pickle 格式读取由 pandas 存储的对象 | +| `read_sas` | 读取存储在 SAS 系统的自定义存储格式之一中的 SAS 数据集 | +| `read_spss` | 读取由 SPSS 创建的数据文件 | +| `read_sql` | 读取 SQL 查询的结果(使用 SQLAlchemy) | +| `read_sql_table` | 读取整个 SQL 表(使用 SQLAlchemy);等同于使用选择该表中的所有内容的查询使用`read_sql` | +| `read_stata` | 从 Stata 文件格式中读取数据集 | +| `read_xml` | 从 XML 文件中读取数据表 | + +我将概述这些函数的机制,这些函数旨在将文本数据转换为 DataFrame。这些函数的可选参数可能属于几个类别: + +索引 + +可以将一个或多个列视为返回的 DataFrame,并确定是否从文件、您提供的参数或根本不获取列名。 + +类型推断和数据转换 + +包括用户定义的值转换和自定义缺失值标记列表。 + +日期和时间解析 + +包括一种组合能力,包括将分布在多个列中的日期和时间信息组合成结果中的单个列。 + +迭代 + +支持迭代处理非常大文件的块。 + +不干净的数据问题 + +包括跳过行或页脚、注释或其他像数字数据以逗号分隔的小事物。 + +由于现实世界中的数据可能会很混乱,一些数据加载函数(特别是`pandas.read_csv`)随着时间的推移积累了很长的可选参数列表。对于不同参数的数量感到不知所措是正常的(`pandas.read_csv`大约有 50 个)。在线 pandas 文档有许多关于每个参数如何工作的示例,因此如果您在阅读特定文件时感到困惑,可能会有足够相似的示例帮助您找到正确的参数。 + +其中一些函数执行*类型推断*,因为列数据类型不是数据格式的一部分。这意味着您不一定需要指定哪些列是数字、整数、布尔值或字符串。其他数据格式,如 HDF5、ORC 和 Parquet,将数据类型信息嵌入到格式中。 + +处理日期和其他自定义类型可能需要额外的努力。 + +让我们从一个小的逗号分隔值(CSV)文本文件开始: + +```py +In [10]: !cat examples/ex1.csv +a,b,c,d,message +1,2,3,4,hello +5,6,7,8,world +9,10,11,12,foo +``` + +注意 + +这里我使用了 Unix 的`cat` shell 命令将文件的原始内容打印到屏幕上。如果您使用 Windows,可以在 Windows 终端(或命令行)中使用`type`代替`cat`来实现相同的效果。 + +由于这是逗号分隔的,我们可以使用`pandas.read_csv`将其读入 DataFrame: + +```py +In [11]: df = pd.read_csv("examples/ex1.csv") + +In [12]: df +Out[12]: + a b c d message +0 1 2 3 4 hello +1 5 6 7 8 world +2 9 10 11 12 foo +``` + +文件不总是有标题行。考虑这个文件: + +```py +In [13]: !cat examples/ex2.csv +1,2,3,4,hello +5,6,7,8,world +9,10,11,12,foo +``` + +要读取此文件,您有几个选项。您可以允许 pandas 分配默认列名,或者您可以自己指定名称: + +```py +In [14]: pd.read_csv("examples/ex2.csv", header=None) +Out[14]: + 0 1 2 3 4 +0 1 2 3 4 hello +1 5 6 7 8 world +2 9 10 11 12 foo + +In [15]: pd.read_csv("examples/ex2.csv", names=["a", "b", "c", "d", "message"]) +Out[15]: + a b c d message +0 1 2 3 4 hello +1 5 6 7 8 world +2 9 10 11 12 foo +``` + +假设您希望`message`列成为返回的 DataFrame 的索引。您可以使用`index_col`参数指示您希望在索引 4 处或使用名称`"message"`: + +```py +In [16]: names = ["a", "b", "c", "d", "message"] + +In [17]: pd.read_csv("examples/ex2.csv", names=names, index_col="message") +Out[17]: + a b c d +message +hello 1 2 3 4 +world 5 6 7 8 +foo 9 10 11 12 +``` + +如果要从多个列创建分层索引(在 Ch 8.1:分层索引中讨论),请传递列编号或名称的列表: + +```py +In [18]: !cat examples/csv_mindex.csv +key1,key2,value1,value2 +one,a,1,2 +one,b,3,4 +one,c,5,6 +one,d,7,8 +two,a,9,10 +two,b,11,12 +two,c,13,14 +two,d,15,16 + +In [19]: parsed = pd.read_csv("examples/csv_mindex.csv", + ....: index_col=["key1", "key2"]) + +In [20]: parsed +Out[20]: + value1 value2 +key1 key2 +one a 1 2 + b 3 4 + c 5 6 + d 7 8 +two a 9 10 + b 11 12 + c 13 14 + d 15 16 +``` + +在某些情况下,表格可能没有固定的分隔符,而是使用空格或其他模式来分隔字段。考虑一个看起来像这样的文本文件: + +```py +In [21]: !cat examples/ex3.txt +A B C +aaa -0.264438 -1.026059 -0.619500 +bbb 0.927272 0.302904 -0.032399 +ccc -0.264273 -0.386314 -0.217601 +ddd -0.871858 -0.348382 1.100491 +``` + +虽然您可以手动进行一些数据处理,但这里的字段是由可变数量的空格分隔的。在这些情况下,您可以将正则表达式作为`pandas.read_csv`的分隔符传递。这可以通过正则表达式`\s+`表示,因此我们有: + +```py +In [22]: result = pd.read_csv("examples/ex3.txt", sep="\s+") + +In [23]: result +Out[23]: + A B C +aaa -0.264438 -1.026059 -0.619500 +bbb 0.927272 0.302904 -0.032399 +ccc -0.264273 -0.386314 -0.217601 +ddd -0.871858 -0.348382 1.100491 +``` + +由于列名比数据行数少一个,`pandas.read_csv`推断在这种特殊情况下第一列应该是 DataFrame 的索引。 + +文件解析函数有许多额外的参数,可帮助您处理发生的各种异常文件格式(请参见表 6.2 中的部分列表)。例如,您可以使用`skiprows`跳过文件的第一、第三和第四行: + +```py +In [24]: !cat examples/ex4.csv +# hey! +a,b,c,d,message +# just wanted to make things more difficult for you +# who reads CSV files with computers, anyway? +1,2,3,4,hello +5,6,7,8,world +9,10,11,12,foo + +In [25]: pd.read_csv("examples/ex4.csv", skiprows=[0, 2, 3]) +Out[25]: + a b c d message +0 1 2 3 4 hello +1 5 6 7 8 world +2 9 10 11 12 foo +``` + +处理缺失值是文件读取过程中重要且经常微妙的部分。缺失数据通常要么不存在(空字符串),要么由某个*标记*(占位符)值标记。默认情况下,pandas 使用一组常见的标记,例如`NA`和`NULL`: + +```py +In [26]: !cat examples/ex5.csv +something,a,b,c,d,message +one,1,2,3,4,NA +two,5,6,,8,world +three,9,10,11,12,foo +In [27]: result = pd.read_csv("examples/ex5.csv") + +In [28]: result +Out[28]: + something a b c d message +0 one 1 2 3.0 4 NaN +1 two 5 6 NaN 8 world +2 three 9 10 11.0 12 foo +``` + +请记住,pandas 将缺失值输出为`NaN`,因此在`result`中有两个空值或缺失值: + +```py +In [29]: pd.isna(result) +Out[29]: + something a b c d message +0 False False False False False True +1 False False False True False False +2 False False False False False False +``` + +`na_values`选项接受一个字符串序列,用于添加到默认识别为缺失的字符串列表中: + +```py +In [30]: result = pd.read_csv("examples/ex5.csv", na_values=["NULL"]) + +In [31]: result +Out[31]: + something a b c d message +0 one 1 2 3.0 4 NaN +1 two 5 6 NaN 8 world +2 three 9 10 11.0 12 foo +``` + +`pandas.read_csv`有许多默认的 NA 值表示列表,但这些默认值可以通过`keep_default_na`选项禁用: + +```py +In [32]: result2 = pd.read_csv("examples/ex5.csv", keep_default_na=False) + +In [33]: result2 +Out[33]: + something a b c d message +0 one 1 2 3 4 NA +1 two 5 6 8 world +2 three 9 10 11 12 foo + +In [34]: result2.isna() +Out[34]: + something a b c d message +0 False False False False False False +1 False False False False False False +2 False False False False False False + +In [35]: result3 = pd.read_csv("examples/ex5.csv", keep_default_na=False, + ....: na_values=["NA"]) + +In [36]: result3 +Out[36]: + something a b c d message +0 one 1 2 3 4 NaN +1 two 5 6 8 world +2 three 9 10 11 12 foo + +In [37]: result3.isna() +Out[37]: + something a b c d message +0 False False False False False True +1 False False False False False False +2 False False False False False False +``` + +可以在字典中为每列指定不同的 NA 标记: + +```py +In [38]: sentinels = {"message": ["foo", "NA"], "something": ["two"]} + +In [39]: pd.read_csv("examples/ex5.csv", na_values=sentinels, + ....: keep_default_na=False) +Out[39]: + something a b c d message +0 one 1 2 3 4 NaN +1 NaN 5 6 8 world +2 three 9 10 11 12 NaN +``` + +表 6.2 列出了`pandas.read_csv`中一些经常使用的选项。 + +表 6.2:一些`pandas.read_csv`函数参数 + +| 参数 | 描述 | +| --- | --- | +| `path` | 指示文件系统位置、URL 或类似文件的字符串。 | +| `sep`或`delimiter` | 用于在每行中拆分字段的字符序列或正则表达式。 | +| `header` | 用作列名的行号;默认为 0(第一行),但如果没有标题行,则应为`None`。 | +| `index_col` | 用作结果中行索引的列号或名称;可以是单个名称/编号或用于分层索引的列表。 | +| `names` | 结果的列名列表。 | +| `skiprows` | 要忽略的文件开头的行数或要跳过的行号列表(从 0 开始)。 | +| `na_values` | 要替换为 NA 的值序列。除非传递`keep_default_na=False`,否则它们将添加到默认列表中。 | +| `keep_default_na` | 是否使用默认的 NA 值列表(默认为`True`)。 | +| `comment` | 用于将注释从行末分隔出来的字符。 | +| `parse_dates` | 尝试解析数据为`datetime`;默认为`False`。如果为`True`,将尝试解析所有列。否则,可以指定要解析的列号或名称的列表。如果列表的元素是元组或列表,则将多个列组合在一起并解析为日期(例如,如果日期/时间跨越两列)。 | +| `keep_date_col` | 如果连接列以解析日期,则保留连接的列;默认为`False`。 | +| `converters` | 包含列号或名称映射到函数的字典(例如,`{"foo": f}`将对`"foo"`列中的所有值应用函数`f`)。 | +| `dayfirst` | 在解析可能模糊的日期时,将其视为国际格式(例如,7/6/2012 -> 2012 年 6 月 7 日);默认为`False`。 | +| `date_parser` | 用于解析日期的函数。 | +| `nrows` | 从文件开头读取的行数(不包括标题)。 | +| `iterator` | 返回一个用于逐步读取文件的`TextFileReader`对象。此对象也可以与`with`语句一起使用。 | +| `chunksize` | 用于迭代的文件块的大小。 | +| `skip_footer` | 要忽略的文件末尾行数。 | +| `verbose` | 打印各种解析信息,如文件转换各阶段所花费的时间和内存使用信息。 | +| `encoding` | 文本编码(例如,UTF-8 编码文本的`"utf-8"`)。如果为`None`,默认为`"utf-8"`。 | +| `squeeze` | 如果解析的数据只包含一列,则返回一个 Series。 | +| `thousands` | 千位分隔符(例如,`","`或`"."`);默认为`None`。 | +| `decimal` | 数字中的小数分隔符(例如,`"."`或`","`);默认为`"."`。 | +| `engine` | 要使用的 CSV 解析和转换引擎;可以是`"c"`、`"python"`或`"pyarrow"`之一。默认为`"c"`,尽管较新的`"pyarrow"`引擎可以更快地解析一些文件。`"python"`引擎速度较慢,但支持其他引擎不支持的一些功能。 | + +### 分块读取文本文件 + +在处理非常大的文件或找出正确的参数集以正确处理大文件时,您可能只想读取文件的一小部分或迭代文件的较小块。 + +在查看大文件之前,我们将 pandas 显示设置更加紧凑: + +```py +In [40]: pd.options.display.max_rows = 10 +``` + +现在我们有: + +```py +In [41]: result = pd.read_csv("examples/ex6.csv") + +In [42]: result +Out[42]: + one two three four key +0 0.467976 -0.038649 -0.295344 -1.824726 L +1 -0.358893 1.404453 0.704965 -0.200638 B +2 -0.501840 0.659254 -0.421691 -0.057688 G +3 0.204886 1.074134 1.388361 -0.982404 R +4 0.354628 -0.133116 0.283763 -0.837063 Q +... ... ... ... ... .. +9995 2.311896 -0.417070 -1.409599 -0.515821 L +9996 -0.479893 -0.650419 0.745152 -0.646038 E +9997 0.523331 0.787112 0.486066 1.093156 K +9998 -0.362559 0.598894 -1.843201 0.887292 G +9999 -0.096376 -1.012999 -0.657431 -0.573315 0 +[10000 rows x 5 columns] +``` + +省略号`...`表示已省略数据框中间的行。 + +如果您只想读取少量行(避免读取整个文件),请使用`nrows`指定: + +```py +In [43]: pd.read_csv("examples/ex6.csv", nrows=5) +Out[43]: + one two three four key +0 0.467976 -0.038649 -0.295344 -1.824726 L +1 -0.358893 1.404453 0.704965 -0.200638 B +2 -0.501840 0.659254 -0.421691 -0.057688 G +3 0.204886 1.074134 1.388361 -0.982404 R +4 0.354628 -0.133116 0.283763 -0.837063 Q +``` + +要分块读取文件,指定一个作为行数的`chunksize`: + +```py +In [44]: chunker = pd.read_csv("examples/ex6.csv", chunksize=1000) + +In [45]: type(chunker) +Out[45]: pandas.io.parsers.readers.TextFileReader +``` + +由`pandas.read_csv`返回的`TextFileReader`对象允许您根据`chunksize`迭代文件的部分。例如,我们可以迭代`ex6.csv`,聚合`"key"`列中的值计数,如下所示: + +```py +chunker = pd.read_csv("examples/ex6.csv", chunksize=1000) + +tot = pd.Series([], dtype='int64') +for piece in chunker: + tot = tot.add(piece["key"].value_counts(), fill_value=0) + +tot = tot.sort_values(ascending=False) +``` + +然后我们有: + +```py +In [47]: tot[:10] +Out[47]: +key +E 368.0 +X 364.0 +L 346.0 +O 343.0 +Q 340.0 +M 338.0 +J 337.0 +F 335.0 +K 334.0 +H 330.0 +dtype: float64 +``` + +`TextFileReader`还配备有一个`get_chunk`方法,使您能够以任意大小读取文件的片段。 + +### 将数据写入文本格式 + +数据也可以导出为分隔格式。让我们考虑之前读取的一个 CSV 文件: + +```py +In [48]: data = pd.read_csv("examples/ex5.csv") + +In [49]: data +Out[49]: + something a b c d message +0 one 1 2 3.0 4 NaN +1 two 5 6 NaN 8 world +2 three 9 10 11.0 12 foo +``` + +使用 DataFrame 的 `to_csv` 方法,我们可以将数据写入逗号分隔的文件: + +```py +In [50]: data.to_csv("examples/out.csv") + +In [51]: !cat examples/out.csv +,something,a,b,c,d,message +0,one,1,2,3.0,4, +1,two,5,6,,8,world +2,three,9,10,11.0,12,foo +``` + +当然也可以使用其他分隔符(写入到 `sys.stdout` 以便将文本结果打印到控制台而不是文件): + +```py +In [52]: import sys + +In [53]: data.to_csv(sys.stdout, sep="|") +|something|a|b|c|d|message +0|one|1|2|3.0|4| +1|two|5|6||8|world +2|three|9|10|11.0|12|foo +``` + +缺失值在输出中显示为空字符串。您可能希望用其他标记值来表示它们: + +```py +In [54]: data.to_csv(sys.stdout, na_rep="NULL") +,something,a,b,c,d,message +0,one,1,2,3.0,4,NULL +1,two,5,6,NULL,8,world +2,three,9,10,11.0,12,foo +``` + +如果未指定其他选项,则将同时写入行标签和列标签。这两者都可以禁用: + +```py +In [55]: data.to_csv(sys.stdout, index=False, header=False) +one,1,2,3.0,4, +two,5,6,,8,world +three,9,10,11.0,12,foo +``` + +您还可以仅写入列的子集,并按您选择的顺序进行写入: + +```py +In [56]: data.to_csv(sys.stdout, index=False, columns=["a", "b", "c"]) +a,b,c +1,2,3.0 +5,6, +9,10,11.0 +``` + +### 处理其他分隔格式 + +使用函数如 `pandas.read_csv` 可以从磁盘加载大多数形式的表格数据。然而,在某些情况下,可能需要一些手动处理。接收到一个或多个格式错误的行可能会导致 `pandas.read_csv` 出错。为了说明基本工具,考虑一个小的 CSV 文件: + +```py +In [57]: !cat examples/ex7.csv +"a","b","c" +"1","2","3" +"1","2","3" +``` + +对于任何具有单字符分隔符的文件,您可以使用 Python 的内置 `csv` 模块。要使用它,将任何打开的文件或类似文件的对象传递给 `csv.reader`: + +```py +In [58]: import csv + +In [59]: f = open("examples/ex7.csv") + +In [60]: reader = csv.reader(f) +``` + +像处理文件一样迭代读取器会产生去除任何引号字符的值列表: + +```py +In [61]: for line in reader: + ....: print(line) +['a', 'b', 'c'] +['1', '2', '3'] +['1', '2', '3'] + +In [62]: f.close() +``` + +然后,您需要进行必要的整理以将数据放入所需的形式。让我们一步一步来。首先,我们将文件读取为行列表: + +```py +In [63]: with open("examples/ex7.csv") as f: + ....: lines = list(csv.reader(f)) +``` + +然后我们将行分割为标题行和数据行: + +```py +In [64]: header, values = lines[0], lines[1:] +``` + +然后我们可以使用字典推导和表达式 `zip(*values)` 创建数据列的字典(请注意,这将在大文件上使用大量内存),将行转置为列: + +```py +In [65]: data_dict = {h: v for h, v in zip(header, zip(*values))} + +In [66]: data_dict +Out[66]: {'a': ('1', '1'), 'b': ('2', '2'), 'c': ('3', '3')} +``` + +CSV 文件有许多不同的风格。要定义一个具有不同分隔符、字符串引用约定或行终止符的新格式,我们可以定义一个简单的 `csv.Dialect` 的子类: + +```py +class my_dialect(csv.Dialect): + lineterminator = "\n" + delimiter = ";" + quotechar = '"' + quoting = csv.QUOTE_MINIMAL +``` + +```py +reader = csv.reader(f, dialect=my_dialect) +``` + +我们还可以将单独的 CSV 方言参数作为关键字传递给 `csv.reader`,而无需定义子类: + +```py +reader = csv.reader(f, delimiter="|") +``` + +可能的选项(`csv.Dialect` 的属性)及其作用可以在 表 6.3 中找到。 + +表 6.3: CSV `dialect` 选项 + +| 参数 | 描述 | +| --- | --- | +| `delimiter` | 用于分隔字段的单字符字符串;默认为 `","`。 | +| `lineterminator` | 用于写入的行终止符;默认为 `"\r\n"`。读取器会忽略这个并识别跨平台的行终止符。 | +| `quotechar` | 用于具有特殊字符(如分隔符)的字段的引用字符;默认为 `'"'`。 | +| `quoting` | 引用约定。选项包括 `csv.QUOTE_ALL`(引用所有字段)、`csv.QUOTE_MINIMAL`(只有包含特殊字符如分隔符的字段)、`csv.QUOTE_NONNUMERIC` 和 `csv.QUOTE_NONE`(不引用)。详细信息请参阅 Python 的文档。默认为 `QUOTE_MINIMAL`。 | +| `skipinitialspace` | 忽略每个分隔符后的空格;默认为 `False`。 | +| `doublequote` | 如何处理字段内的引用字符;如果为 `True`,则会加倍(请查看在线文档以获取完整的详细信息和行为)。 | +| `escapechar` | 如果 `quoting` 设置为 `csv.QUOTE_NONE`,用于转义分隔符的字符串;默认情况下禁用。 | + +注意 + +对于具有更复杂或固定多字符分隔符的文件,您将无法使用 `csv` 模块。在这些情况下,您将需要使用字符串的 `split` 方法或正则表达式方法 `re.split` 进行行分割和其他清理。幸运的是,如果传递必要的选项,`pandas.read_csv` 能够几乎做任何您需要的事情,因此您很少需要手动解析文件。 + +要 *手动* 写入分隔文件,可以使用 `csv.writer`。它接受一个打开的可写文件对象以及与 `csv.reader` 相同的方言和格式选项: + +```py +with open("mydata.csv", "w") as f: + writer = csv.writer(f, dialect=my_dialect) + writer.writerow(("one", "two", "three")) + writer.writerow(("1", "2", "3")) + writer.writerow(("4", "5", "6")) + writer.writerow(("7", "8", "9")) +``` + +### JSON 数据 + +JSON(JavaScript 对象表示法的缩写)已经成为在 Web 浏览器和其他应用程序之间通过 HTTP 请求发送数据的标准格式之一。它是比 CSV 等表格文本形式更自由的数据格式。这里是一个例子: + +```py +obj = """ +{"name": "Wes", + "cities_lived": ["Akron", "Nashville", "New York", "San Francisco"], + "pet": null, + "siblings": [{"name": "Scott", "age": 34, "hobbies": ["guitars", "soccer"]}, + {"name": "Katie", "age": 42, "hobbies": ["diving", "art"]}] +} +""" +``` + +JSON 几乎是有效的 Python 代码,只是其空值`null`和一些其他细微差别(例如不允许在列表末尾使用逗号)。基本类型是对象(字典)、数组(列表)、字符串、数字、布尔值和空值。对象中的所有键都必须是字符串。有几个 Python 库可用于读取和写入 JSON 数据。我将在这里使用`json`,因为它内置在 Python 标准库中。要将 JSON 字符串转换为 Python 形式,请使用`json.loads`: + +```py +In [68]: import json + +In [69]: result = json.loads(obj) + +In [70]: result +Out[70]: +{'name': 'Wes', + 'cities_lived': ['Akron', 'Nashville', 'New York', 'San Francisco'], + 'pet': None, + 'siblings': [{'name': 'Scott', + 'age': 34, + 'hobbies': ['guitars', 'soccer']}, + {'name': 'Katie', 'age': 42, 'hobbies': ['diving', 'art']}]} +``` + +`json.dumps`,另一方面,将 Python 对象转换回 JSON: + +```py +In [71]: asjson = json.dumps(result) + +In [72]: asjson +Out[72]: '{"name": "Wes", "cities_lived": ["Akron", "Nashville", "New York", "San + Francisco"], "pet": null, "siblings": [{"name": "Scott", "age": 34, "hobbies": [ +"guitars", "soccer"]}, {"name": "Katie", "age": 42, "hobbies": ["diving", "art"]} +]}' +``` + +如何将 JSON 对象或对象列表转换为 DataFrame 或其他数据结构以进行分析将取决于您。方便的是,您可以将字典列表(先前是 JSON 对象)传递给 DataFrame 构造函数并选择数据字段的子集: + +```py +In [73]: siblings = pd.DataFrame(result["siblings"], columns=["name", "age"]) + +In [74]: siblings +Out[74]: + name age +0 Scott 34 +1 Katie 42 +``` + +`pandas.read_json`可以自动将特定排列的 JSON 数据集转换为 Series 或 DataFrame。例如: + +```py +In [75]: !cat examples/example.json +[{"a": 1, "b": 2, "c": 3}, + {"a": 4, "b": 5, "c": 6}, + {"a": 7, "b": 8, "c": 9}] +``` + +`pandas.read_json`的默认选项假定 JSON 数组中的每个对象是表中的一行: + +```py +In [76]: data = pd.read_json("examples/example.json") + +In [77]: data +Out[77]: + a b c +0 1 2 3 +1 4 5 6 +2 7 8 9 +``` + +有关阅读和操作 JSON 数据的扩展示例(包括嵌套记录),请参见第十三章:数据分析示例中的美国农业部食品数据库示例。 + +如果您需要将数据从 pandas 导出为 JSON,一种方法是在 Series 和 DataFrame 上使用`to_json`方法: + +```py +In [78]: data.to_json(sys.stdout) +{"a":{"0":1,"1":4,"2":7},"b":{"0":2,"1":5,"2":8},"c":{"0":3,"1":6,"2":9}} +In [79]: data.to_json(sys.stdout, orient="records") +[{"a":1,"b":2,"c":3},{"a":4,"b":5,"c":6},{"a":7,"b":8,"c":9}] +``` + +### XML 和 HTML:网络抓取 + +Python 有许多用于读取和写入 HTML 和 XML 格式数据的库。示例包括 lxml、Beautiful Soup 和 html5lib。虽然 lxml 通常在一般情况下更快,但其他库可以更好地处理格式不正确的 HTML 或 XML 文件。 + +pandas 有一个内置函数`pandas.read_html`,它使用所有这些库自动将 HTML 文件中的表格解析为 DataFrame 对象。为了展示这是如何工作的,我下载了一个 HTML 文件(在 pandas 文档中使用)从美国联邦存款保险公司显示银行倒闭。¹首先,您必须安装一些`read_html`使用的附加库: + +```py +conda install lxml beautifulsoup4 html5lib +``` + +如果您没有使用 conda,`pip install lxml`也应该可以工作。 + +`pandas.read_html`函数有许多选项,但默认情况下它会搜索并尝试解析包含在``标签中的所有表格数据。结果是一个 DataFrame 对象的列表: + +```py +In [80]: tables = pd.read_html("examples/fdic_failed_bank_list.html") + +In [81]: len(tables) +Out[81]: 1 + +In [82]: failures = tables[0] + +In [83]: failures.head() +Out[83]: + Bank Name City ST CERT +0 Allied Bank Mulberry AR 91 \ +1 The Woodbury Banking Company Woodbury GA 11297 +2 First CornerStone Bank King of Prussia PA 35312 +3 Trust Company Bank Memphis TN 9956 +4 North Milwaukee State Bank Milwaukee WI 20364 + Acquiring Institution Closing Date Updated Date +0 Today's Bank September 23, 2016 November 17, 2016 +1 United Bank August 19, 2016 November 17, 2016 +2 First-Citizens Bank & Trust Company May 6, 2016 September 6, 2016 +3 The Bank of Fayette County April 29, 2016 September 6, 2016 +4 First-Citizens Bank & Trust Company March 11, 2016 June 16, 2016 +``` + +由于`failures`有许多列,pandas 会插入一个换行符`\`。 + +正如您将在后面的章节中了解到的那样,从这里我们可以继续进行一些数据清理和分析,比如计算每年的银行倒闭次数: + +```py +In [84]: close_timestamps = pd.to_datetime(failures["Closing Date"]) + +In [85]: close_timestamps.dt.year.value_counts() +Out[85]: +Closing Date +2010 157 +2009 140 +2011 92 +2012 51 +2008 25 + ... +2004 4 +2001 4 +2007 3 +2003 3 +2000 2 +Name: count, Length: 15, dtype: int64 +``` + +#### 使用`lxml.objectify`解析 XML + +XML 是另一种常见的结构化数据格式,支持具有元数据的分层嵌套数据。您当前正在阅读的书实际上是从一系列大型 XML 文档创建的。 + +之前,我展示了`pandas.read_html`函数,它在底层使用 lxml 或 Beautiful Soup 来解析 HTML 中的数据。XML 和 HTML 在结构上相似,但 XML 更通用。在这里,我将展示如何使用 lxml 来解析更一般的 XML 格式中的数据的示例。 + +多年来,纽约大都会交通管理局(MTA)以 XML 格式发布了许多关于其公交车和火车服务的数据系列。在这里,我们将查看性能数据,这些数据包含在一组 XML 文件中。每个火车或公交车服务都有一个不同的文件(例如*Performance_MNR.xml*用于 Metro-North Railroad),其中包含作为一系列 XML 记录的月度数据,看起来像这样: + +```py + + 373889 + + Metro-North Railroad + Escalator Availability + Percent of the time that escalators are operational + systemwide. The availability rate is based on physical observations performed + the morning of regular business days only. This is a new indicator the agency + began reporting in 2009. + 2011 + 12 + Service Indicators + M + U + % + 1 + 97.00 + + 97.00 + + +``` + +使用`lxml.objectify`,我们解析文件并获取 XML 文件的根节点的引用: + +```py +In [86]: from lxml import objectify + +In [87]: path = "datasets/mta_perf/Performance_MNR.xml" + +In [88]: with open(path) as f: + ....: parsed = objectify.parse(f) + +In [89]: root = parsed.getroot() +``` + +`root.INDICATOR`返回一个生成器,产生每个`` XML 元素。对于每条记录,我们可以通过运行以下代码填充一个标签名称(如`YTD_ACTUAL`)到数据值(排除一些标签)的字典: + +```py +data = [] + +skip_fields = ["PARENT_SEQ", "INDICATOR_SEQ", + "DESIRED_CHANGE", "DECIMAL_PLACES"] + +for elt in root.INDICATOR: + el_data = {} + for child in elt.getchildren(): + if child.tag in skip_fields: + continue + el_data[child.tag] = child.pyval + data.append(el_data) +``` + +最后,将这个字典列表转换为 DataFrame: + +```py +In [91]: perf = pd.DataFrame(data) + +In [92]: perf.head() +Out[92]: + AGENCY_NAME INDICATOR_NAME +0 Metro-North Railroad On-Time Performance (West of Hudson) \ +1 Metro-North Railroad On-Time Performance (West of Hudson) +2 Metro-North Railroad On-Time Performance (West of Hudson) +3 Metro-North Railroad On-Time Performance (West of Hudson) +4 Metro-North Railroad On-Time Performance (West of Hudson) + DESCRIPTION +0 Percent of commuter trains that arrive at their destinations within 5 m... \ +1 Percent of commuter trains that arrive at their destinations within 5 m... +2 Percent of commuter trains that arrive at their destinations within 5 m... +3 Percent of commuter trains that arrive at their destinations within 5 m... +4 Percent of commuter trains that arrive at their destinations within 5 m... + PERIOD_YEAR PERIOD_MONTH CATEGORY FREQUENCY INDICATOR_UNIT +0 2008 1 Service Indicators M % \ +1 2008 2 Service Indicators M % +2 2008 3 Service Indicators M % +3 2008 4 Service Indicators M % +4 2008 5 Service Indicators M % + YTD_TARGET YTD_ACTUAL MONTHLY_TARGET MONTHLY_ACTUAL +0 95.0 96.9 95.0 96.9 +1 95.0 96.0 95.0 95.0 +2 95.0 96.3 95.0 96.9 +3 95.0 96.8 95.0 98.3 +4 95.0 96.6 95.0 95.8 +``` + +pandas 的`pandas.read_xml`函数将此过程转换为一行表达式: + +```py +In [93]: perf2 = pd.read_xml(path) + +In [94]: perf2.head() +Out[94]: + INDICATOR_SEQ PARENT_SEQ AGENCY_NAME +0 28445 NaN Metro-North Railroad \ +1 28445 NaN Metro-North Railroad +2 28445 NaN Metro-North Railroad +3 28445 NaN Metro-North Railroad +4 28445 NaN Metro-North Railroad + INDICATOR_NAME +0 On-Time Performance (West of Hudson) \ +1 On-Time Performance (West of Hudson) +2 On-Time Performance (West of Hudson) +3 On-Time Performance (West of Hudson) +4 On-Time Performance (West of Hudson) + DESCRIPTION +0 Percent of commuter trains that arrive at their destinations within 5 m... \ +1 Percent of commuter trains that arrive at their destinations within 5 m... +2 Percent of commuter trains that arrive at their destinations within 5 m... +3 Percent of commuter trains that arrive at their destinations within 5 m... +4 Percent of commuter trains that arrive at their destinations within 5 m... + PERIOD_YEAR PERIOD_MONTH CATEGORY FREQUENCY DESIRED_CHANGE +0 2008 1 Service Indicators M U \ +1 2008 2 Service Indicators M U +2 2008 3 Service Indicators M U +3 2008 4 Service Indicators M U +4 2008 5 Service Indicators M U + INDICATOR_UNIT DECIMAL_PLACES YTD_TARGET YTD_ACTUAL MONTHLY_TARGET +0 % 1 95.00 96.90 95.00 \ +1 % 1 95.00 96.00 95.00 +2 % 1 95.00 96.30 95.00 +3 % 1 95.00 96.80 95.00 +4 % 1 95.00 96.60 95.00 + MONTHLY_ACTUAL +0 96.90 +1 95.00 +2 96.90 +3 98.30 +4 95.80 +``` + +对于更复杂的 XML 文档,请参考`pandas.read_xml`的文档字符串,其中描述了如何进行选择和过滤以提取感兴趣的特定表格。 + +## 6.2 二进制数据格式 + +以二进制格式存储(或*序列化*)数据的一种简单方法是使用 Python 的内置`pickle`模块。所有 pandas 对象都有一个`to_pickle`方法,它以 pickle 格式将数据写入磁盘: + +```py +In [95]: frame = pd.read_csv("examples/ex1.csv") + +In [96]: frame +Out[96]: + a b c d message +0 1 2 3 4 hello +1 5 6 7 8 world +2 9 10 11 12 foo + +In [97]: frame.to_pickle("examples/frame_pickle") +``` + +Pickle 文件通常只能在 Python 中读取。您可以直接使用内置的`pickle`读取存储在文件中的任何“pickled”对象,或者更方便地使用`pandas.read_pickle`: + +```py +In [98]: pd.read_pickle("examples/frame_pickle") +Out[98]: + a b c d message +0 1 2 3 4 hello +1 5 6 7 8 world +2 9 10 11 12 foo +``` + +注意 + +`pickle`仅建议作为短期存储格式。问题在于很难保证格式随时间稳定;今天使用 pickle 的对象可能无法在以后的库版本中解除 pickle。pandas 在可能的情况下尽力保持向后兼容性,但在将来的某个时候可能需要“破坏”pickle 格式。 + +pandas 内置支持其他几种开源二进制数据格式,例如 HDF5、ORC 和 Apache Parquet。例如,如果安装`pyarrow`包(`conda install pyarrow`),则可以使用`pandas.read_parquet`读取 Parquet 文件: + +```py +In [100]: fec = pd.read_parquet('datasets/fec/fec.parquet') +``` + +我将在 HDF5 格式使用中给出一些 HDF5 示例。我鼓励您探索不同的文件格式,看看它们的速度和对您的分析工作的适用性。 + +### 读取 Microsoft Excel 文件 + +pandas 还支持使用`pandas.ExcelFile`类或`pandas.read_excel`函数读取存储在 Excel 2003(及更高版本)文件中的表格数据。在内部,这些工具使用附加包`xlrd`和`openpyxl`来分别读取旧式 XLS 和新式 XLSX 文件。这些必须使用 pip 或 conda 单独安装,而不是从 pandas 安装: + +```py +conda install openpyxl xlrd +``` + +要使用`pandas.ExcelFile`,请通过传递路径到`xls`或`xlsx`文件来创建一个实例: + +```py +In [101]: xlsx = pd.ExcelFile("examples/ex1.xlsx") +``` + +此对象可以显示文件中可用工作表名称的列表: + +```py +In [102]: xlsx.sheet_names +Out[102]: ['Sheet1'] +``` + +可以使用`parse`将工作表中存储的数据读入 DataFrame: + +```py +In [103]: xlsx.parse(sheet_name="Sheet1") +Out[103]: + Unnamed: 0 a b c d message +0 0 1 2 3 4 hello +1 1 5 6 7 8 world +2 2 9 10 11 12 foo +``` + +此 Excel 表具有索引列,因此我们可以使用`index_col`参数指示: + +```py +In [104]: xlsx.parse(sheet_name="Sheet1", index_col=0) +Out[104]: + a b c d message +0 1 2 3 4 hello +1 5 6 7 8 world +2 9 10 11 12 foo +``` + +如果要在一个文件中读取多个工作表,则创建`pandas.ExcelFile`会更快,但您也可以简单地将文件名传递给`pandas.read_excel`: + +```py +In [105]: frame = pd.read_excel("examples/ex1.xlsx", sheet_name="Sheet1") + +In [106]: frame +Out[106]: + Unnamed: 0 a b c d message +0 0 1 2 3 4 hello +1 1 5 6 7 8 world +2 2 9 10 11 12 foo +``` + +要将 pandas 数据写入 Excel 格式,必须首先创建一个`ExcelWriter`,然后使用 pandas 对象的`to_excel`方法将数据写入其中: + +```py +In [107]: writer = pd.ExcelWriter("examples/ex2.xlsx") + +In [108]: frame.to_excel(writer, "Sheet1") + +In [109]: writer.close() +``` + +您还可以将文件路径传递给`to_excel`,避免使用`ExcelWriter`: + +```py +In [110]: frame.to_excel("examples/ex2.xlsx") +``` + +### 使用 HDF5 格式 + +HDF5 是一种受尊敬的文件格式,用于存储大量科学数组数据。它作为一个 C 库可用,并且在许多其他语言中都有接口,包括 Java、Julia、MATLAB 和 Python。HDF5 中的“HDF”代表*分层数据格式*。每个 HDF5 文件可以存储多个数据集和支持的元数据。与更简单的格式相比,HDF5 支持各种压缩模式的即时压缩,使具有重复模式的数据能够更有效地存储。HDF5 可以是处理不适合内存的数据集的良好选择,因为您可以有效地读取和写入更大数组的小部分。 + +要开始使用 HDF5 和 pandas,您必须首先通过使用 conda 安装`tables`包来安装 PyTables: + +```py +conda install pytables +``` + +注意 + +请注意,PyTables 包在 PyPI 中称为“tables”,因此如果您使用 pip 安装,您将需要运行`pip install tables`。 + +虽然可以直接使用 PyTables 或 h5py 库访问 HDF5 文件,但 pandas 提供了一个简化存储 Series 和 DataFrame 对象的高级接口。`HDFStore`类的工作方式类似于字典,并处理底层细节: + +```py +In [113]: frame = pd.DataFrame({"a": np.random.standard_normal(100)}) + +In [114]: store = pd.HDFStore("examples/mydata.h5") + +In [115]: store["obj1"] = frame + +In [116]: store["obj1_col"] = frame["a"] + +In [117]: store +Out[117]: + +File path: examples/mydata.h5 +``` + +然后可以使用相同类似字典的 API 检索 HDF5 文件中包含的对象: + +```py +In [118]: store["obj1"] +Out[118]: + a +0 -0.204708 +1 0.478943 +2 -0.519439 +3 -0.555730 +4 1.965781 +.. ... +95 0.795253 +96 0.118110 +97 -0.748532 +98 0.584970 +99 0.152677 +[100 rows x 1 columns] +``` + +`HDFStore`支持两种存储模式,`"fixed"`和`"table"`(默认为`"fixed"`)。后者通常较慢,但支持使用特殊语法进行查询操作: + +```py +In [119]: store.put("obj2", frame, format="table") + +In [120]: store.select("obj2", where=["index >= 10 and index <= 15"]) +Out[120]: + a +10 1.007189 +11 -1.296221 +12 0.274992 +13 0.228913 +14 1.352917 +15 0.886429 + +In [121]: store.close() +``` + +`put`是`store["obj2"] = frame`方法的显式版本,但允许我们设置其他选项,如存储格式。 + +`pandas.read_hdf`函数为您提供了这些工具的快捷方式: + +```py +In [122]: frame.to_hdf("examples/mydata.h5", "obj3", format="table") + +In [123]: pd.read_hdf("examples/mydata.h5", "obj3", where=["index < 5"]) +Out[123]: + a +0 -0.204708 +1 0.478943 +2 -0.519439 +3 -0.555730 +4 1.965781 +``` + +如果您愿意,可以删除您创建的 HDF5 文件,方法如下: + +```py +In [124]: import os + +In [125]: os.remove("examples/mydata.h5") +``` + +注意 + +如果您正在处理存储在远程服务器上的数据,如 Amazon S3 或 HDFS,使用设计用于分布式存储的不同二进制格式(如[Apache Parquet](http://parquet.apache.org))可能更合适。 + +如果您在本地处理大量数据,我建议您探索 PyTables 和 h5py,看看它们如何满足您的需求。由于许多数据分析问题受 I/O 限制(而不是 CPU 限制),使用 HDF5 等工具可以大大加速您的应用程序。 + +注意 + +HDF5 不是数据库。它最适合于一次写入,多次读取的数据集。虽然数据可以随时添加到文件中,但如果多个写入者同时这样做,文件可能会损坏。 + +## 6.3 与 Web API 交互 + +许多网站都有提供数据源的公共 API,可以通过 JSON 或其他格式提供数据。有许多方法可以从 Python 访问这些 API;我推荐的一种方法是[`requests`包](http://docs.python-requests.org),可以使用 pip 或 conda 进行安装: + +```py +conda install requests +``` + +要在 GitHub 上找到 pandas 的最近 30 个问题,我们可以使用附加的`requests`库进行`GET` HTTP 请求: + +```py +In [126]: import requests + +In [127]: url = "https://api.github.com/repos/pandas-dev/pandas/issues" + +In [128]: resp = requests.get(url) + +In [129]: resp.raise_for_status() + +In [130]: resp +Out[130]: +``` + +在使用`requests.get`后,始终调用`raise_for_status`以检查 HTTP 错误是一个好习惯。 + +响应对象的`json`方法将返回一个包含解析后的 JSON 数据的 Python 对象,作为字典或列表(取决于返回的 JSON 是什么): + +```py +In [131]: data = resp.json() + +In [132]: data[0]["title"] +Out[132]: 'BUG: DataFrame.pivot mutates empty index.name attribute with typing._L +iteralGenericAlias' +``` + +由于检索到的结果基于实时数据,当您运行此代码时,您看到的结果几乎肯定会有所不同。 + +`data`中的每个元素都是一个包含 GitHub 问题页面上找到的所有数据的字典(评论除外)。我们可以直接将`data`传递给`pandas.DataFrame`并提取感兴趣的字段: + +```py +In [133]: issues = pd.DataFrame(data, columns=["number", "title", + .....: "labels", "state"]) + +In [134]: issues +Out[134]: + number +0 52629 \ +1 52628 +2 52626 +3 52625 +4 52624 +.. ... +25 52579 +26 52577 +27 52576 +28 52571 +29 52570 + title +0 BUG: DataFrame.pivot mutates empty index.name attribute with typing._Li... \ +1 DEPR: unused keywords in DTI/TDI construtors +2 ENH: Infer best datetime format from a random sample +3 BUG: ArrowExtensionArray logical_op not working in all directions +4 ENH: pandas.core.groupby.SeriesGroupBy.apply allow raw argument +.. ... +25 BUG: Axial inconsistency of pandas.diff +26 BUG: describe not respecting ArrowDtype in include/exclude +27 BUG: describe does not distinguish between Int64 and int64 +28 BUG: `pandas.DataFrame.replace` silently fails to replace category type... +29 BUG: DataFrame.describe include/exclude do not work for arrow datatypes + labels +0 [{'id': 76811, 'node_id': 'MDU6TGFiZWw3NjgxMQ==', 'url': 'https://api.g... \ +1 [] +2 [] +3 [{'id': 76811, 'node_id': 'MDU6TGFiZWw3NjgxMQ==', 'url': 'https://api.g... +4 [{'id': 76812, 'node_id': 'MDU6TGFiZWw3NjgxMg==', 'url': 'https://api.g... +.. ... +25 [{'id': 76811, 'node_id': 'MDU6TGFiZWw3NjgxMQ==', 'url': 'https://api.g... +26 [{'id': 3303158446, 'node_id': 'MDU6TGFiZWwzMzAzMTU4NDQ2', 'url': 'http... +27 [{'id': 76811, 'node_id': 'MDU6TGFiZWw3NjgxMQ==', 'url': 'https://api.g... +28 [{'id': 76811, 'node_id': 'MDU6TGFiZWw3NjgxMQ==', 'url': 'https://api.g... +29 [{'id': 76811, 'node_id': 'MDU6TGFiZWw3NjgxMQ==', 'url': 'https://api.g... + state +0 open +1 open +2 open +3 open +4 open +.. ... +25 open +26 open +27 open +28 open +29 open +[30 rows x 4 columns] +``` + +通过一些努力,您可以创建一些更高级的接口,用于常见的 Web API,返回 DataFrame 对象以便进行更方便的分析。 + +## 6.4 与数据库交互 + +在商业环境中,许多数据可能不存储在文本或 Excel 文件中。基于 SQL 的关系数据库(如 SQL Server、PostgreSQL 和 MySQL)被广泛使用,许多替代数据库也变得非常流行。数据库的选择通常取决于应用程序的性能、数据完整性和可扩展性需求。 + +pandas 有一些函数可以简化将 SQL 查询结果加载到 DataFrame 中。例如,我将使用 Python 内置的`sqlite3`驱动程序创建一个 SQLite3 数据库: + +```py +In [135]: import sqlite3 + +In [136]: query = """ + .....: CREATE TABLE test + .....: (a VARCHAR(20), b VARCHAR(20), + .....: c REAL, d INTEGER + .....: );""" + +In [137]: con = sqlite3.connect("mydata.sqlite") + +In [138]: con.execute(query) +Out[138]: + +In [139]: con.commit() +``` + +然后,插入一些数据行: + +```py +In [140]: data = [("Atlanta", "Georgia", 1.25, 6), + .....: ("Tallahassee", "Florida", 2.6, 3), + .....: ("Sacramento", "California", 1.7, 5)] + +In [141]: stmt = "INSERT INTO test VALUES(?, ?, ?, ?)" + +In [142]: con.executemany(stmt, data) +Out[142]: + +In [143]: con.commit() +``` + +大多数 Python SQL 驱动程序在从表中选择数据时返回一个元组列表: + +```py +In [144]: cursor = con.execute("SELECT * FROM test") + +In [145]: rows = cursor.fetchall() + +In [146]: rows +Out[146]: +[('Atlanta', 'Georgia', 1.25, 6), + ('Tallahassee', 'Florida', 2.6, 3), + ('Sacramento', 'California', 1.7, 5)] +``` + +您可以将元组列表传递给 DataFrame 构造函数,但还需要列名,这些列名包含在游标的`description`属性中。请注意,对于 SQLite3,游标的`description`仅提供列名(其他字段,这些字段是 Python 的数据库 API 规范的一部分,为`None`),但对于其他一些数据库驱动程序,提供了更多的列信息: + +```py +In [147]: cursor.description +Out[147]: +(('a', None, None, None, None, None, None), + ('b', None, None, None, None, None, None), + ('c', None, None, None, None, None, None), + ('d', None, None, None, None, None, None)) + +In [148]: pd.DataFrame(rows, columns=[x[0] for x in cursor.description]) +Out[148]: + a b c d +0 Atlanta Georgia 1.25 6 +1 Tallahassee Florida 2.60 3 +2 Sacramento California 1.70 5 +``` + +这是一种相当复杂的操作,您不希望每次查询数据库时都重复。[SQLAlchemy 项目](http://www.sqlalchemy.org/)是一个流行的 Python SQL 工具包,它抽象了 SQL 数据库之间的许多常见差异。pandas 有一个`read_sql`函数,可以让您轻松地从通用的 SQLAlchemy 连接中读取数据。您可以像这样使用 conda 安装 SQLAlchemy: + +```py +conda install sqlalchemy +``` + +现在,我们将使用 SQLAlchemy 连接到相同的 SQLite 数据库,并从之前创建的表中读取数据: + +```py +In [149]: import sqlalchemy as sqla + +In [150]: db = sqla.create_engine("sqlite:///mydata.sqlite") + +In [151]: pd.read_sql("SELECT * FROM test", db) +Out[151]: + a b c d +0 Atlanta Georgia 1.25 6 +1 Tallahassee Florida 2.60 3 +2 Sacramento California 1.70 5 +``` + +## 6.5 结论 + +获取数据通常是数据分析过程中的第一步。在本章中,我们已经介绍了一些有用的工具,这些工具应该可以帮助您入门。在接下来的章节中,我们将深入探讨数据整理、数据可视化、时间序列分析等主题。 + +* * * + +1. 完整列表请参见[`www.fdic.gov/bank/individual/failed/banklist.html`](https://www.fdic.gov/bank/individual/failed/banklist.html)。 diff --git a/translations/cn/pyda3e_10.md b/translations/cn/pyda3e_10.md new file mode 100644 index 000000000..f761be4c7 --- /dev/null +++ b/translations/cn/pyda3e_10.md @@ -0,0 +1,2147 @@ +# 七、数据清理和准备 + +> 原文:[`wesmckinney.com/book/data-cleaning`](https://wesmckinney.com/book/data-cleaning) +> +> 译者:[飞龙](https://github.com/wizardforcel) +> +> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) + + +> 此开放访问网络版本的《Python 数据分析第三版》现已作为[印刷版和数字版](https://amzn.to/3DyLaJc)的伴侣提供。如果您发现任何勘误,请[在此处报告](https://oreilly.com/catalog/0636920519829/errata)。请注意,由 Quarto 生成的本站点的某些方面与 O'Reilly 的印刷版和电子书版本的格式不同。 +> +> 如果您发现本书的在线版本有用,请考虑[订购纸质版](https://amzn.to/3DyLaJc)或[无 DRM 的电子书](https://www.ebooks.com/en-us/book/210644288/python-for-data-analysis/wes-mckinney/?affId=WES398681F)以支持作者。本网站的内容不得复制或再生产。代码示例采用 MIT 许可,可在 GitHub 或 Gitee 上找到。 + +在进行数据分析和建模过程中,大量时间花费在数据准备上:加载、清理、转换和重新排列。这些任务通常被报告为占据分析师 80%或更多的时间。有时,文件或数据库中存储数据的方式并不适合特定任务。许多研究人员选择使用通用编程语言(如 Python、Perl、R 或 Java)或 Unix 文本处理工具(如 sed 或 awk)对数据进行自发处理,从一种形式转换为另一种形式。幸运的是,pandas 与内置的 Python 语言功能一起,为您提供了一套高级、灵活和快速的工具,使您能够将数据转换为正确的形式。 + +如果您发现在本书或 pandas 库中找不到的数据操作类型,请随时在 Python 邮件列表或 pandas GitHub 网站上分享您的用例。事实上,pandas 的设计和实现很大程度上是由真实应用程序的需求驱动的。 + +在本章中,我讨论了有关缺失数据、重复数据、字符串操作和其他一些分析数据转换的工具。在下一章中,我将专注于以各种方式组合和重新排列数据集。 + +## 7.1 处理缺失数据 + +缺失数据在许多数据分析应用中很常见。pandas 的目标之一是尽可能地使处理缺失数据变得轻松。例如,默认情况下,pandas 对象上的所有描述性统计都排除缺失数据。 + +pandas 对象中表示缺失数据的方式有些不完美,但对于大多数真实世界的用途来说是足够的。对于`float64`数据类型,pandas 使用浮点值`NaN`(Not a Number)表示缺失数据。 + +我们称之为*标记值*:当存在时,表示缺失(或*空*)值: + +```py +In [14]: float_data = pd.Series([1.2, -3.5, np.nan, 0]) + +In [15]: float_data +Out[15]: +0 1.2 +1 -3.5 +2 NaN +3 0.0 +dtype: float64 +``` + +`isna`方法为我们提供一个布尔 Series,其中值为空时为`True`: + +```py +In [16]: float_data.isna() +Out[16]: +0 False +1 False +2 True +3 False +dtype: bool +``` + +在 pandas 中,我们采用了 R 编程语言中使用的惯例,将缺失数据称为 NA,代表*不可用*。在统计应用中,NA 数据可能是不存在的数据,也可能是存在但未被观察到的数据(例如通过数据收集问题)。在清理数据进行分析时,通常重要的是对缺失数据本身进行分析,以识别数据收集问题或由缺失数据引起的数据潜在偏差。 + +内置的 Python `None`值也被视为 NA: + +```py +In [17]: string_data = pd.Series(["aardvark", np.nan, None, "avocado"]) + +In [18]: string_data +Out[18]: +0 aardvark +1 NaN +2 None +3 avocado +dtype: object + +In [19]: string_data.isna() +Out[19]: +0 False +1 True +2 True +3 False +dtype: bool + +In [20]: float_data = pd.Series([1, 2, None], dtype='float64') + +In [21]: float_data +Out[21]: +0 1.0 +1 2.0 +2 NaN +dtype: float64 + +In [22]: float_data.isna() +Out[22]: +0 False +1 False +2 True +dtype: bool +``` + +pandas 项目已经尝试使处理缺失数据在不同数据类型之间保持一致。像`pandas.isna`这样的函数抽象了许多烦人的细节。请参阅表 7.1 以获取与处理缺失数据相关的一些函数列表。 + +表 7.1:NA 处理对象方法 + +| 方法 | 描述 | +| --- | --- | +| `dropna` | 根据每个标签的值是否具有缺失数据来过滤轴标签,对于可以容忍多少缺失数据有不同的阈值。 | +| `fillna` | 使用某个值或插值方法(如 `"ffill"` 或 `"bfill"`)填充缺失数据。 | +| `isna` | 返回指示哪些值缺失/NA 的布尔值。 | +| `notna` | `isna` 的否定,对于非 NA 值返回 `True`,对于 NA 值返回 `False`。 | + +### 过滤缺失数据 + +有几种过滤缺失数据的方法。虽然您始终可以选择使用 `pandas.isna` 和布尔索引手动执行,但 `dropna` 可能会有所帮助。对于 Series,它返回仅具有非空数据和索引值的 Series: + +```py +In [23]: data = pd.Series([1, np.nan, 3.5, np.nan, 7]) + +In [24]: data.dropna() +Out[24]: +0 1.0 +2 3.5 +4 7.0 +dtype: float64 +``` + +这与执行以下操作相同: + +```py +In [25]: data[data.notna()] +Out[25]: +0 1.0 +2 3.5 +4 7.0 +dtype: float64 +``` + +对于 DataFrame 对象,有不同的方法可以删除缺失数据。您可能希望删除所有 NA 的行或列,或者仅删除包含任何 NA 的行或列。`dropna` 默认情况下会删除包含缺失值的任何行: + +```py +In [26]: data = pd.DataFrame([[1., 6.5, 3.], [1., np.nan, np.nan], + ....: [np.nan, np.nan, np.nan], [np.nan, 6.5, 3.]]) + +In [27]: data +Out[27]: + 0 1 2 +0 1.0 6.5 3.0 +1 1.0 NaN NaN +2 NaN NaN NaN +3 NaN 6.5 3.0 + +In [28]: data.dropna() +Out[28]: + 0 1 2 +0 1.0 6.5 3.0 +``` + +传递 `how="all"` 将仅删除所有 NA 的行: + +```py +In [29]: data.dropna(how="all") +Out[29]: + 0 1 2 +0 1.0 6.5 3.0 +1 1.0 NaN NaN +3 NaN 6.5 3.0 +``` + +请记住,这些函数默认情况下返回新对象,不会修改原始对象的内容。 + +要以相同方式删除列,请传递 `axis="columns"`: + +```py +In [30]: data[4] = np.nan + +In [31]: data +Out[31]: + 0 1 2 4 +0 1.0 6.5 3.0 NaN +1 1.0 NaN NaN NaN +2 NaN NaN NaN NaN +3 NaN 6.5 3.0 NaN + +In [32]: data.dropna(axis="columns", how="all") +Out[32]: + 0 1 2 +0 1.0 6.5 3.0 +1 1.0 NaN NaN +2 NaN NaN NaN +3 NaN 6.5 3.0 +``` + +假设您只想保留包含至多一定数量缺失观察的行。您可以使用 `thresh` 参数指示这一点: + +```py +In [33]: df = pd.DataFrame(np.random.standard_normal((7, 3))) + +In [34]: df.iloc[:4, 1] = np.nan + +In [35]: df.iloc[:2, 2] = np.nan + +In [36]: df +Out[36]: + 0 1 2 +0 -0.204708 NaN NaN +1 -0.555730 NaN NaN +2 0.092908 NaN 0.769023 +3 1.246435 NaN -1.296221 +4 0.274992 0.228913 1.352917 +5 0.886429 -2.001637 -0.371843 +6 1.669025 -0.438570 -0.539741 + +In [37]: df.dropna() +Out[37]: + 0 1 2 +4 0.274992 0.228913 1.352917 +5 0.886429 -2.001637 -0.371843 +6 1.669025 -0.438570 -0.539741 + +In [38]: df.dropna(thresh=2) +Out[38]: + 0 1 2 +2 0.092908 NaN 0.769023 +3 1.246435 NaN -1.296221 +4 0.274992 0.228913 1.352917 +5 0.886429 -2.001637 -0.371843 +6 1.669025 -0.438570 -0.539741 +``` + +### 填充缺失数据 + +与过滤缺失数据(并可能连同其他数据一起丢弃)不同,您可能希望以任意方式填补任意数量的“空洞”。对于大多数情况,`fillna` 方法是要使用的主要函数。通过使用常量调用 `fillna` 可以用该值替换缺失值: + +```py +In [39]: df.fillna(0) +Out[39]: + 0 1 2 +0 -0.204708 0.000000 0.000000 +1 -0.555730 0.000000 0.000000 +2 0.092908 0.000000 0.769023 +3 1.246435 0.000000 -1.296221 +4 0.274992 0.228913 1.352917 +5 0.886429 -2.001637 -0.371843 +6 1.669025 -0.438570 -0.539741 +``` + +通过字典调用 `fillna`,您可以为每列使用不同的填充值: + +```py +In [40]: df.fillna({1: 0.5, 2: 0}) +Out[40]: + 0 1 2 +0 -0.204708 0.500000 0.000000 +1 -0.555730 0.500000 0.000000 +2 0.092908 0.500000 0.769023 +3 1.246435 0.500000 -1.296221 +4 0.274992 0.228913 1.352917 +5 0.886429 -2.001637 -0.371843 +6 1.669025 -0.438570 -0.539741 +``` + +可用于重新索引的相同插值方法(请参见 表 5.3)也可用于 `fillna`: + +```py +In [41]: df = pd.DataFrame(np.random.standard_normal((6, 3))) + +In [42]: df.iloc[2:, 1] = np.nan + +In [43]: df.iloc[4:, 2] = np.nan + +In [44]: df +Out[44]: + 0 1 2 +0 0.476985 3.248944 -1.021228 +1 -0.577087 0.124121 0.302614 +2 0.523772 NaN 1.343810 +3 -0.713544 NaN -2.370232 +4 -1.860761 NaN NaN +5 -1.265934 NaN NaN + +In [45]: df.fillna(method="ffill") +Out[45]: + 0 1 2 +0 0.476985 3.248944 -1.021228 +1 -0.577087 0.124121 0.302614 +2 0.523772 0.124121 1.343810 +3 -0.713544 0.124121 -2.370232 +4 -1.860761 0.124121 -2.370232 +5 -1.265934 0.124121 -2.370232 + +In [46]: df.fillna(method="ffill", limit=2) +Out[46]: + 0 1 2 +0 0.476985 3.248944 -1.021228 +1 -0.577087 0.124121 0.302614 +2 0.523772 0.124121 1.343810 +3 -0.713544 0.124121 -2.370232 +4 -1.860761 NaN -2.370232 +5 -1.265934 NaN -2.370232 +``` + +使用 `fillna`,您可以做很多其他事情,比如使用中位数或平均统计数据进行简单的数据填充: + +```py +In [47]: data = pd.Series([1., np.nan, 3.5, np.nan, 7]) + +In [48]: data.fillna(data.mean()) +Out[48]: +0 1.000000 +1 3.833333 +2 3.500000 +3 3.833333 +4 7.000000 +dtype: float64 +``` + +请参见 表 7.2 了解 `fillna` 函数参数的参考。 + +表 7.2:`fillna` 函数参数 + +| 参数 | 描述 | +| --- | --- | +| `value` | 用于填充缺失值的标量值或类似字典的对象 | +| `method` | 插值方法:可以是 `"bfill"`(向后填充)或 `"ffill"`(向前填充)之一;默认为 `None` | +| `axis` | 填充的轴(`"index"` 或 `"columns"`);默认为 `axis="index"` | +| `limit` | 对于向前和向后填充,最大连续填充周期数 | + +## 7.2 数据转换 + +到目前为止,在本章中,我们一直关注处理缺失数据。过滤、清理和其他转换是另一类重要操作。 + +### 删除重复项 + +DataFrame 中可能会出现重复行,原因有很多。这里是一个例子: + +```py +In [49]: data = pd.DataFrame({"k1": ["one", "two"] * 3 + ["two"], + ....: "k2": [1, 1, 2, 3, 3, 4, 4]}) + +In [50]: data +Out[50]: + k1 k2 +0 one 1 +1 two 1 +2 one 2 +3 two 3 +4 one 3 +5 two 4 +6 two 4 +``` + +DataFrame 方法 `duplicated` 返回一个布尔 Series,指示每行是否为重复行(其列值与较早行中的值完全相等): + +```py +In [51]: data.duplicated() +Out[51]: +0 False +1 False +2 False +3 False +4 False +5 False +6 True +dtype: bool +``` + +相关地,`drop_duplicates` 返回一个 DataFrame,其中过滤掉 `duplicated` 数组为 `False` 的行: + +```py +In [52]: data.drop_duplicates() +Out[52]: + k1 k2 +0 one 1 +1 two 1 +2 one 2 +3 two 3 +4 one 3 +5 two 4 +``` + +默认情况下,这两种方法都考虑所有列;或者,您可以指定任何子集来检测重复项。假设我们有一个额外的值列,并且只想基于 `"k1"` 列过滤重复项: + +```py +In [53]: data["v1"] = range(7) + +In [54]: data +Out[54]: + k1 k2 v1 +0 one 1 0 +1 two 1 1 +2 one 2 2 +3 two 3 3 +4 one 3 4 +5 two 4 5 +6 two 4 6 + +In [55]: data.drop_duplicates(subset=["k1"]) +Out[55]: + k1 k2 v1 +0 one 1 0 +1 two 1 1 +``` + +`duplicated` 和 `drop_duplicates` 默认保留第一个观察到的值组合。传递 `keep="last"` 将返回最后一个: + +```py +In [56]: data.drop_duplicates(["k1", "k2"], keep="last") +Out[56]: + k1 k2 v1 +0 one 1 0 +1 two 1 1 +2 one 2 2 +3 two 3 3 +4 one 3 4 +6 two 4 6 +``` + +### 使用函数或映射转换数据 + +对于许多数据集,您可能希望根据数组、Series 或 DataFrame 中的值执行一些基于值的转换。考虑收集的关于各种肉类的假设数据: + +```py +In [57]: data = pd.DataFrame({"food": ["bacon", "pulled pork", "bacon", + ....: "pastrami", "corned beef", "bacon", + ....: "pastrami", "honey ham", "nova lox"], + ....: "ounces": [4, 3, 12, 6, 7.5, 8, 3, 5, 6]}) + +In [58]: data +Out[58]: + food ounces +0 bacon 4.0 +1 pulled pork 3.0 +2 bacon 12.0 +3 pastrami 6.0 +4 corned beef 7.5 +5 bacon 8.0 +6 pastrami 3.0 +7 honey ham 5.0 +8 nova lox 6.0 +``` + +假设您想要添加一个指示每种食物来自哪种动物的列。让我们写下每种不同肉类到动物种类的映射: + +```py +meat_to_animal = { + "bacon": "pig", + "pulled pork": "pig", + "pastrami": "cow", + "corned beef": "cow", + "honey ham": "pig", + "nova lox": "salmon" +} +``` + +Series 上的 `map` 方法(也在 Ch 5.2.5: 函数应用和映射 中讨论)接受一个包含映射的函数或类似字典的对象,用于对值进行转换: + +```py +In [60]: data["animal"] = data["food"].map(meat_to_animal) + +In [61]: data +Out[61]: + food ounces animal +0 bacon 4.0 pig +1 pulled pork 3.0 pig +2 bacon 12.0 pig +3 pastrami 6.0 cow +4 corned beef 7.5 cow +5 bacon 8.0 pig +6 pastrami 3.0 cow +7 honey ham 5.0 pig +8 nova lox 6.0 salmon +``` + +我们也可以传递一个执行所有工作的函数: + +```py +In [62]: def get_animal(x): + ....: return meat_to_animal[x] + +In [63]: data["food"].map(get_animal) +Out[63]: +0 pig +1 pig +2 pig +3 cow +4 cow +5 pig +6 cow +7 pig +8 salmon +Name: food, dtype: object +``` + +使用 `map` 是执行逐元素转换和其他数据清理相关操作的便捷方式。 + +### 替换值 + +使用 `fillna` 方法填充缺失数据是更一般的值替换的特殊情况。正如您已经看到的,`map` 可以用于修改对象中的一部分值,但 `replace` 提供了一种更简单、更灵活的方法。让我们考虑这个 Series: + +```py +In [64]: data = pd.Series([1., -999., 2., -999., -1000., 3.]) + +In [65]: data +Out[65]: +0 1.0 +1 -999.0 +2 2.0 +3 -999.0 +4 -1000.0 +5 3.0 +dtype: float64 +``` + +`-999` 值可能是缺失数据的标记值。要用 pandas 理解的 NA 值替换这些值,可以使用 `replace`,生成一个新的 Series: + +```py +In [66]: data.replace(-999, np.nan) +Out[66]: +0 1.0 +1 NaN +2 2.0 +3 NaN +4 -1000.0 +5 3.0 +dtype: float64 +``` + +如果您想一次替换多个值,可以传递一个列表,然后是替代值: + +```py +In [67]: data.replace([-999, -1000], np.nan) +Out[67]: +0 1.0 +1 NaN +2 2.0 +3 NaN +4 NaN +5 3.0 +dtype: float64 +``` + +要为每个值使用不同的替代值,传递一个替代列表: + +```py +In [68]: data.replace([-999, -1000], [np.nan, 0]) +Out[68]: +0 1.0 +1 NaN +2 2.0 +3 NaN +4 0.0 +5 3.0 +dtype: float64 +``` + +传递的参数也可以是一个字典: + +```py +In [69]: data.replace({-999: np.nan, -1000: 0}) +Out[69]: +0 1.0 +1 NaN +2 2.0 +3 NaN +4 0.0 +5 3.0 +dtype: float64 +``` + +注意 + +`data.replace` 方法与 `data.str.replace` 是不同的,后者执行逐元素的字符串替换。我们将在本章后面的 Series 中查看这些字符串方法。 + +### 重命名轴索引 + +与 Series 中的值类似,轴标签也可以通过函数或某种形式的映射进行类似转换,以生成新的、不同标记的对象。您还可以在原地修改轴,而不创建新的数据结构。这是一个简单的例子: + +```py +In [70]: data = pd.DataFrame(np.arange(12).reshape((3, 4)), + ....: index=["Ohio", "Colorado", "New York"], + ....: columns=["one", "two", "three", "four"]) +``` + +与 Series 一样,轴索引具有 `map` 方法: + +```py +In [71]: def transform(x): + ....: return x[:4].upper() + +In [72]: data.index.map(transform) +Out[72]: Index(['OHIO', 'COLO', 'NEW '], dtype='object') +``` + +您可以分配给 `index` 属性,直接修改 DataFrame: + +```py +In [73]: data.index = data.index.map(transform) + +In [74]: data +Out[74]: + one two three four +OHIO 0 1 2 3 +COLO 4 5 6 7 +NEW 8 9 10 11 +``` + +如果要创建一个转换后的数据集副本而不修改原始数据集,一个有用的方法是 `rename`: + +```py +In [75]: data.rename(index=str.title, columns=str.upper) +Out[75]: + ONE TWO THREE FOUR +Ohio 0 1 2 3 +Colo 4 5 6 7 +New 8 9 10 11 +``` + +值得注意的是,`rename` 可以与类似字典的对象一起使用,为轴标签的子集提供新值: + +```py +In [76]: data.rename(index={"OHIO": "INDIANA"}, + ....: columns={"three": "peekaboo"}) +Out[76]: + one two peekaboo four +INDIANA 0 1 2 3 +COLO 4 5 6 7 +NEW 8 9 10 11 +``` + +`rename` 可以避免手动复制 DataFrame 并为其 `index` 和 `columns` 属性分配新值的繁琐工作。 + +### 离散化和分箱 + +连续数据通常被离散化或以其他方式分成“箱子”进行分析。假设您有一组人的研究数据,并且想要将它们分成离散的年龄段: + +```py +In [77]: ages = [20, 22, 25, 27, 21, 23, 37, 31, 61, 45, 41, 32] +``` + +让我们将这些分成 18 至 25 岁、26 至 35 岁、36 至 60 岁,最后是 61 岁及以上的箱子。为此,您必须使用 `pandas.cut`: + +```py +In [78]: bins = [18, 25, 35, 60, 100] + +In [79]: age_categories = pd.cut(ages, bins) + +In [80]: age_categories +Out[80]: +[(18, 25], (18, 25], (18, 25], (25, 35], (18, 25], ..., (25, 35], (60, 100], (35, + 60], (35, 60], (25, 35]] +Length: 12 +Categories (4, interval[int64, right]): [(18, 25] < (25, 35] < (35, 60] < (60, 10 +0]] +``` + +pandas 返回的对象是一个特殊的分类对象。您看到的输出描述了 `pandas.cut` 计算的箱。每个箱由一个特殊的(对于 pandas 是唯一的)区间值类型标识,其中包含每个箱的下限和上限: + +```py +In [81]: age_categories.codes +Out[81]: array([0, 0, 0, 1, 0, 0, 2, 1, 3, 2, 2, 1], dtype=int8) + +In [82]: age_categories.categories +Out[82]: IntervalIndex([(18, 25], (25, 35], (35, 60], (60, 100]], dtype='interval +[int64, right]') + +In [83]: age_categories.categories[0] +Out[83]: Interval(18, 25, closed='right') + +In [84]: pd.value_counts(age_categories) +Out[84]: +(18, 25] 5 +(25, 35] 3 +(35, 60] 3 +(60, 100] 1 +Name: count, dtype: int64 +``` + +请注意,`pd.value_counts(categories)` 是 `pandas.cut` 结果的箱计数。 + +在区间的字符串表示中,括号表示一侧是 *开放的*(排除的),而方括号表示一侧是 *闭合的*(包含的)。您可以通过传递 `right=False` 来更改哪一侧是闭合的: + +```py +In [85]: pd.cut(ages, bins, right=False) +Out[85]: +[[18, 25), [18, 25), [25, 35), [25, 35), [18, 25), ..., [25, 35), [60, 100), [35, + 60), [35, 60), [25, 35)] +Length: 12 +Categories (4, interval[int64, left]): [[18, 25) < [25, 35) < [35, 60) < [60, 100 +)] +``` + +通过将列表或数组传递给 `labels` 选项,可以覆盖默认的基于区间的箱标签: + +```py +In [86]: group_names = ["Youth", "YoungAdult", "MiddleAged", "Senior"] + +In [87]: pd.cut(ages, bins, labels=group_names) +Out[87]: +['Youth', 'Youth', 'Youth', 'YoungAdult', 'Youth', ..., 'YoungAdult', 'Senior', ' +MiddleAged', 'MiddleAged', 'YoungAdult'] +Length: 12 +Categories (4, object): ['Youth' < 'YoungAdult' < 'MiddleAged' < 'Senior'] +``` + +如果将整数数量的箱传递给 `pandas.cut` 而不是显式的箱边界,它将基于数据中的最小值和最大值计算等长的箱。考虑一下一些均匀分布的数据被分成四等份的情况: + +```py +In [88]: data = np.random.uniform(size=20) + +In [89]: pd.cut(data, 4, precision=2) +Out[89]: +[(0.34, 0.55], (0.34, 0.55], (0.76, 0.97], (0.76, 0.97], (0.34, 0.55], ..., (0.34 +, 0.55], (0.34, 0.55], (0.55, 0.76], (0.34, 0.55], (0.12, 0.34]] +Length: 20 +Categories (4, interval[float64, right]): [(0.12, 0.34] < (0.34, 0.55] < (0.55, 0 +.76] < + (0.76, 0.97]] +``` + +`precision=2` 选项将小数精度限制为两位数。 + +一个与之密切相关的函数 `pandas.qcut`,根据样本分位数对数据进行分箱。根据数据的分布,使用 `pandas.cut` 通常不会导致每个箱具有相同数量的数据点。由于 `pandas.qcut` 使用样本分位数,因此您将获得大致相同大小的箱: + +```py +In [90]: data = np.random.standard_normal(1000) + +In [91]: quartiles = pd.qcut(data, 4, precision=2) + +In [92]: quartiles +Out[92]: +[(-0.026, 0.62], (0.62, 3.93], (-0.68, -0.026], (0.62, 3.93], (-0.026, 0.62], ... +, (-0.68, -0.026], (-0.68, -0.026], (-2.96, -0.68], (0.62, 3.93], (-0.68, -0.026] +] +Length: 1000 +Categories (4, interval[float64, right]): [(-2.96, -0.68] < (-0.68, -0.026] < (-0 +.026, 0.62] < + (0.62, 3.93]] + +In [93]: pd.value_counts(quartiles) +Out[93]: +(-2.96, -0.68] 250 +(-0.68, -0.026] 250 +(-0.026, 0.62] 250 +(0.62, 3.93] 250 +Name: count, dtype: int64 +``` + +类似于 `pandas.cut`,您可以传递自己的分位数(介于 0 和 1 之间的数字): + +```py +In [94]: pd.qcut(data, [0, 0.1, 0.5, 0.9, 1.]).value_counts() +Out[94]: +(-2.9499999999999997, -1.187] 100 +(-1.187, -0.0265] 400 +(-0.0265, 1.286] 400 +(1.286, 3.928] 100 +Name: count, dtype: int64 +``` + +我们将在本章后面的聚合和分组操作讨论中再次回到`pandas.cut`和`pandas.qcut`,因为这些离散化函数对于分位数和分组分析特别有用。 + +### 检测和过滤异常值 + +过滤或转换异常值主要是应用数组操作的问题。考虑一个包含一些正态分布数据的 DataFrame: + +```py +In [95]: data = pd.DataFrame(np.random.standard_normal((1000, 4))) + +In [96]: data.describe() +Out[96]: + 0 1 2 3 +count 1000.000000 1000.000000 1000.000000 1000.000000 +mean 0.049091 0.026112 -0.002544 -0.051827 +std 0.996947 1.007458 0.995232 0.998311 +min -3.645860 -3.184377 -3.745356 -3.428254 +25% -0.599807 -0.612162 -0.687373 -0.747478 +50% 0.047101 -0.013609 -0.022158 -0.088274 +75% 0.756646 0.695298 0.699046 0.623331 +max 2.653656 3.525865 2.735527 3.366626 +``` + +假设您想要查找绝对值超过 3 的某一列中的值: + +```py +In [97]: col = data[2] + +In [98]: col[col.abs() > 3] +Out[98]: +41 -3.399312 +136 -3.745356 +Name: 2, dtype: float64 +``` + +要选择所有值超过 3 或-3 的行,您可以在布尔 DataFrame 上使用`any`方法: + +```py +In [99]: data[(data.abs() > 3).any(axis="columns")] +Out[99]: + 0 1 2 3 +41 0.457246 -0.025907 -3.399312 -0.974657 +60 1.951312 3.260383 0.963301 1.201206 +136 0.508391 -0.196713 -3.745356 -1.520113 +235 -0.242459 -3.056990 1.918403 -0.578828 +258 0.682841 0.326045 0.425384 -3.428254 +322 1.179227 -3.184377 1.369891 -1.074833 +544 -3.548824 1.553205 -2.186301 1.277104 +635 -0.578093 0.193299 1.397822 3.366626 +782 -0.207434 3.525865 0.283070 0.544635 +803 -3.645860 0.255475 -0.549574 -1.907459 +``` + +在`data.abs() > 3`周围的括号是必要的,以便在比较操作的结果上调用`any`方法。 + +可以根据这些标准设置值。以下是将值限制在区间-3 到 3 之外的代码: + +```py +In [100]: data[data.abs() > 3] = np.sign(data) * 3 + +In [101]: data.describe() +Out[101]: + 0 1 2 3 +count 1000.000000 1000.000000 1000.000000 1000.000000 +mean 0.050286 0.025567 -0.001399 -0.051765 +std 0.992920 1.004214 0.991414 0.995761 +min -3.000000 -3.000000 -3.000000 -3.000000 +25% -0.599807 -0.612162 -0.687373 -0.747478 +50% 0.047101 -0.013609 -0.022158 -0.088274 +75% 0.756646 0.695298 0.699046 0.623331 +max 2.653656 3.000000 2.735527 3.000000 +``` + +`np.sign(data)`语句根据`data`中的值是正数还是负数产生 1 和-1 值: + +```py +In [102]: np.sign(data).head() +Out[102]: + 0 1 2 3 +0 -1.0 1.0 -1.0 1.0 +1 1.0 -1.0 1.0 -1.0 +2 1.0 1.0 1.0 -1.0 +3 -1.0 -1.0 1.0 -1.0 +4 -1.0 1.0 -1.0 -1.0 +``` + +### 排列和随机抽样 + +通过使用`numpy.random.permutation`函数,可以对 Series 或 DataFrame 中的行进行排列(随机重新排序)。调用`permutation`并传入您想要排列的轴的长度会产生一个整数数组,指示新的排序: + +```py +In [103]: df = pd.DataFrame(np.arange(5 * 7).reshape((5, 7))) + +In [104]: df +Out[104]: + 0 1 2 3 4 5 6 +0 0 1 2 3 4 5 6 +1 7 8 9 10 11 12 13 +2 14 15 16 17 18 19 20 +3 21 22 23 24 25 26 27 +4 28 29 30 31 32 33 34 + +In [105]: sampler = np.random.permutation(5) + +In [106]: sampler +Out[106]: array([3, 1, 4, 2, 0]) +``` + +然后可以将该数组用于基于`iloc`的索引或等效的`take`函数: + +```py +In [107]: df.take(sampler) +Out[107]: + 0 1 2 3 4 5 6 +3 21 22 23 24 25 26 27 +1 7 8 9 10 11 12 13 +4 28 29 30 31 32 33 34 +2 14 15 16 17 18 19 20 +0 0 1 2 3 4 5 6 + +In [108]: df.iloc[sampler] +Out[108]: + 0 1 2 3 4 5 6 +3 21 22 23 24 25 26 27 +1 7 8 9 10 11 12 13 +4 28 29 30 31 32 33 34 +2 14 15 16 17 18 19 20 +0 0 1 2 3 4 5 6 +``` + +通过使用`axis="columns"`调用`take`,我们还可以选择列的排列: + +```py +In [109]: column_sampler = np.random.permutation(7) + +In [110]: column_sampler +Out[110]: array([4, 6, 3, 2, 1, 0, 5]) + +In [111]: df.take(column_sampler, axis="columns") +Out[111]: + 4 6 3 2 1 0 5 +0 4 6 3 2 1 0 5 +1 11 13 10 9 8 7 12 +2 18 20 17 16 15 14 19 +3 25 27 24 23 22 21 26 +4 32 34 31 30 29 28 33 +``` + +要选择一个不带替换的随机子集(同一行不能出现两次),可以在 Series 和 DataFrame 上使用`sample`方法: + +```py +In [112]: df.sample(n=3) +Out[112]: + 0 1 2 3 4 5 6 +2 14 15 16 17 18 19 20 +4 28 29 30 31 32 33 34 +0 0 1 2 3 4 5 6 +``` + +要生成一个*带有*替换的样本(允许重复选择),请将`replace=True`传递给`sample`: + +```py +In [113]: choices = pd.Series([5, 7, -1, 6, 4]) + +In [114]: choices.sample(n=10, replace=True) +Out[114]: +2 -1 +0 5 +3 6 +1 7 +4 4 +0 5 +4 4 +0 5 +4 4 +4 4 +dtype: int64 +``` + +### 计算指示/虚拟变量 + +另一种用于统计建模或机器学习应用的转换类型是将分类变量转换为*虚拟*或*指示*矩阵。如果 DataFrame 中的一列有`k`个不同的值,您将得到一个包含所有 1 和 0 的`k`列的矩阵或 DataFrame。pandas 有一个`pandas.get_dummies`函数可以做到这一点,尽管您也可以自己设计一个。让我们考虑一个示例 DataFrame: + +```py +In [115]: df = pd.DataFrame({"key": ["b", "b", "a", "c", "a", "b"], + .....: "data1": range(6)}) + +In [116]: df +Out[116]: + key data1 +0 b 0 +1 b 1 +2 a 2 +3 c 3 +4 a 4 +5 b 5 + +In [117]: pd.get_dummies(df["key"], dtype=float) +Out[117]: + a b c +0 0.0 1.0 0.0 +1 0.0 1.0 0.0 +2 1.0 0.0 0.0 +3 0.0 0.0 1.0 +4 1.0 0.0 0.0 +5 0.0 1.0 0.0 +``` + +在这里,我传递了`dtype=float`以将输出类型从布尔值(pandas 较新版本中的默认值)更改为浮点数。 + +在某些情况下,您可能希望在指示 DataFrame 的列中添加前缀,然后将其与其他数据合并。`pandas.get_dummies`有一个用于执行此操作的前缀参数: + +```py +In [118]: dummies = pd.get_dummies(df["key"], prefix="key", dtype=float) + +In [119]: df_with_dummy = df[["data1"]].join(dummies) + +In [120]: df_with_dummy +Out[120]: + data1 key_a key_b key_c +0 0 0.0 1.0 0.0 +1 1 0.0 1.0 0.0 +2 2 1.0 0.0 0.0 +3 3 0.0 0.0 1.0 +4 4 1.0 0.0 0.0 +5 5 0.0 1.0 0.0 +``` + +`DataFrame.join`方法将在下一章中详细解释。 + +如果 DataFrame 中的一行属于多个类别,则我们必须使用不同的方法来创建虚拟变量。让我们看一下 MovieLens 1M 数据集,该数据集在 Ch 13:数据分析示例中有更详细的研究: + +```py +In [121]: mnames = ["movie_id", "title", "genres"] + +In [122]: movies = pd.read_table("datasets/movielens/movies.dat", sep="::", + .....: header=None, names=mnames, engine="python") + +In [123]: movies[:10] +Out[123]: + movie_id title genres +0 1 Toy Story (1995) Animation|Children's|Comedy +1 2 Jumanji (1995) Adventure|Children's|Fantasy +2 3 Grumpier Old Men (1995) Comedy|Romance +3 4 Waiting to Exhale (1995) Comedy|Drama +4 5 Father of the Bride Part II (1995) Comedy +5 6 Heat (1995) Action|Crime|Thriller +6 7 Sabrina (1995) Comedy|Romance +7 8 Tom and Huck (1995) Adventure|Children's +8 9 Sudden Death (1995) Action +9 10 GoldenEye (1995) Action|Adventure|Thriller +``` + +pandas 实现了一个特殊的 Series 方法`str.get_dummies`(以`str.`开头的方法将在字符串操作中更详细地讨论),处理了将多个组成员身份编码为分隔字符串的情况: + +```py +In [124]: dummies = movies["genres"].str.get_dummies("|") + +In [125]: dummies.iloc[:10, :6] +Out[125]: + Action Adventure Animation Children's Comedy Crime +0 0 0 1 1 1 0 +1 0 1 0 1 0 0 +2 0 0 0 0 1 0 +3 0 0 0 0 1 0 +4 0 0 0 0 1 0 +5 1 0 0 0 0 1 +6 0 0 0 0 1 0 +7 0 1 0 1 0 0 +8 1 0 0 0 0 0 +9 1 1 0 0 0 0 +``` + +然后,与之前一样,您可以将此与`movies`组合,同时在`dummies` DataFrame 的列名中添加`"Genre_"`,使用`add_prefix`方法: + +```py +In [126]: movies_windic = movies.join(dummies.add_prefix("Genre_")) + +In [127]: movies_windic.iloc[0] +Out[127]: +movie_id 1 +title Toy Story (1995) +genres Animation|Children's|Comedy +Genre_Action 0 +Genre_Adventure 0 +Genre_Animation 1 +Genre_Children's 1 +Genre_Comedy 1 +Genre_Crime 0 +Genre_Documentary 0 +Genre_Drama 0 +Genre_Fantasy 0 +Genre_Film-Noir 0 +Genre_Horror 0 +Genre_Musical 0 +Genre_Mystery 0 +Genre_Romance 0 +Genre_Sci-Fi 0 +Genre_Thriller 0 +Genre_War 0 +Genre_Western 0 +Name: 0, dtype: object +``` + +注意 + +对于更大的数据,使用这种构建具有多个成员身份的指示变量的方法并不特别快速。最好编写一个直接写入 NumPy 数组的低级函数,然后将结果包装在 DataFrame 中。 + +在统计应用中的一个有用的技巧是将`pandas.get_dummies`与像`pandas.cut`这样的离散化函数结合使用:* + +```py +In [128]: np.random.seed(12345) # to make the example repeatable + +In [129]: values = np.random.uniform(size=10) + +In [130]: values +Out[130]: +array([0.9296, 0.3164, 0.1839, 0.2046, 0.5677, 0.5955, 0.9645, 0.6532, + 0.7489, 0.6536]) + +In [131]: bins = [0, 0.2, 0.4, 0.6, 0.8, 1] + +In [132]: pd.get_dummies(pd.cut(values, bins)) +Out[132]: + (0.0, 0.2] (0.2, 0.4] (0.4, 0.6] (0.6, 0.8] (0.8, 1.0] +0 False False False False True +1 False True False False False +2 True False False False False +3 False True False False False +4 False False True False False +5 False False True False False +6 False False False False True +7 False False False True False +8 False False False True False +9 False False False True False +``` + +我们稍后将再次查看`pandas.get_dummies`,在为建模创建虚拟变量中。 + +## 7.3 扩展数据类型 + +注意 + +这是一个较新且更高级的主题,许多 pandas 用户不需要了解太多,但我在这里完整地介绍它,因为在接下来的章节中我将引用和使用扩展数据类型。 + +pandas 最初是建立在 NumPy 的基础上的,NumPy 是一个主要用于处理数值数据的数组计算库。许多 pandas 概念,如缺失数据,是使用 NumPy 中可用的内容实现的,同时尽量在使用 NumPy 和 pandas 的库之间最大程度地保持兼容性。 + +基于 NumPy 的构建存在许多缺点,例如: + ++ 对于一些数值数据类型,如整数和布尔值,缺失数据处理是不完整的。因此,当这些数据中引入缺失数据时,pandas 会将数据类型转换为`float64`,并使用`np.nan`表示空值。这导致许多 pandas 算法中出现了微妙的问题。 + ++ 具有大量字符串数据的数据集在计算上是昂贵的,并且使用了大量内存。 + ++ 一些数据类型,如时间间隔、时间增量和带时区的时间戳,如果不使用计算昂贵的 Python 对象数组,将无法有效支持。 + +最近,pandas 开发了一个*扩展类型*系统,允许添加新的数据类型,即使它们在 NumPy 中没有原生支持。这些新数据类型可以被视为与来自 NumPy 数组的数据同等重要。 + +让我们看一个例子,我们创建一个带有缺失值的整数 Series: + +```py +In [133]: s = pd.Series([1, 2, 3, None]) + +In [134]: s +Out[134]: +0 1.0 +1 2.0 +2 3.0 +3 NaN +dtype: float64 + +In [135]: s.dtype +Out[135]: dtype('float64') +``` + +主要出于向后兼容的原因,Series 使用了使用`float64`数据类型和`np.nan`表示缺失值的传统行为。我们可以使用`pandas.Int64Dtype`来创建这个 Series: + +```py +In [136]: s = pd.Series([1, 2, 3, None], dtype=pd.Int64Dtype()) + +In [137]: s +Out[137]: +0 1 +1 2 +2 3 +3 +dtype: Int64 + +In [138]: s.isna() +Out[138]: +0 False +1 False +2 False +3 True +dtype: bool + +In [139]: s.dtype +Out[139]: Int64Dtype() +``` + +输出``表示扩展类型数组中的值缺失。这使用了特殊的`pandas.NA`标记值: + +```py +In [140]: s[3] +Out[140]: + +In [141]: s[3] is pd.NA +Out[141]: True +``` + +我们也可以使用缩写`"Int64"`来指定类型,而不是`pd.Int64Dtype()`。大写是必需的,否则它将是一个基于 NumPy 的非扩展类型: + +```py +In [142]: s = pd.Series([1, 2, 3, None], dtype="Int64") +``` + +pandas 还有一种专门用于字符串数据的扩展类型,不使用 NumPy 对象数组(需要安装 pyarrow 库): + +```py +In [143]: s = pd.Series(['one', 'two', None, 'three'], dtype=pd.StringDtype()) + +In [144]: s +Out[144]: +0 one +1 two +2 +3 three +dtype: string +``` + +这些字符串数组通常使用更少的内存,并且在对大型数据集进行操作时通常更高效。 + +另一个重要的扩展类型是`Categorical`,我们将在 Categorical Data 中更详细地讨论。截至本文撰写时,可用的扩展类型的相对完整列表在表 7.3 中。 + +扩展类型可以传递给 Series 的`astype`方法,允许您在数据清理过程中轻松转换: + +```py +In [145]: df = pd.DataFrame({"A": [1, 2, None, 4], + .....: "B": ["one", "two", "three", None], + .....: "C": [False, None, False, True]}) + +In [146]: df +Out[146]: + A B C +0 1.0 one False +1 2.0 two None +2 NaN three False +3 4.0 None True + +In [147]: df["A"] = df["A"].astype("Int64") + +In [148]: df["B"] = df["B"].astype("string") + +In [149]: df["C"] = df["C"].astype("boolean") + +In [150]: df +Out[150]: + A B C +0 1 one False +1 2 two +2 three False +3 4 True +``` + +表 7.3:pandas 扩展数据类型 + +| 扩展类型 | 描述 | +| --- | --- | +| `BooleanDtype` | 可空布尔数据,传递字符串时使用`"boolean"` | +| `CategoricalDtype` | 分类数据类型,传递字符串时使用`"category"` | +| `DatetimeTZDtype` | 带时区的日期时间 | +| `Float32Dtype` | 32 位可空浮点数,传递字符串时使用`"Float32"` | +| `Float64Dtype` | 64 位可空浮点数,传递字符串时使用`"Float64"` | +| `Int8Dtype` | 8 位可空有符号整数,传递字符串时使用`"Int8"` | +| `Int16Dtype` | 16 位可空有符号整数,传递字符串时使用`"Int16"` | +| `Int32Dtype` | 32 位可空有符号整数,传递字符串时使用`"Int32"` | +| `Int64Dtype` | 64 位可空有符号整数,传递字符串时使用`"Int64"` | +| `UInt8Dtype` | 8 位可空无符号整数,传递字符串时使用`"UInt8"` | +| `UInt16Dtype` | 16 位可空无符号整数,传递字符串时使用`"UInt16"` | +| `UInt32Dtype` | 32 位可空无符号整数,传递字符串时使用`"UInt32"` | + +| `UInt64Dtype` | 64 位可空无符号整数,在传递为字符串时使用`"UInt64"` | + +## 7.4 字符串操作 + +Python 长期以来一直是一种流行的原始数据处理语言,部分原因是它易于用于字符串和文本处理。大多数文本操作都可以通过字符串对象的内置方法简化。对于更复杂的模式匹配和文本操作,可能需要使用正则表达式。pandas 通过使您能够简洁地在整个数据数组上应用字符串和正则表达式,另外处理了缺失数据的烦恼。 + +### Python 内置字符串对象方法 + +在许多字符串处理和脚本应用程序中,内置字符串方法已经足够。例如,逗号分隔的字符串可以使用`split`分割成多个部分: + +```py +In [151]: val = "a,b, guido" + +In [152]: val.split(",") +Out[152]: ['a', 'b', ' guido'] +``` + +`split`通常与`strip`结合使用以修剪空格(包括换行符): + +```py +In [153]: pieces = [x.strip() for x in val.split(",")] + +In [154]: pieces +Out[154]: ['a', 'b', 'guido'] +``` + +这些子字符串可以使用加法和双冒号分隔符连接在一起: + +```py +In [155]: first, second, third = pieces + +In [156]: first + "::" + second + "::" + third +Out[156]: 'a::b::guido' +``` + +但这并不是一种实用的通用方法。更快速和更符合 Python 风格的方法是将列表或元组传递给字符串`"::"`上的`join`方法: + +```py +In [157]: "::".join(pieces) +Out[157]: 'a::b::guido' +``` + +其他方法涉及定位子字符串。使用 Python 的`in`关键字是检测子字符串的最佳方法,尽管也可以使用`index`和`find`: + +```py +In [158]: "guido" in val +Out[158]: True + +In [159]: val.index(",") +Out[159]: 1 + +In [160]: val.find(":") +Out[160]: -1 +``` + +请注意,`find`和`index`之间的区别在于,如果未找到字符串,`index`会引发异常(而不是返回-1): + +```py +In [161]: val.index(":") +--------------------------------------------------------------------------- +ValueError Traceback (most recent call last) + in +----> 1 val.index(":") +ValueError: substring not found +``` + +相关地,`count`返回特定子字符串的出现次数: + +```py +In [162]: val.count(",") +Out[162]: 2 +``` + +`replace`将一个模式的出现替换为另一个。通常也用于通过传递空字符串来删除模式: + +```py +In [163]: val.replace(",", "::") +Out[163]: 'a::b:: guido' + +In [164]: val.replace(",", "") +Out[164]: 'ab guido' +``` + +请参阅表 7.4 以获取 Python 的一些字符串方法列表。 + +正则表达式也可以与许多这些操作一起使用,您将看到。 + +表 7.4:Python 内置字符串方法 + +| 方法 | 描述 | +| --- | --- | +| `count` | 返回字符串中子字符串的非重叠出现次数 | +| `endswith` | 如果字符串以后缀结尾,则返回`True` | +| `startswith` | 如果字符串以前缀开头,则返回`True` | +| `join` | 用作分隔符将字符串用于连接其他字符串序列 | +| `index` | 如果在字符串中找到传递的子字符串,则返回第一个出现的起始索引;否则,如果未找到,则引发`ValueError` | +| `find` | 返回字符串中*第一个*出现的子字符串的第一个字符的位置;类似于`index`,但如果未找到则返回-1 | +| `rfind` | 返回字符串中*最后*出现的子字符串的第一个字符的位置;如果未找到则返回-1 | +| `replace` | 用另一个字符串替换字符串的出现 | +| `strip, rstrip, lstrip` | 修剪空格,包括右侧、左侧或两侧的换行符 | +| `split` | 使用传递的分隔符将字符串拆分为子字符串列表 | +| `lower` | 将字母字符转换为小写 | +| `upper` | 将字母字符转换为大写 | +| `casefold` | 将字符转换为小写,并将任何区域特定的可变字符组合转换为一个通用的可比较形式 | +| `ljust, rjust` | 分别左对齐或右对齐;用空格(或其他填充字符)填充字符串的对侧,以返回具有最小宽度的字符串 | + +### 正则表达式 + +*正则表达式*提供了一种灵活的方式来在文本中搜索或匹配(通常更复杂的)字符串模式。单个表达式,通常称为*regex*,是根据正则表达式语言形成的字符串。Python 的内置`re`模块负责将正则表达式应用于字符串;我将在这里给出一些示例。 + +注意 + +编写正则表达式的艺术可能是一个单独的章节,因此超出了本书的范围。互联网和其他书籍上有许多优秀的教程和参考资料。 + +`re` 模块的函数分为三类:模式匹配、替换和拆分。当然,这些都是相关的;正则表达式描述了要在文本中定位的模式,然后可以用于许多目的。让我们看一个简单的例子:假设我们想要使用可变数量的空白字符(制表符、空格和换行符)来拆分字符串。 + +描述一个或多个空白字符的正则表达式是 `\s+`: + +```py +In [165]: import re + +In [166]: text = "foo bar\t baz \tqux" + +In [167]: re.split(r"\s+", text) +Out[167]: ['foo', 'bar', 'baz', 'qux'] +``` + +当您调用 `re.split(r"\s+", text)` 时,正则表达式首先被 *编译*,然后在传递的文本上调用其 `split` 方法。您可以使用 `re.compile` 自己编译正则表达式,形成一个可重用的正则表达式对象: + +```py +In [168]: regex = re.compile(r"\s+") + +In [169]: regex.split(text) +Out[169]: ['foo', 'bar', 'baz', 'qux'] +``` + +如果您想要获取与正则表达式匹配的所有模式的列表,可以使用 `findall` 方法: + +```py +In [170]: regex.findall(text) +Out[170]: [' ', '\t ', ' \t'] +``` + +注意 + +为了避免在正则表达式中使用 `\` 进行不必要的转义,请使用 *原始* 字符串字面量,如 `r"C:\x"`,而不是等效的 `"C:\\x"`。 + +如果您打算将相同的表达式应用于许多字符串,强烈建议使用 `re.compile` 创建一个正则表达式对象;这样可以节省 CPU 周期。 + +`match` 和 `search` 与 `findall` 密切相关。虽然 `findall` 返回字符串中的所有匹配项,但 `search` 只返回第一个匹配项。更严格地说,`match` *仅* 在字符串开头匹配。作为一个不太琐碎的例子,让我们考虑一个文本块和一个能够识别大多数电子邮件地址的正则表达式: + +```py +text = """Dave dave@google.com +Steve steve@gmail.com +Rob rob@gmail.com +Ryan ryan@yahoo.com""" +pattern = r"[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}" + +# re.IGNORECASE makes the regex case insensitive +regex = re.compile(pattern, flags=re.IGNORECASE) +``` + +在文本上使用 `findall` 会产生一个电子邮件地址列表: + +```py +In [172]: regex.findall(text) +Out[172]: +['dave@google.com', + 'steve@gmail.com', + 'rob@gmail.com', + 'ryan@yahoo.com'] +``` + +`search` 为文本中的第一个电子邮件地址返回一个特殊的匹配对象。对于前面的正则表达式,匹配对象只能告诉我们模式在字符串中的起始和结束位置: + +```py +In [173]: m = regex.search(text) + +In [174]: m +Out[174]: + +In [175]: text[m.start():m.end()] +Out[175]: 'dave@google.com' +``` + +`regex.match` 返回 `None`,因为它只会匹配如果模式出现在字符串的开头时: + +```py +In [176]: print(regex.match(text)) +None +``` + +相关地,`sub` 将返回一个新字符串,其中模式的出现被新字符串替换: + +```py +In [177]: print(regex.sub("REDACTED", text)) +Dave REDACTED +Steve REDACTED +Rob REDACTED +Ryan REDACTED +``` + +假设您想要查找电子邮件地址,并同时将每个地址分成三个组件:用户名、域名和域后缀。为此,请在模式的部分周围加上括号以进行分段: + +```py +In [178]: pattern = r"([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\.([A-Z]{2,4})" + +In [179]: regex = re.compile(pattern, flags=re.IGNORECASE) +``` + +由此修改后的正则表达式生成的匹配对象将使用其 `groups` 方法返回模式组件的元组: + +```py +In [180]: m = regex.match("wesm@bright.net") + +In [181]: m.groups() +Out[181]: ('wesm', 'bright', 'net') +``` + +当模式有组时,`findall` 返回一个元组列表: + +```py +In [182]: regex.findall(text) +Out[182]: +[('dave', 'google', 'com'), + ('steve', 'gmail', 'com'), + ('rob', 'gmail', 'com'), + ('ryan', 'yahoo', 'com')] +``` + +`sub` 还可以使用特殊符号如 `\1` 和 `\2` 访问每个匹配中的组。符号 `\1` 对应于第一个匹配组,`\2` 对应于第二个,依此类推: + +```py +In [183]: print(regex.sub(r"Username: \1, Domain: \2, Suffix: \3", text)) +Dave Username: dave, Domain: google, Suffix: com +Steve Username: steve, Domain: gmail, Suffix: com +Rob Username: rob, Domain: gmail, Suffix: com +Ryan Username: ryan, Domain: yahoo, Suffix: com +``` + +Python 中的正则表达式还有很多内容,其中大部分超出了本书的范围。表 7.5 提供了一个简要总结。 + +表 7.5:正则表达式方法 + +| 方法 | 描述 | +| --- | --- | +| `findall` | 返回字符串中所有非重叠匹配模式的列表 | +| `finditer` | 类似于 `findall`,但返回一个迭代器 | +| `match` | 在字符串开头匹配模式,并可选择将模式组件分段;如果模式匹配,则返回一个匹配对象,否则返回 `None` | +| `search` | 扫描字符串以查找与模式匹配的内容,如果匹配,则返回一个匹配对象;与 `match` 不同,匹配可以出现在字符串的任何位置,而不仅仅是在开头 | +| `split` | 在每次模式出现时将字符串分割成片段 | + +| `sub, subn` | 用替换表达式替换字符串中所有 (`sub`) 或前 `n` 次出现 (`subn`) 的模式;使用符号 `\1, \2, ...` 来引用替换字符串中的匹配组元素 | + +### pandas 中的字符串函数 + +清理混乱的数据集以进行分析通常需要大量的字符串操作。为了使事情更加复杂,包含字符串的列有时会有缺失数据: + +```py +In [184]: data = {"Dave": "dave@google.com", "Steve": "steve@gmail.com", + .....: "Rob": "rob@gmail.com", "Wes": np.nan} + +In [185]: data = pd.Series(data) + +In [186]: data +Out[186]: +Dave dave@google.com +Steve steve@gmail.com +Rob rob@gmail.com +Wes NaN +dtype: object + +In [187]: data.isna() +Out[187]: +Dave False +Steve False +Rob False +Wes True +dtype: bool +``` + +可以将字符串和正则表达式方法应用于每个值(传递 `lambda` 或其他函数)使用 `data.map`,但它将在 NA(空值)上失败。为了应对这一情况,Series 具有面向数组的字符串操作方法,可以跳过并传播 NA 值。这些方法通过 Series 的 `str` 属性访问;例如,我们可以使用 `str.contains` 检查每个电子邮件地址中是否包含 `"gmail"`: + +```py +In [188]: data.str.contains("gmail") +Out[188]: +Dave False +Steve True +Rob True +Wes NaN +dtype: object +``` + +请注意,此操作的结果具有 `object` 类型。pandas 具有提供对字符串、整数和布尔数据进行专门处理的*扩展类型*,这些类型在处理缺失数据时一直存在一些问题: + +```py +In [189]: data_as_string_ext = data.astype('string') + +In [190]: data_as_string_ext +Out[190]: +Dave dave@google.com +Steve steve@gmail.com +Rob rob@gmail.com +Wes +dtype: string + +In [191]: data_as_string_ext.str.contains("gmail") +Out[191]: +Dave False +Steve True +Rob True +Wes +dtype: boolean +``` + +更详细地讨论了扩展类型,请参阅扩展数据类型。 + +也可以使用正则表达式,以及任何 `re` 选项,如 `IGNORECASE`: + +```py +In [192]: pattern = r"([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\.([A-Z]{2,4})" + +In [193]: data.str.findall(pattern, flags=re.IGNORECASE) +Out[193]: +Dave [(dave, google, com)] +Steve [(steve, gmail, com)] +Rob [(rob, gmail, com)] +Wes NaN +dtype: object +``` + +有几种进行矢量化元素检索的方法。可以使用 `str.get` 或索引到 `str` 属性: + +```py +In [194]: matches = data.str.findall(pattern, flags=re.IGNORECASE).str[0] + +In [195]: matches +Out[195]: +Dave (dave, google, com) +Steve (steve, gmail, com) +Rob (rob, gmail, com) +Wes NaN +dtype: object + +In [196]: matches.str.get(1) +Out[196]: +Dave google +Steve gmail +Rob gmail +Wes NaN +dtype: object +``` + +您也可以使用以下语法对字符串进行切片: + +```py +In [197]: data.str[:5] +Out[197]: +Dave dave@ +Steve steve +Rob rob@g +Wes NaN +dtype: object +``` + +`str.extract` 方法将返回正则表达式的捕获组作为 DataFrame: + +```py +In [198]: data.str.extract(pattern, flags=re.IGNORECASE) +Out[198]: + 0 1 2 +Dave dave google com +Steve steve gmail com +Rob rob gmail com +Wes NaN NaN NaN +``` + +查看更多 pandas 字符串方法,请参阅表 7.6。 + +表 7.6: Series 字符串方法的部分列表 + +| 方法 | 描述 | +| --- | --- | +| `cat` | 逐元素连接字符串,可选分隔符 | +| `contains` | 如果每个字符串包含模式/正则表达式,则返回布尔数组 | +| `count` | 计算模式的出现次数 | +| `extract` | 使用具有组的正则表达式从字符串 Series 中提取一个或多个字符串;结果将是一个每组一列的 DataFrame | +| `endswith` | 对每个元素等同于 `x.endswith(pattern)` | +| `startswith` | 对每个元素等同于 `x.startswith(pattern)` | +| `findall` | 计算每个字符串的模式/正则表达式的所有出现的列表 | +| `get` | 索引到每个元素(检索第 *i* 个元素) | +| `isalnum` | 等同于内置的 `str.alnum` | +| `isalpha` | 等同于内置的 `str.isalpha` | +| `isdecimal` | 等同于内置的 `str.isdecimal` | +| `isdigit` | 等同于内置的 `str.isdigit` | +| `islower` | 等同于内置的 `str.islower` | +| `isnumeric` | 等同于内置的 `str.isnumeric` | +| `isupper` | 等同于内置的 `str.isupper` | +| `join` | 使用传递的分隔符连接 Series 中每个元素的字符串 | +| `len` | 计算每个字符串的长度 | +| `lower, upper` | 转换大小写;对每个元素等同于 `x.lower()` 或 `x.upper()` | +| `match` | 对每个元素使用传递的正则表达式的 `re.match`,返回是否匹配的 `True` 或 `False` | +| `pad` | 在字符串的左侧、右侧或两侧添加空格 | +| `center` | 等同于 `pad(side="both")` | +| `repeat` | 复制值(例如,`s.str.repeat(3)` 相当于对每个字符串执行 `x * 3`) | +| `replace` | 用其他字符串替换模式/正则表达式的出现 | +| `slice` | 对 Series 中的每个字符串进行切片 | +| `split` | 按分隔符或正则表达式拆分字符串 | +| `strip` | 从两侧修剪空白,包括换行符 | +| `rstrip` | 修剪右侧的空白 | + +| `lstrip` | 修剪左侧的空白 | + +## 7.5 分类数据 + +本节介绍了 pandas 的 `Categorical` 类型。我将展示如何通过使用它在某些 pandas 操作中实现更好的性能和内存使用。我还介绍了一些工具,这些工具可能有助于在统计和机器学习应用中使用分类数据。 + +### 背景和动机 + +通常,表中的一列可能包含较小一组不同值的重复实例。我们已经看到了像 `unique` 和 `value_counts` 这样的函数,它们使我们能够从数组中提取不同的值并分别计算它们的频率: + +```py +In [199]: values = pd.Series(['apple', 'orange', 'apple', + .....: 'apple'] * 2) + +In [200]: values +Out[200]: +0 apple +1 orange +2 apple +3 apple +4 apple +5 orange +6 apple +7 apple +dtype: object + +In [201]: pd.unique(values) +Out[201]: array(['apple', 'orange'], dtype=object) + +In [202]: pd.value_counts(values) +Out[202]: +apple 6 +orange 2 +Name: count, dtype: int64 +``` + +许多数据系统(用于数据仓库、统计计算或其他用途)已经开发了专门的方法来表示具有重复值的数据,以实现更高效的存储和计算。在数据仓库中,最佳实践是使用所谓的*维度表*,其中包含不同的值,并将主要观察结果存储为引用维度表的整数键: + +```py +In [203]: values = pd.Series([0, 1, 0, 0] * 2) + +In [204]: dim = pd.Series(['apple', 'orange']) + +In [205]: values +Out[205]: +0 0 +1 1 +2 0 +3 0 +4 0 +5 1 +6 0 +7 0 +dtype: int64 + +In [206]: dim +Out[206]: +0 apple +1 orange +dtype: object +``` + +我们可以使用`take`方法恢复原始的字符串 Series: + +```py +In [207]: dim.take(values) +Out[207]: +0 apple +1 orange +0 apple +0 apple +0 apple +1 orange +0 apple +0 apple +dtype: object +``` + +这种整数表示被称为*分类*或*字典编码*表示。不同值的数组可以称为数据的*类别*、*字典*或*级别*。在本书中,我们将使用术语*分类*和*类别*。引用类别的整数值称为*类别代码*或简称*代码*。 + +在进行分析时,分类表示可以显著提高性能。您还可以在保持代码不变的情况下对类别执行转换。一些可以以相对较低的成本进行的示例转换包括: + ++ 重命名类别 + ++ 追加一个新类别而不改变现有类别的顺序或位置 + +### pandas 中的分类扩展类型 + +pandas 具有专门的`Categorical`扩展类型,用于保存使用基于整数的分类表示或*编码*的数据。这是一种流行的数据压缩技术,适用于具有许多相似值出现的数据,并且可以提供更快的性能和更低的内存使用,特别是对于字符串数据。 + +让我们考虑之前的示例 Series: + +```py +In [208]: fruits = ['apple', 'orange', 'apple', 'apple'] * 2 + +In [209]: N = len(fruits) + +In [210]: rng = np.random.default_rng(seed=12345) + +In [211]: df = pd.DataFrame({'fruit': fruits, + .....: 'basket_id': np.arange(N), + .....: 'count': rng.integers(3, 15, size=N), + .....: 'weight': rng.uniform(0, 4, size=N)}, + .....: columns=['basket_id', 'fruit', 'count', 'weight']) + +In [212]: df +Out[212]: + basket_id fruit count weight +0 0 apple 11 1.564438 +1 1 orange 5 1.331256 +2 2 apple 12 2.393235 +3 3 apple 6 0.746937 +4 4 apple 5 2.691024 +5 5 orange 12 3.767211 +6 6 apple 10 0.992983 +7 7 apple 11 3.795525 +``` + +这里,`df['fruit']`是 Python 字符串对象的数组。我们可以通过调用以下方式将其转换为分类: + +```py +In [213]: fruit_cat = df['fruit'].astype('category') + +In [214]: fruit_cat +Out[214]: +0 apple +1 orange +2 apple +3 apple +4 apple +5 orange +6 apple +7 apple +Name: fruit, dtype: category +Categories (2, object): ['apple', 'orange'] +``` + +现在,`fruit_cat`的值是`pandas.Categorical`的一个实例,您可以通过`.array`属性访问: + +```py +In [215]: c = fruit_cat.array + +In [216]: type(c) +Out[216]: pandas.core.arrays.categorical.Categorical +``` + +`Categorical`对象具有`categories`和`codes`属性: + +```py +In [217]: c.categories +Out[217]: Index(['apple', 'orange'], dtype='object') + +In [218]: c.codes +Out[218]: array([0, 1, 0, 0, 0, 1, 0, 0], dtype=int8) +``` + +可以使用`cat`访问器更轻松地访问这些,这将在 Categorical Methods 中很快解释。 + +获取代码和类别之间的映射的一个有用技巧是: + +```py +In [219]: dict(enumerate(c.categories)) +Out[219]: {0: 'apple', 1: 'orange'} +``` + +您可以通过分配转换后的结果将 DataFrame 列转换为分类: + +```py +In [220]: df['fruit'] = df['fruit'].astype('category') + +In [221]: df["fruit"] +Out[221]: +0 apple +1 orange +2 apple +3 apple +4 apple +5 orange +6 apple +7 apple +Name: fruit, dtype: category +Categories (2, object): ['apple', 'orange'] +``` + +您还可以直接从其他类型的 Python 序列创建`pandas.Categorical`: + +```py +In [222]: my_categories = pd.Categorical(['foo', 'bar', 'baz', 'foo', 'bar']) + +In [223]: my_categories +Out[223]: +['foo', 'bar', 'baz', 'foo', 'bar'] +Categories (3, object): ['bar', 'baz', 'foo'] +``` + +如果您从另一个来源获得了分类编码数据,可以使用替代的`from_codes`构造函数: + +```py +In [224]: categories = ['foo', 'bar', 'baz'] + +In [225]: codes = [0, 1, 2, 0, 0, 1] + +In [226]: my_cats_2 = pd.Categorical.from_codes(codes, categories) + +In [227]: my_cats_2 +Out[227]: +['foo', 'bar', 'baz', 'foo', 'foo', 'bar'] +Categories (3, object): ['foo', 'bar', 'baz'] +``` + +除非明确指定,分类转换假定类别没有特定的排序。因此,`categories`数组的顺序可能会根据输入数据的顺序而有所不同。在使用`from_codes`或任何其他构造函数时,您可以指示类别具有有意义的排序: + +```py +In [228]: ordered_cat = pd.Categorical.from_codes(codes, categories, + .....: ordered=True) + +In [229]: ordered_cat +Out[229]: +['foo', 'bar', 'baz', 'foo', 'foo', 'bar'] +Categories (3, object): ['foo' < 'bar' < 'baz'] +``` + +输出`[foo < bar < baz]`表示`'foo'`在排序中位于`'bar'`之前,依此类推。无序的分类实例可以通过`as_ordered`变为有序: + +```py +In [230]: my_cats_2.as_ordered() +Out[230]: +['foo', 'bar', 'baz', 'foo', 'foo', 'bar'] +Categories (3, object): ['foo' < 'bar' < 'baz'] +``` + +最后一点,分类数据不一定是字符串,尽管我只展示了字符串示例。分类数组可以由任何不可变的值类型组成。 + +### 使用 Categoricals 进行计算 + +与非编码版本(如字符串数组)相比,在 pandas 中使用`Categorical`通常表现相同。在处理分类数据时,pandas 的某些部分,如`groupby`函数,表现更好。还有一些函数可以利用`ordered`标志。 + +让我们考虑一些随机数值数据,并使用`pandas.qcut`分箱函数。这将返回`pandas.Categorical`;我们在本书的早期使用了`pandas.cut`,但忽略了分类的工作原理的细节: + +```py +In [231]: rng = np.random.default_rng(seed=12345) + +In [232]: draws = rng.standard_normal(1000) + +In [233]: draws[:5] +Out[233]: array([-1.4238, 1.2637, -0.8707, -0.2592, -0.0753]) +``` + +让我们计算一下这些数据的四分位数分箱,并提取一些统计数据: + +```py +In [234]: bins = pd.qcut(draws, 4) + +In [235]: bins +Out[235]: +[(-3.121, -0.675], (0.687, 3.211], (-3.121, -0.675], (-0.675, 0.0134], (-0.675, 0 +.0134], ..., (0.0134, 0.687], (0.0134, 0.687], (-0.675, 0.0134], (0.0134, 0.687], + (-0.675, 0.0134]] +Length: 1000 +Categories (4, interval[float64, right]): [(-3.121, -0.675] < (-0.675, 0.0134] < +(0.0134, 0.687] < + (0.687, 3.211]] +``` + +尽管有用,确切的样本四分位数可能不如四分位数名称有用于生成报告。我们可以通过`qcut`的`labels`参数实现这一点: + +```py +In [236]: bins = pd.qcut(draws, 4, labels=['Q1', 'Q2', 'Q3', 'Q4']) + +In [237]: bins +Out[237]: +['Q1', 'Q4', 'Q1', 'Q2', 'Q2', ..., 'Q3', 'Q3', 'Q2', 'Q3', 'Q2'] +Length: 1000 +Categories (4, object): ['Q1' < 'Q2' < 'Q3' < 'Q4'] + +In [238]: bins.codes[:10] +Out[238]: array([0, 3, 0, 1, 1, 0, 0, 2, 2, 0], dtype=int8) +``` + +标记的`bins`分类不包含数据中的箱边信息,因此我们可以使用`groupby`来提取一些摘要统计信息: + +```py +In [239]: bins = pd.Series(bins, name='quartile') + +In [240]: results = (pd.Series(draws) + .....: .groupby(bins) + .....: .agg(['count', 'min', 'max']) + .....: .reset_index()) + +In [241]: results +Out[241]: + quartile count min max +0 Q1 250 -3.119609 -0.678494 +1 Q2 250 -0.673305 0.008009 +2 Q3 250 0.018753 0.686183 +3 Q4 250 0.688282 3.211418 +``` + +结果中的`'quartile'`列保留了来自`bins`的原始分类信息,包括排序: + +```py +In [242]: results['quartile'] +Out[242]: +0 Q1 +1 Q2 +2 Q3 +3 Q4 +Name: quartile, dtype: category +Categories (4, object): ['Q1' < 'Q2' < 'Q3' < 'Q4'] +``` + +#### 分类数据的更好性能 + +在本节开头,我说过分类类型可以提高性能和内存使用,所以让我们看一些例子。考虑一些具有 1000 万个元素和少量不同类别的 Series: + +```py +In [243]: N = 10_000_000 + +In [244]: labels = pd.Series(['foo', 'bar', 'baz', 'qux'] * (N // 4)) +``` + +现在我们将`labels`转换为分类: + +```py +In [245]: categories = labels.astype('category') +``` + +现在我们注意到`labels`使用的内存比`categories`要多得多: + +```py +In [246]: labels.memory_usage(deep=True) +Out[246]: 600000128 + +In [247]: categories.memory_usage(deep=True) +Out[247]: 10000540 +``` + +当然,转换为类别并不是免费的,但这是一次性的成本: + +```py +In [248]: %time _ = labels.astype('category') +CPU times: user 279 ms, sys: 6.06 ms, total: 285 ms +Wall time: 285 ms +``` + +由于底层算法使用基于整数的代码数组而不是字符串数组,因此使用分类的 GroupBy 操作可以显着提高性能。这里我们比较了使用 GroupBy 机制的`value_counts()`的性能: + +```py +In [249]: %timeit labels.value_counts() +331 ms +- 5.39 ms per loop (mean +- std. dev. of 7 runs, 1 loop each) + +In [250]: %timeit categories.value_counts() +15.6 ms +- 152 us per loop (mean +- std. dev. of 7 runs, 100 loops each) +``` + +### 分类方法 + +包含分类数据的 Series 具有几个类似于`Series.str`专门的字符串方法的特殊方法。这也提供了方便访问类别和代码。考虑 Series: + +```py +In [251]: s = pd.Series(['a', 'b', 'c', 'd'] * 2) + +In [252]: cat_s = s.astype('category') + +In [253]: cat_s +Out[253]: +0 a +1 b +2 c +3 d +4 a +5 b +6 c +7 d +dtype: category +Categories (4, object): ['a', 'b', 'c', 'd'] +``` + +特殊的*访问器*属性`cat`提供了对分类方法的访问: + +```py +In [254]: cat_s.cat.codes +Out[254]: +0 0 +1 1 +2 2 +3 3 +4 0 +5 1 +6 2 +7 3 +dtype: int8 + +In [255]: cat_s.cat.categories +Out[255]: Index(['a', 'b', 'c', 'd'], dtype='object') +``` + +假设我们知道此数据的实际类别集扩展到数据中观察到的四个值之外。我们可以使用`set_categories`方法来更改它们: + +```py +In [256]: actual_categories = ['a', 'b', 'c', 'd', 'e'] + +In [257]: cat_s2 = cat_s.cat.set_categories(actual_categories) + +In [258]: cat_s2 +Out[258]: +0 a +1 b +2 c +3 d +4 a +5 b +6 c +7 d +dtype: category +Categories (5, object): ['a', 'b', 'c', 'd', 'e'] +``` + +虽然数据看起来没有改变,但使用它们的操作将反映新的类别。例如,如果存在,`value_counts`会尊重类别: + +```py +In [259]: cat_s.value_counts() +Out[259]: +a 2 +b 2 +c 2 +d 2 +Name: count, dtype: int64 + +In [260]: cat_s2.value_counts() +Out[260]: +a 2 +b 2 +c 2 +d 2 +e 0 +Name: count, dtype: int64 +``` + +在大型数据集中,分类通常被用作一种方便的工具,用于节省内存和提高性能。在过滤大型 DataFrame 或 Series 之后,许多类别可能不会出现在数据中。为了帮助解决这个问题,我们可以使用`remove_unused_categories`方法来修剪未观察到的类别: + +```py +In [261]: cat_s3 = cat_s[cat_s.isin(['a', 'b'])] + +In [262]: cat_s3 +Out[262]: +0 a +1 b +4 a +5 b +dtype: category +Categories (4, object): ['a', 'b', 'c', 'd'] + +In [263]: cat_s3.cat.remove_unused_categories() +Out[263]: +0 a +1 b +4 a +5 b +dtype: category +Categories (2, object): ['a', 'b'] +``` + +请参见表 7.7 列出的可用分类方法。 + +表 7.7:pandas 中 Series 的分类方法 + +| 方法 | 描述 | +| --- | --- | +| `add_categories` | 在现有类别的末尾追加新的(未使用的)类别 | +| `as_ordered` | 使类别有序 | +| `as_unordered` | 使类别无序 | +| `remove_categories` | 删除类别,将任何删除的值设置为 null | +| `remove_unused_categories` | 删除数据中不存在的任何类别值 | +| `rename_categories` | 用指定的新类别名称集替换类别;不能改变类别数量 | +| `reorder_categories` | 表现类似于`rename_categories`,但也可以改变结果为有序类别 | +| `set_categories` | 用指定的新类别集替换类别;可以添加或删除类别 | + +#### 为建模创建虚拟变量 + +当您使用统计或机器学习工具时,通常会将分类数据转换为*虚拟变量*,也称为*独热*编码。这涉及创建一个 DataFrame,其中每个不同的类别都有一列;这些列包含给定类别的出现为 1,否则为 0。 + +考虑前面的例子: + +```py +In [264]: cat_s = pd.Series(['a', 'b', 'c', 'd'] * 2, dtype='category') +``` + +如本章前面提到的,`pandas.get_dummies`函数将这个一维分类数据转换为包含虚拟变量的 DataFrame: + +```py +In [265]: pd.get_dummies(cat_s, dtype=float) +Out[265]: + a b c d +0 1.0 0.0 0.0 0.0 +1 0.0 1.0 0.0 0.0 +2 0.0 0.0 1.0 0.0 +3 0.0 0.0 0.0 1.0 +4 1.0 0.0 0.0 0.0 +5 0.0 1.0 0.0 0.0 +6 0.0 0.0 1.0 0.0 +7 0.0 0.0 0.0 1.0 +``` + +## 7.6 结论 + +有效的数据准备可以通过使您花更多时间分析数据而不是准备分析数据来显着提高生产率。本章中我们探讨了许多工具,但这里的覆盖范围并不全面。在下一章中,我们将探讨 pandas 的连接和分组功能。 diff --git a/translations/cn/pyda3e_11.md b/translations/cn/pyda3e_11.md new file mode 100644 index 000000000..09991c997 --- /dev/null +++ b/translations/cn/pyda3e_11.md @@ -0,0 +1,1654 @@ +# 八、数据整理:连接、合并和重塑 + +> 原文:[`wesmckinney.com/book/data-wrangling`](https://wesmckinney.com/book/data-wrangling) +> +> 译者:[飞龙](https://github.com/wizardforcel) +> +> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) + + +> 此开放访问网络版本的《Python 数据分析第三版》现已作为[印刷版和数字版](https://amzn.to/3DyLaJc)的伴侣提供。如果您发现任何勘误,请[在此处报告](https://oreilly.com/catalog/0636920519829/errata)。请注意,由 Quarto 生成的本站点的某些方面与 O'Reilly 的印刷版和电子书版本的格式不同。 +> +> 如果您发现本书的在线版本有用,请考虑[订购纸质版](https://amzn.to/3DyLaJc)或[无 DRM 的电子书](https://www.ebooks.com/en-us/book/210644288/python-for-data-analysis/wes-mckinney/?affId=WES398681F)以支持作者。本网站的内容不得复制或再生产。代码示例采用 MIT 许可,可在 GitHub 或 Gitee 上找到。 + +在许多应用程序中,数据可能分布在许多文件或数据库中,或者以不便于分析的形式排列。本章重点介绍帮助组合、连接和重新排列数据的工具。 + +首先,我介绍了 pandas 中*层次索引*的概念,这在某些操作中被广泛使用。然后我深入研究了特定的数据操作。您可以在第十三章:数据分析示例中看到这些工具的各种应用用法。 + +## 8.1 层次索引 + +*层次索引*是 pandas 的一个重要特性,它使您能够在轴上具有多个(两个或更多)索引*级别*。另一种思考方式是,它为您提供了一种以较低维度形式处理较高维度数据的方法。让我们从一个简单的示例开始:创建一个 Series,其索引为列表的列表(或数组): + +```py +In [11]: data = pd.Series(np.random.uniform(size=9), + ....: index=[["a", "a", "a", "b", "b", "c", "c", "d", "d"], + ....: [1, 2, 3, 1, 3, 1, 2, 2, 3]]) + +In [12]: data +Out[12]: +a 1 0.929616 + 2 0.316376 + 3 0.183919 +b 1 0.204560 + 3 0.567725 +c 1 0.595545 + 2 0.964515 +d 2 0.653177 + 3 0.748907 +dtype: float64 +``` + +您看到的是一个带有`MultiIndex`作为索引的 Series 的美化视图。索引显示中的“间隙”表示“使用直接上面的标签”: + +```py +In [13]: data.index +Out[13]: +MultiIndex([('a', 1), + ('a', 2), + ('a', 3), + ('b', 1), + ('b', 3), + ('c', 1), + ('c', 2), + ('d', 2), + ('d', 3)], + ) +``` + +对于具有层次索引的对象,可以进行所谓的*部分*索引,使您能够简洁地选择数据的子集: + +```py +In [14]: data["b"] +Out[14]: +1 0.204560 +3 0.567725 +dtype: float64 + +In [15]: data["b":"c"] +Out[15]: +b 1 0.204560 + 3 0.567725 +c 1 0.595545 + 2 0.964515 +dtype: float64 + +In [16]: data.loc[["b", "d"]] +Out[16]: +b 1 0.204560 + 3 0.567725 +d 2 0.653177 + 3 0.748907 +dtype: float64 +``` + +甚至可以从“内部”级别进行选择。在这里,我从第二个索引级别选择所有具有值`2`的值: + +```py +In [17]: data.loc[:, 2] +Out[17]: +a 0.316376 +c 0.964515 +d 0.653177 +dtype: float64 +``` + +层次索引在重塑数据和基于组的操作(如形成数据透视表)中发挥着重要作用。例如,您可以使用其`unstack`方法将这些数据重新排列为 DataFrame: + +```py +In [18]: data.unstack() +Out[18]: + 1 2 3 +a 0.929616 0.316376 0.183919 +b 0.204560 NaN 0.567725 +c 0.595545 0.964515 NaN +d NaN 0.653177 0.748907 +``` + +`unstack`的逆操作是`stack`: + +```py +In [19]: data.unstack().stack() +Out[19]: +a 1 0.929616 + 2 0.316376 + 3 0.183919 +b 1 0.204560 + 3 0.567725 +c 1 0.595545 + 2 0.964515 +d 2 0.653177 + 3 0.748907 +dtype: float64 +``` + +`stack`和`unstack`将在重塑和透视中更详细地探讨。 + +对于 DataFrame,任一轴都可以具有分层索引: + +```py +In [20]: frame = pd.DataFrame(np.arange(12).reshape((4, 3)), + ....: index=[["a", "a", "b", "b"], [1, 2, 1, 2]], + ....: columns=[["Ohio", "Ohio", "Colorado"], + ....: ["Green", "Red", "Green"]]) + +In [21]: frame +Out[21]: + Ohio Colorado + Green Red Green +a 1 0 1 2 + 2 3 4 5 +b 1 6 7 8 + 2 9 10 11 +``` + +层次级别可以有名称(作为字符串或任何 Python 对象)。如果有的话,这些名称将显示在控制台输出中: + +```py +In [22]: frame.index.names = ["key1", "key2"] + +In [23]: frame.columns.names = ["state", "color"] + +In [24]: frame +Out[24]: +state Ohio Colorado +color Green Red Green +key1 key2 +a 1 0 1 2 + 2 3 4 5 +b 1 6 7 8 + 2 9 10 11 +``` + +这些名称取代了仅用于单级索引的`name`属性。 + +注意 + +请注意,索引名称`"state"`和`"color"`不是行标签(`frame.index`值)的一部分。 + +您可以通过访问其`nlevels`属性来查看索引具有多少级别: + +```py +In [25]: frame.index.nlevels +Out[25]: 2 +``` + +通过部分列索引,您也可以类似地选择列组: + +```py +In [26]: frame["Ohio"] +Out[26]: +color Green Red +key1 key2 +a 1 0 1 + 2 3 4 +b 1 6 7 + 2 9 10 +``` + +`MultiIndex`可以单独创建,然后重复使用;具有级别名称的前述 DataFrame 中的列也可以这样创建: + +```py +pd.MultiIndex.from_arrays([["Ohio", "Ohio", "Colorado"], + ["Green", "Red", "Green"]], + names=["state", "color"]) +``` + +### 重新排序和排序级别 + +有时您可能需要重新排列轴上级别的顺序或按特定级别的值对数据进行排序。`swaplevel`方法接受两个级别编号或名称,并返回一个级别互换的新对象(但数据本身不变): + +```py +In [27]: frame.swaplevel("key1", "key2") +Out[27]: +state Ohio Colorado +color Green Red Green +key2 key1 +1 a 0 1 2 +2 a 3 4 5 +1 b 6 7 8 +2 b 9 10 11 +``` + +`sort_index`默认按所有索引级别词典顺序对数据进行排序,但您可以选择通过传递`level`参数仅使用单个级别或一组级别进行排序。例如: + +```py +In [28]: frame.sort_index(level=1) +Out[28]: +state Ohio Colorado +color Green Red Green +key1 key2 +a 1 0 1 2 +b 1 6 7 8 +a 2 3 4 5 +b 2 9 10 11 + +In [29]: frame.swaplevel(0, 1).sort_index(level=0) +Out[29]: +state Ohio Colorado +color Green Red Green +key2 key1 +1 a 0 1 2 + b 6 7 8 +2 a 3 4 5 + b 9 10 11 +``` + +注意 + +如果索引按字典顺序排序,从最外层级别开始,那么在具有分层索引的对象上进行数据选择性能要好得多——也就是说,调用`sort_index(level=0)`或`sort_index()`的结果。 + +### 按级别汇总统计 + +DataFrame 和 Series 上的许多描述性和汇总统计信息具有`level`选项,您可以在特定轴上指定要按级别聚合的级别。考虑上面的 DataFrame;我们可以按行或列的级别进行聚合,如下所示: + +```py +In [30]: frame.groupby(level="key2").sum() +Out[30]: +state Ohio Colorado +color Green Red Green +key2 +1 6 8 10 +2 12 14 16 + +In [31]: frame.groupby(level="color", axis="columns").sum() +Out[31]: +color Green Red +key1 key2 +a 1 2 1 + 2 8 4 +b 1 14 7 + 2 20 10 +``` + +我们将在第十章:数据聚合和分组操作中更详细地讨论`groupby`。 + +### 使用 DataFrame 的列进行索引 + +希望使用一个或多个 DataFrame 列作为行索引并不罕见;或者,您可能希望将行索引移入 DataFrame 的列中。这是一个示例 DataFrame: + +```py +In [32]: frame = pd.DataFrame({"a": range(7), "b": range(7, 0, -1), + ....: "c": ["one", "one", "one", "two", "two", + ....: "two", "two"], + ....: "d": [0, 1, 2, 0, 1, 2, 3]}) + +In [33]: frame +Out[33]: + a b c d +0 0 7 one 0 +1 1 6 one 1 +2 2 5 one 2 +3 3 4 two 0 +4 4 3 two 1 +5 5 2 two 2 +6 6 1 two 3 +``` + +DataFrame 的`set_index`函数将使用一个或多个列作为索引创建一个新的 DataFrame: + +```py +In [34]: frame2 = frame.set_index(["c", "d"]) + +In [35]: frame2 +Out[35]: + a b +c d +one 0 0 7 + 1 1 6 + 2 2 5 +two 0 3 4 + 1 4 3 + 2 5 2 + 3 6 1 +``` + +默认情况下,列会从 DataFrame 中移除,但您可以通过向`set_index`传递`drop=False`来保留它们: + +```py +In [36]: frame.set_index(["c", "d"], drop=False) +Out[36]: + a b c d +c d +one 0 0 7 one 0 + 1 1 6 one 1 + 2 2 5 one 2 +two 0 3 4 two 0 + 1 4 3 two 1 + 2 5 2 two 2 + 3 6 1 two 3 +``` + +另一方面,`reset_index`的作用与`set_index`相反;层次化索引级别被移动到列中: + +```py +In [37]: frame2.reset_index() +Out[37]: + c d a b +0 one 0 0 7 +1 one 1 1 6 +2 one 2 2 5 +3 two 0 3 4 +4 two 1 4 3 +5 two 2 5 2 +6 two 3 6 1 +``` + +## 8.2 合并和组合数据集 + +pandas 对象中包含的数据可以以多种方式组合: + +`pandas.merge` + +基于一个或多个键连接 DataFrame 中的行。这将为使用 SQL 或其他关系数据库的用户提供熟悉的操作,因为它实现了数据库*join*操作。 + +`pandas.concat` + +沿轴连接或“堆叠”对象。 + +`combine_first` + +将重叠数据拼接在一起,用另一个对象中的值填充另一个对象中的缺失值。 + +我将逐个讨论这些并给出一些示例。它们将在本书的其余部分的示例中使用。 + +### 数据库风格的 DataFrame 连接 + +*合并*或*连接*操作通过使用一个或多个*键*链接行来合并数据集。这些操作在关系数据库(例如基于 SQL 的数据库)中尤为重要。pandas 中的`pandas.merge`函数是使用这些算法在您的数据上的主要入口点。 + +让我们从一个简单的例子开始: + +```py +In [38]: df1 = pd.DataFrame({"key": ["b", "b", "a", "c", "a", "a", "b"], + ....: "data1": pd.Series(range(7), dtype="Int64")}) + +In [39]: df2 = pd.DataFrame({"key": ["a", "b", "d"], + ....: "data2": pd.Series(range(3), dtype="Int64")}) + +In [40]: df1 +Out[40]: + key data1 +0 b 0 +1 b 1 +2 a 2 +3 c 3 +4 a 4 +5 a 5 +6 b 6 + +In [41]: df2 +Out[41]: + key data2 +0 a 0 +1 b 1 +2 d 2 +``` + +在这里,我使用 pandas 的`Int64`扩展类型来表示可空整数,详细讨论请参见第 7.3 章:扩展数据类型。 + +这是一个*多对一*连接的示例;`df1`中的数据有多行标记为`a`和`b`,而`df2`中的每个值在`key`列中只有一行。使用这些对象调用`pandas.merge`,我们得到: + +```py +In [42]: pd.merge(df1, df2) +Out[42]: + key data1 data2 +0 b 0 1 +1 b 1 1 +2 b 6 1 +3 a 2 0 +4 a 4 0 +5 a 5 0 +``` + +请注意,我没有指定要连接的列。如果没有指定该信息,`pandas.merge`将使用重叠的列名作为键。不过,最好明确指定: + +```py +In [43]: pd.merge(df1, df2, on="key") +Out[43]: + key data1 data2 +0 b 0 1 +1 b 1 1 +2 b 6 1 +3 a 2 0 +4 a 4 0 +5 a 5 0 +``` + +一般来说,在`pandas.merge`操作中,列的输出顺序是不确定的。 + +如果每个对象中的列名不同,您可以分别指定它们: + +```py +In [44]: df3 = pd.DataFrame({"lkey": ["b", "b", "a", "c", "a", "a", "b"], + ....: "data1": pd.Series(range(7), dtype="Int64")}) + +In [45]: df4 = pd.DataFrame({"rkey": ["a", "b", "d"], + ....: "data2": pd.Series(range(3), dtype="Int64")}) + +In [46]: pd.merge(df3, df4, left_on="lkey", right_on="rkey") +Out[46]: + lkey data1 rkey data2 +0 b 0 b 1 +1 b 1 b 1 +2 b 6 b 1 +3 a 2 a 0 +4 a 4 a 0 +5 a 5 a 0 +``` + +您可能会注意到结果中缺少`"c"`和`"d"`值及其相关数据。默认情况下,`pandas.merge`执行的是`"inner"`连接;结果中的键是交集,或者是在两个表中都找到的公共集合。其他可能的选项是`"left"`、`"right"`和`"outer"`。外连接取键的并集,结合了应用左连接和右连接的效果: + +```py +In [47]: pd.merge(df1, df2, how="outer") +Out[47]: + key data1 data2 +0 b 0 1 +1 b 1 1 +2 b 6 1 +3 a 2 0 +4 a 4 0 +5 a 5 0 +6 c 3 +7 d 2 + +In [48]: pd.merge(df3, df4, left_on="lkey", right_on="rkey", how="outer") +Out[48]: + lkey data1 rkey data2 +0 b 0 b 1 +1 b 1 b 1 +2 b 6 b 1 +3 a 2 a 0 +4 a 4 a 0 +5 a 5 a 0 +6 c 3 NaN +7 NaN d 2 +``` + +在外连接中,左侧或右侧 DataFrame 对象中与另一个 DataFrame 中的键不匹配的行将在另一个 DataFrame 的列中出现 NA 值。 + +请参阅表 8.1 以获取`how`选项的摘要。 + +表 8.1:使用`how`参数的不同连接类型 + +| 选项 | 行为 | +| --- | --- | +| `how="inner"` | 仅使用在两个表中观察到的键组合 | +| `how="left"` | 使用在左表中找到的所有键组合 | +| `how="right"` | 使用在右表中找到的所有键组合 | +| `how="outer"` | 使用两个表中观察到的所有键组合 | + +*多对多* 合并形成匹配键的笛卡尔积。以下是一个示例: + +```py +In [49]: df1 = pd.DataFrame({"key": ["b", "b", "a", "c", "a", "b"], + ....: "data1": pd.Series(range(6), dtype="Int64")}) + +In [50]: df2 = pd.DataFrame({"key": ["a", "b", "a", "b", "d"], + ....: "data2": pd.Series(range(5), dtype="Int64")}) + +In [51]: df1 +Out[51]: + key data1 +0 b 0 +1 b 1 +2 a 2 +3 c 3 +4 a 4 +5 b 5 + +In [52]: df2 +Out[52]: + key data2 +0 a 0 +1 b 1 +2 a 2 +3 b 3 +4 d 4 + +In [53]: pd.merge(df1, df2, on="key", how="left") +Out[53]: + key data1 data2 +0 b 0 1 +1 b 0 3 +2 b 1 1 +3 b 1 3 +4 a 2 0 +5 a 2 2 +6 c 3 +7 a 4 0 +8 a 4 2 +9 b 5 1 +10 b 5 3 +``` + +由于左侧 DataFrame 中有三行`"b"`,右侧 DataFrame 中有两行`"b"`,因此结果中有六行`"b"`。传递给`how`关键字参数的连接方法仅影响结果中出现的不同键值: + +```py +In [54]: pd.merge(df1, df2, how="inner") +Out[54]: + key data1 data2 +0 b 0 1 +1 b 0 3 +2 b 1 1 +3 b 1 3 +4 b 5 1 +5 b 5 3 +6 a 2 0 +7 a 2 2 +8 a 4 0 +9 a 4 2 +``` + +要使用多个键进行合并,请传递列名列表: + +```py +In [55]: left = pd.DataFrame({"key1": ["foo", "foo", "bar"], + ....: "key2": ["one", "two", "one"], + ....: "lval": pd.Series([1, 2, 3], dtype='Int64')}) + +In [56]: right = pd.DataFrame({"key1": ["foo", "foo", "bar", "bar"], + ....: "key2": ["one", "one", "one", "two"], + ....: "rval": pd.Series([4, 5, 6, 7], dtype='Int64')}) + +In [57]: pd.merge(left, right, on=["key1", "key2"], how="outer") +Out[57]: + key1 key2 lval rval +0 foo one 1 4 +1 foo one 1 5 +2 foo two 2 +3 bar one 3 6 +4 bar two 7 +``` + +要确定根据合并方法的选择将出现在结果中的哪些键组合,请将多个键视为形成元组数组,用作单个连接键。 + +注意 + +当您在列上进行列连接时,传递的 DataFrame 对象的索引会被丢弃。如果需要保留索引值,可以使用`reset_index`将索引附加到列中。 + +合并操作中要考虑的最后一个问题是处理重叠列名的方式。例如: + +```py +In [58]: pd.merge(left, right, on="key1") +Out[58]: + key1 key2_x lval key2_y rval +0 foo one 1 one 4 +1 foo one 1 one 5 +2 foo two 2 one 4 +3 foo two 2 one 5 +4 bar one 3 one 6 +5 bar one 3 two 7 +``` + +虽然您可以手动处理重叠(请参阅 Ch 7.2.4:重命名轴索引部分以重命名轴标签),`pandas.merge`具有一个`suffixes`选项,用于指定要附加到左侧和右侧 DataFrame 对象中重叠名称的字符串: + +```py +In [59]: pd.merge(left, right, on="key1", suffixes=("_left", "_right")) +Out[59]: + key1 key2_left lval key2_right rval +0 foo one 1 one 4 +1 foo one 1 one 5 +2 foo two 2 one 4 +3 foo two 2 one 5 +4 bar one 3 one 6 +5 bar one 3 two 7 +``` + +请参阅 pandas.merge 中的表 8.2,了解有关参数的参考。下一节将介绍使用 DataFrame 的行索引进行连接。 + +表 8.2:`pandas.merge`函数参数 + +| 参数 | 描述 | +| --- | --- | +| `left` | 要在左侧合并的 DataFrame。 | +| `right` | 要在右侧合并的 DataFrame。 | +| `how` | 要应用的连接类型:`"inner"`、`"outer"`、`"left"`或`"right"`之一;默认为`"inner"`。 | +| `on` | 要连接的列名。必须在两个 DataFrame 对象中找到。如果未指定并且没有给出其他连接键,则将使用`left`和`right`中的列名的交集作为连接键。 | +| `left_on` | 用作连接键的`left` DataFrame 中的列。可以是单个列名或列名列表。 | +| `right_on` | 与`right` DataFrame 的`left_on`类似。 | +| `left_index` | 使用`left`中的行索引作为其连接键(或键,如果是`MultiIndex`)。 | +| `right_index` | 与`left_index`类似。 | +| `sort` | 按连接键按字典顺序对合并数据进行排序;默认为`False`。 | +| `suffixes` | 字符串元组值,用于在重叠的列名后追加(默认为`("_x", "_y")`,例如,如果两个 DataFrame 对象中都有`"data"`,则在结果中会显示为`"data_x"`和`"data_y"`。 | +| `copy` | 如果为`False`,则在某些特殊情况下避免将数据复制到结果数据结构中;默认情况下始终复制。 | +| `validate` | 验证合并是否是指定类型,一对一、一对多或多对多。有关选项的完整详细信息,请参阅文档字符串。 | + +| `indicator` | 添加一个特殊列`_merge`,指示每行的来源;值将根据每行中连接数据的来源为`"left_only"`、`"right_only"`或`"both"`。 + +### 在索引上合并 + +在某些情况下,DataFrame 中的合并键会在其索引(行标签)中找到。在这种情况下,您可以传递`left_index=True`或`right_index=True`(或两者都传递)来指示索引应该用作合并键: + +```py +In [60]: left1 = pd.DataFrame({"key": ["a", "b", "a", "a", "b", "c"], + ....: "value": pd.Series(range(6), dtype="Int64")}) + +In [61]: right1 = pd.DataFrame({"group_val": [3.5, 7]}, index=["a", "b"]) + +In [62]: left1 +Out[62]: + key value +0 a 0 +1 b 1 +2 a 2 +3 a 3 +4 b 4 +5 c 5 + +In [63]: right1 +Out[63]: + group_val +a 3.5 +b 7.0 + +In [64]: pd.merge(left1, right1, left_on="key", right_index=True) +Out[64]: + key value group_val +0 a 0 3.5 +2 a 2 3.5 +3 a 3 3.5 +1 b 1 7.0 +4 b 4 7.0 +``` + +注意 + +如果您仔细观察这里,您会发现`left1`的索引值已被保留,而在上面的其他示例中,输入 DataFrame 对象的索引已被丢弃。由于`right1`的索引是唯一的,这种“一对多”合并(使用默认的`how="inner"`方法)可以保留与输出中的行对应的`left1`的索引值。 + +由于默认合并方法是交集连接键,您可以使用外连接来形成它们的并集: + +```py +In [65]: pd.merge(left1, right1, left_on="key", right_index=True, how="outer") +Out[65]: + key value group_val +0 a 0 3.5 +2 a 2 3.5 +3 a 3 3.5 +1 b 1 7.0 +4 b 4 7.0 +5 c 5 NaN +``` + +对于具有分层索引的数据,情况会更加复杂,因为在索引上进行连接等效于多键合并: + +```py +In [66]: lefth = pd.DataFrame({"key1": ["Ohio", "Ohio", "Ohio", + ....: "Nevada", "Nevada"], + ....: "key2": [2000, 2001, 2002, 2001, 2002], + ....: "data": pd.Series(range(5), dtype="Int64")}) + +In [67]: righth_index = pd.MultiIndex.from_arrays( + ....: [ + ....: ["Nevada", "Nevada", "Ohio", "Ohio", "Ohio", "Ohio"], + ....: [2001, 2000, 2000, 2000, 2001, 2002] + ....: ] + ....: ) + +In [68]: righth = pd.DataFrame({"event1": pd.Series([0, 2, 4, 6, 8, 10], dtype="I +nt64", + ....: index=righth_index), + ....: "event2": pd.Series([1, 3, 5, 7, 9, 11], dtype="I +nt64", + ....: index=righth_index)}) + +In [69]: lefth +Out[69]: + key1 key2 data +0 Ohio 2000 0 +1 Ohio 2001 1 +2 Ohio 2002 2 +3 Nevada 2001 3 +4 Nevada 2002 4 + +In [70]: righth +Out[70]: + event1 event2 +Nevada 2001 0 1 + 2000 2 3 +Ohio 2000 4 5 + 2000 6 7 + 2001 8 9 + 2002 10 11 +``` + +在这种情况下,您必须指示要合并的多个列作为列表(注意使用`how="outer"`处理重复索引值): + +```py +In [71]: pd.merge(lefth, righth, left_on=["key1", "key2"], right_index=True) +Out[71]: + key1 key2 data event1 event2 +0 Ohio 2000 0 4 5 +0 Ohio 2000 0 6 7 +1 Ohio 2001 1 8 9 +2 Ohio 2002 2 10 11 +3 Nevada 2001 3 0 1 + +In [72]: pd.merge(lefth, righth, left_on=["key1", "key2"], + ....: right_index=True, how="outer") +Out[72]: + key1 key2 data event1 event2 +0 Ohio 2000 0 4 5 +0 Ohio 2000 0 6 7 +1 Ohio 2001 1 8 9 +2 Ohio 2002 2 10 11 +3 Nevada 2001 3 0 1 +4 Nevada 2002 4 +4 Nevada 2000 2 3 +``` + +使用合并的两侧的索引也是可能的: + +```py +In [73]: left2 = pd.DataFrame([[1., 2.], [3., 4.], [5., 6.]], + ....: index=["a", "c", "e"], + ....: columns=["Ohio", "Nevada"]).astype("Int64") + +In [74]: right2 = pd.DataFrame([[7., 8.], [9., 10.], [11., 12.], [13, 14]], + ....: index=["b", "c", "d", "e"], + ....: columns=["Missouri", "Alabama"]).astype("Int64") + +In [75]: left2 +Out[75]: + Ohio Nevada +a 1 2 +c 3 4 +e 5 6 + +In [76]: right2 +Out[76]: + Missouri Alabama +b 7 8 +c 9 10 +d 11 12 +e 13 14 + +In [77]: pd.merge(left2, right2, how="outer", left_index=True, right_index=True) +Out[77]: + Ohio Nevada Missouri Alabama +a 1 2 +b 7 8 +c 3 4 9 10 +d 11 12 +e 5 6 13 14 +``` + +DataFrame 有一个`join`实例方法,可以简化按索引合并。它还可以用于合并许多具有相同或类似索引但列不重叠的 DataFrame 对象。在前面的例子中,我们可以这样写: + +```py +In [78]: left2.join(right2, how="outer") +Out[78]: + Ohio Nevada Missouri Alabama +a 1 2 +b 7 8 +c 3 4 9 10 +d 11 12 +e 5 6 13 14 +``` + +与`pandas.merge`相比,DataFrame 的`join`方法默认在连接键上执行左连接。它还支持将传递的 DataFrame 的索引与调用 DataFrame 的某一列进行连接: + +```py +In [79]: left1.join(right1, on="key") +Out[79]: + key value group_val +0 a 0 3.5 +1 b 1 7.0 +2 a 2 3.5 +3 a 3 3.5 +4 b 4 7.0 +5 c 5 NaN +``` + +您可以将此方法视为将数据“合并”到调用其`join`方法的对象中。 + +最后,对于简单的索引对索引合并,您可以将 DataFrame 的列表传递给`join`,作为使用下一节中描述的更一般的`pandas.concat`函数的替代方法: + +```py +In [80]: another = pd.DataFrame([[7., 8.], [9., 10.], [11., 12.], [16., 17.]], + ....: index=["a", "c", "e", "f"], + ....: columns=["New York", "Oregon"]) + +In [81]: another +Out[81]: + New York Oregon +a 7.0 8.0 +c 9.0 10.0 +e 11.0 12.0 +f 16.0 17.0 + +In [82]: left2.join([right2, another]) +Out[82]: + Ohio Nevada Missouri Alabama New York Oregon +a 1 2 7.0 8.0 +c 3 4 9 10 9.0 10.0 +e 5 6 13 14 11.0 12.0 + +In [83]: left2.join([right2, another], how="outer") +Out[83]: + Ohio Nevada Missouri Alabama New York Oregon +a 1 2 7.0 8.0 +c 3 4 9 10 9.0 10.0 +e 5 6 13 14 11.0 12.0 +b 7 8 NaN NaN +d 11 12 NaN NaN +f 16.0 17.0 +``` + +### 沿轴连接 + +另一种数据组合操作被称为*连接*或*堆叠*。NumPy 的`concatenate`函数可以使用 NumPy 数组来执行此操作: + +```py +In [84]: arr = np.arange(12).reshape((3, 4)) + +In [85]: arr +Out[85]: +array([[ 0, 1, 2, 3], + [ 4, 5, 6, 7], + [ 8, 9, 10, 11]]) + +In [86]: np.concatenate([arr, arr], axis=1) +Out[86]: +array([[ 0, 1, 2, 3, 0, 1, 2, 3], + [ 4, 5, 6, 7, 4, 5, 6, 7], + [ 8, 9, 10, 11, 8, 9, 10, 11]]) +``` + +在 pandas 对象(如 Series 和 DataFrame)的上下文中,具有标记轴使您能够进一步推广数组连接。特别是,您有许多额外的考虑: + ++ 如果对象在其他轴上的索引不同,我们应该合并这些轴中的不同元素还是仅使用共同的值? + ++ 连接的数据块在结果对象中需要被识别吗? + ++ “连接轴”中包含需要保留的数据吗?在许多情况下,DataFrame 中的默认整数标签在连接时最好被丢弃。 + +pandas 中的`concat`函数提供了一种一致的方法来解决这些问题。我将给出一些示例来说明它是如何工作的。假设我们有三个没有索引重叠的 Series: + +```py +In [87]: s1 = pd.Series([0, 1], index=["a", "b"], dtype="Int64") + +In [88]: s2 = pd.Series([2, 3, 4], index=["c", "d", "e"], dtype="Int64") + +In [89]: s3 = pd.Series([5, 6], index=["f", "g"], dtype="Int64") +``` + +使用这些对象的列表调用`pandas.concat`会将值和索引粘合在一起: + +```py +In [90]: s1 +Out[90]: +a 0 +b 1 +dtype: Int64 + +In [91]: s2 +Out[91]: +c 2 +d 3 +e 4 +dtype: Int64 + +In [92]: s3 +Out[92]: +f 5 +g 6 +dtype: Int64 + +In [93]: pd.concat([s1, s2, s3]) +Out[93]: +a 0 +b 1 +c 2 +d 3 +e 4 +f 5 +g 6 +dtype: Int64 +``` + +默认情况下,`pandas.concat`沿着`axis="index"`工作,产生另一个 Series。如果传递`axis="columns"`,结果将是一个 DataFrame: + +```py +In [94]: pd.concat([s1, s2, s3], axis="columns") +Out[94]: + 0 1 2 +a 0 +b 1 +c 2 +d 3 +e 4 +f 5 +g 6 +``` + +在这种情况下,另一个轴上没有重叠,您可以看到这是索引的并集(`"outer"`连接)。您可以通过传递`join="inner"`来取交集: + +```py +In [95]: s4 = pd.concat([s1, s3]) + +In [96]: s4 +Out[96]: +a 0 +b 1 +f 5 +g 6 +dtype: Int64 + +In [97]: pd.concat([s1, s4], axis="columns") +Out[97]: + 0 1 +a 0 0 +b 1 1 +f 5 +g 6 + +In [98]: pd.concat([s1, s4], axis="columns", join="inner") +Out[98]: + 0 1 +a 0 0 +b 1 1 +``` + +在这个最后的例子中,`"f"`和`"g"`标签消失了,因为使用了`join="inner"`选项。 + +一个潜在的问题是结果中无法识别连接的片段。假设您希望在连接轴上创建一个分层索引。为此,请使用`keys`参数: + +```py +In [99]: result = pd.concat([s1, s1, s3], keys=["one", "two", "three"]) + +In [100]: result +Out[100]: +one a 0 + b 1 +two a 0 + b 1 +three f 5 + g 6 +dtype: Int64 + +In [101]: result.unstack() +Out[101]: + a b f g +one 0 1 +two 0 1 +three 5 6 +``` + +在沿`axis="columns"`组合 Series 的情况下,`keys`变成了 DataFrame 的列标题: + +```py +In [102]: pd.concat([s1, s2, s3], axis="columns", keys=["one", "two", "three"]) +Out[102]: + one two three +a 0 +b 1 +c 2 +d 3 +e 4 +f 5 +g 6 +``` + +相同的逻辑也适用于 DataFrame 对象: + +```py +In [103]: df1 = pd.DataFrame(np.arange(6).reshape(3, 2), index=["a", "b", "c"], + .....: columns=["one", "two"]) + +In [104]: df2 = pd.DataFrame(5 + np.arange(4).reshape(2, 2), index=["a", "c"], + .....: columns=["three", "four"]) + +In [105]: df1 +Out[105]: + one two +a 0 1 +b 2 3 +c 4 5 + +In [106]: df2 +Out[106]: + three four +a 5 6 +c 7 8 + +In [107]: pd.concat([df1, df2], axis="columns", keys=["level1", "level2"]) +Out[107]: + level1 level2 + one two three four +a 0 1 5.0 6.0 +b 2 3 NaN NaN +c 4 5 7.0 8.0 +``` + +在这里,`keys`参数用于创建一个分层索引,其中第一级可以用于标识每个连接的 DataFrame 对象。 + +如果您传递的是对象字典而不是列表,那么字典的键将用于`keys`选项: + +```py +In [108]: pd.concat({"level1": df1, "level2": df2}, axis="columns") +Out[108]: + level1 level2 + one two three four +a 0 1 5.0 6.0 +b 2 3 NaN NaN +c 4 5 7.0 8.0 +``` + +有一些额外的参数控制如何创建分层索引(参见表 8.3)。例如,我们可以使用`names`参数为创建的轴级别命名: + +```py +In [109]: pd.concat([df1, df2], axis="columns", keys=["level1", "level2"], + .....: names=["upper", "lower"]) +Out[109]: +upper level1 level2 +lower one two three four +a 0 1 5.0 6.0 +b 2 3 NaN NaN +c 4 5 7.0 8.0 +``` + +最后一个考虑因素涉及行索引不包含任何相关数据的 DataFrame: + +```py +In [110]: df1 = pd.DataFrame(np.random.standard_normal((3, 4)), + .....: columns=["a", "b", "c", "d"]) + +In [111]: df2 = pd.DataFrame(np.random.standard_normal((2, 3)), + .....: columns=["b", "d", "a"]) + +In [112]: df1 +Out[112]: + a b c d +0 1.248804 0.774191 -0.319657 -0.624964 +1 1.078814 0.544647 0.855588 1.343268 +2 -0.267175 1.793095 -0.652929 -1.886837 + +In [113]: df2 +Out[113]: + b d a +0 1.059626 0.644448 -0.007799 +1 -0.449204 2.448963 0.667226 +``` + +在这种情况下,您可以传递`ignore_index=True`,这将丢弃每个 DataFrame 的索引并仅连接列中的数据,分配一个新的默认索引: + +```py +In [114]: pd.concat([df1, df2], ignore_index=True) +Out[114]: + a b c d +0 1.248804 0.774191 -0.319657 -0.624964 +1 1.078814 0.544647 0.855588 1.343268 +2 -0.267175 1.793095 -0.652929 -1.886837 +3 -0.007799 1.059626 NaN 0.644448 +4 0.667226 -0.449204 NaN 2.448963 +``` + +表 8.3 描述了`pandas.concat`函数的参数。 + +表 8.3:`pandas.concat`函数参数 + +| 参数 | 描述 | +| --- | --- | +| `objs` | 要连接的 pandas 对象的列表或字典;这是唯一必需的参数 | +| `axis` | 要沿着连接的轴;默认为沿着行连接(`axis="index"`) | +| `join` | 要么是`"inner"`要么是`"outer"`(默认为`"outer"`);是否沿着其他轴相交(inner)或联合(outer)索引 | +| `keys` | 与要连接的对象关联的值,形成沿着连接轴的分层索引;可以是任意值的列表或数组,元组的数组,或数组的列表(如果在`levels`中传递了多级数组) | +| `levels` | 用作分层索引级别的特定索引,如果传递了键 | +| `names` | 如果传递了`keys`和/或`levels`,则为创建的分层级别命名 | +| `verify_integrity` | 检查连接对象中的新轴是否存在重复项,如果存在则引发异常;默认情况下(`False`)允许重复项 | +| `ignore_index` | 不保留沿着连接`axis`的索引,而是生成一个新的`range(total_length)`索引 | + +### 组合具有重叠部分的数据 + +还有另一种数据组合情况,既不能表示为合并操作也不能表示为连接操作。您可能有两个具有完全或部分重叠索引的数据集。作为一个激励性的例子,考虑 NumPy 的`where`函数,它执行数组导向的 if-else 表达式的等效操作: + +```py +In [115]: a = pd.Series([np.nan, 2.5, 0.0, 3.5, 4.5, np.nan], + .....: index=["f", "e", "d", "c", "b", "a"]) + +In [116]: b = pd.Series([0., np.nan, 2., np.nan, np.nan, 5.], + .....: index=["a", "b", "c", "d", "e", "f"]) + +In [117]: a +Out[117]: +f NaN +e 2.5 +d 0.0 +c 3.5 +b 4.5 +a NaN +dtype: float64 + +In [118]: b +Out[118]: +a 0.0 +b NaN +c 2.0 +d NaN +e NaN +f 5.0 +dtype: float64 + +In [119]: np.where(pd.isna(a), b, a) +Out[119]: array([0. , 2.5, 0. , 3.5, 4.5, 5. ]) +``` + +在这里,每当`a`中的值为空时,将选择`b`中的值,否则将选择`a`中的非空值。使用`numpy.where`不会检查索引标签是否对齐(甚至不需要对象具有相同的长度),因此如果要按索引对齐值,请使用 Series`combine_first`方法: + +```py +In [120]: a.combine_first(b) +Out[120]: +a 0.0 +b 4.5 +c 3.5 +d 0.0 +e 2.5 +f 5.0 +dtype: float64 +``` + +对于 DataFrame,`combine_first`按列执行相同的操作,因此您可以将其视为使用传递的对象中的数据“修补”调用对象中的缺失数据: + +```py +In [121]: df1 = pd.DataFrame({"a": [1., np.nan, 5., np.nan], + .....: "b": [np.nan, 2., np.nan, 6.], + .....: "c": range(2, 18, 4)}) + +In [122]: df2 = pd.DataFrame({"a": [5., 4., np.nan, 3., 7.], + .....: "b": [np.nan, 3., 4., 6., 8.]}) + +In [123]: df1 +Out[123]: + a b c +0 1.0 NaN 2 +1 NaN 2.0 6 +2 5.0 NaN 10 +3 NaN 6.0 14 + +In [124]: df2 +Out[124]: + a b +0 5.0 NaN +1 4.0 3.0 +2 NaN 4.0 +3 3.0 6.0 +4 7.0 8.0 + +In [125]: df1.combine_first(df2) +Out[125]: + a b c +0 1.0 NaN 2.0 +1 4.0 2.0 6.0 +2 5.0 4.0 10.0 +3 3.0 6.0 14.0 +4 7.0 8.0 NaN +``` + +使用 DataFrame 对象的`combine_first`的输出将具有所有列名称的并集。 + +## 8.3 重塑和旋转 + +有许多用于重新排列表格数据的基本操作。这些操作被称为*重塑*或*旋转*操作。 + +### 使用分层索引进行重塑 + +分层索引提供了在 DataFrame 中重新排列数据的一致方法。有两个主要操作: + +`stack` + +这将从数据中的列旋转或旋转到行。 + +`unstack` + +这将从行旋转到列。 + +我将通过一系列示例来说明这些操作。考虑一个具有字符串数组作为行和列索引的小 DataFrame: + +```py +In [126]: data = pd.DataFrame(np.arange(6).reshape((2, 3)), + .....: index=pd.Index(["Ohio", "Colorado"], name="state"), + .....: columns=pd.Index(["one", "two", "three"], + .....: name="number")) + +In [127]: data +Out[127]: +number one two three +state +Ohio 0 1 2 +Colorado 3 4 5 +``` + +在这些数据上使用`stack`方法将列旋转为行,生成一个 Series: + +```py +In [128]: result = data.stack() + +In [129]: result +Out[129]: +state number +Ohio one 0 + two 1 + three 2 +Colorado one 3 + two 4 + three 5 +dtype: int64 +``` + +从具有分层索引的 Series 中,您可以使用`unstack`将数据重新排列回 DataFrame: + +```py +In [130]: result.unstack() +Out[130]: +number one two three +state +Ohio 0 1 2 +Colorado 3 4 5 +``` + +默认情况下,最内层级别被取消堆叠(与`stack`相同)。您可以通过传递级别编号或名称来取消堆叠不同的级别: + +```py +In [131]: result.unstack(level=0) +Out[131]: +state Ohio Colorado +number +one 0 3 +two 1 4 +three 2 5 + +In [132]: result.unstack(level="state") +Out[132]: +state Ohio Colorado +number +one 0 3 +two 1 4 +three 2 5 +``` + +如果在每个子组中未找到级别中的所有值,则取消堆叠可能会引入缺失数据: + +```py +In [133]: s1 = pd.Series([0, 1, 2, 3], index=["a", "b", "c", "d"], dtype="Int64") + +In [134]: s2 = pd.Series([4, 5, 6], index=["c", "d", "e"], dtype="Int64") + +In [135]: data2 = pd.concat([s1, s2], keys=["one", "two"]) + +In [136]: data2 +Out[136]: +one a 0 + b 1 + c 2 + d 3 +two c 4 + d 5 + e 6 +dtype: Int64 +``` + +堆叠默认会过滤掉缺失数据,因此该操作更容易反转: + +```py +In [137]: data2.unstack() +Out[137]: + a b c d e +one 0 1 2 3 +two 4 5 6 + +In [138]: data2.unstack().stack() +Out[138]: +one a 0 + b 1 + c 2 + d 3 +two c 4 + d 5 + e 6 +dtype: Int64 + +In [139]: data2.unstack().stack(dropna=False) +Out[139]: +one a 0 + b 1 + c 2 + d 3 + e +two a + b + c 4 + d 5 + e 6 +dtype: Int64 +``` + +当您在 DataFrame 中取消堆叠时,取消堆叠的级别将成为结果中的最低级别: + +```py +In [140]: df = pd.DataFrame({"left": result, "right": result + 5}, + .....: columns=pd.Index(["left", "right"], name="side")) + +In [141]: df +Out[141]: +side left right +state number +Ohio one 0 5 + two 1 6 + three 2 7 +Colorado one 3 8 + two 4 9 + three 5 10 + +In [142]: df.unstack(level="state") +Out[142]: +side left right +state Ohio Colorado Ohio Colorado +number +one 0 3 5 8 +two 1 4 6 9 +three 2 5 7 10 +``` + +与`unstack`一样,调用`stack`时,我们可以指定要堆叠的轴的名称: + +```py +In [143]: df.unstack(level="state").stack(level="side") +Out[143]: +state Colorado Ohio +number side +one left 3 0 + right 8 5 +two left 4 1 + right 9 6 +three left 5 2 + right 10 7 +``` + +### 将“长”格式旋转为“宽”格式 + +在数据库和 CSV 文件中存储多个时间序列的常见方法有时被称为*长*或*堆叠*格式。在此格式中,单个值由表中的一行表示,而不是每行多个值。 + +让我们加载一些示例数据,并进行少量时间序列整理和其他数据清理: + +```py +In [144]: data = pd.read_csv("examples/macrodata.csv") + +In [145]: data = data.loc[:, ["year", "quarter", "realgdp", "infl", "unemp"]] + +In [146]: data.head() +Out[146]: + year quarter realgdp infl unemp +0 1959 1 2710.349 0.00 5.8 +1 1959 2 2778.801 2.34 5.1 +2 1959 3 2775.488 2.74 5.3 +3 1959 4 2785.204 0.27 5.6 +4 1960 1 2847.699 2.31 5.2 +``` + +首先,我使用`pandas.PeriodIndex`(表示时间间隔而不是时间点),在 Ch 11: Time Series 中更详细地讨论,将`year`和`quarter`列组合起来,将索引设置为每个季度末的`datetime`值: + +```py +In [147]: periods = pd.PeriodIndex(year=data.pop("year"), + .....: quarter=data.pop("quarter"), + .....: name="date") + +In [148]: periods +Out[148]: +PeriodIndex(['1959Q1', '1959Q2', '1959Q3', '1959Q4', '1960Q1', '1960Q2', + '1960Q3', '1960Q4', '1961Q1', '1961Q2', + ... + '2007Q2', '2007Q3', '2007Q4', '2008Q1', '2008Q2', '2008Q3', + '2008Q4', '2009Q1', '2009Q2', '2009Q3'], + dtype='period[Q-DEC]', name='date', length=203) + +In [149]: data.index = periods.to_timestamp("D") + +In [150]: data.head() +Out[150]: + realgdp infl unemp +date +1959-01-01 2710.349 0.00 5.8 +1959-04-01 2778.801 2.34 5.1 +1959-07-01 2775.488 2.74 5.3 +1959-10-01 2785.204 0.27 5.6 +1960-01-01 2847.699 2.31 5.2 +``` + +在这里,我在 DataFrame 上使用了`pop`方法,该方法返回一个列,同时从 DataFrame 中删除它。 + +然后,我选择一部分列,并给`columns`索引命名为`"item"`: + +```py +In [151]: data = data.reindex(columns=["realgdp", "infl", "unemp"]) + +In [152]: data.columns.name = "item" + +In [153]: data.head() +Out[153]: +item realgdp infl unemp +date +1959-01-01 2710.349 0.00 5.8 +1959-04-01 2778.801 2.34 5.1 +1959-07-01 2775.488 2.74 5.3 +1959-10-01 2785.204 0.27 5.6 +1960-01-01 2847.699 2.31 5.2 +``` + +最后,我使用`stack`重新塑造,使用`reset_index`将新的索引级别转换为列,最后给包含数据值的列命名为`"value"`: + +```py +In [154]: long_data = (data.stack() + .....: .reset_index() + .....: .rename(columns={0: "value"})) +``` + +现在,`ldata`看起来像这样: + +```py +In [155]: long_data[:10] +Out[155]: + date item value +0 1959-01-01 realgdp 2710.349 +1 1959-01-01 infl 0.000 +2 1959-01-01 unemp 5.800 +3 1959-04-01 realgdp 2778.801 +4 1959-04-01 infl 2.340 +5 1959-04-01 unemp 5.100 +6 1959-07-01 realgdp 2775.488 +7 1959-07-01 infl 2.740 +8 1959-07-01 unemp 5.300 +9 1959-10-01 realgdp 2785.204 +``` + +在这种所谓的*长*格式中,每个时间序列的每一行在表中代表一个单独的观察。 + +数据经常以这种方式存储在关系型 SQL 数据库中,因为固定的模式(列名和数据类型)允许`item`列中的不同值的数量随着数据添加到表中而改变。在前面的例子中,`date`和`item`通常会成为主键(在关系数据库术语中),提供关系完整性和更容易的连接。在某些情况下,以这种格式处理数据可能更加困难;您可能更喜欢拥有一个 DataFrame,其中包含一个以`date`列中的时间戳为索引的每个不同`item`值的列。DataFrame 的`pivot`方法正好执行这种转换: + +```py +In [156]: pivoted = long_data.pivot(index="date", columns="item", + .....: values="value") + +In [157]: pivoted.head() +Out[157]: +item infl realgdp unemp +date +1959-01-01 0.00 2710.349 5.8 +1959-04-01 2.34 2778.801 5.1 +1959-07-01 2.74 2775.488 5.3 +1959-10-01 0.27 2785.204 5.6 +1960-01-01 2.31 2847.699 5.2 +``` + +传递的前两个值分别是要使用的列,作为行和列索引,最后是一个可选的值列,用于填充 DataFrame。假设您有两个值列,希望同时重塑: + +```py +In [159]: long_data["value2"] = np.random.standard_normal(len(long_data)) + +In [160]: long_data[:10] +Out[160]: + date item value value2 +0 1959-01-01 realgdp 2710.349 0.802926 +1 1959-01-01 infl 0.000 0.575721 +2 1959-01-01 unemp 5.800 1.381918 +3 1959-04-01 realgdp 2778.801 0.000992 +4 1959-04-01 infl 2.340 -0.143492 +5 1959-04-01 unemp 5.100 -0.206282 +6 1959-07-01 realgdp 2775.488 -0.222392 +7 1959-07-01 infl 2.740 -1.682403 +8 1959-07-01 unemp 5.300 1.811659 +9 1959-10-01 realgdp 2785.204 -0.351305 +``` + +通过省略最后一个参数,您可以获得一个具有分层列的 DataFrame: + +```py +In [161]: pivoted = long_data.pivot(index="date", columns="item") + +In [162]: pivoted.head() +Out[162]: + value value2 +item infl realgdp unemp infl realgdp unemp +date +1959-01-01 0.00 2710.349 5.8 0.575721 0.802926 1.381918 +1959-04-01 2.34 2778.801 5.1 -0.143492 0.000992 -0.206282 +1959-07-01 2.74 2775.488 5.3 -1.682403 -0.222392 1.811659 +1959-10-01 0.27 2785.204 5.6 0.128317 -0.351305 -1.313554 +1960-01-01 2.31 2847.699 5.2 -0.615939 0.498327 0.174072 + +In [163]: pivoted["value"].head() +Out[163]: +item infl realgdp unemp +date +1959-01-01 0.00 2710.349 5.8 +1959-04-01 2.34 2778.801 5.1 +1959-07-01 2.74 2775.488 5.3 +1959-10-01 0.27 2785.204 5.6 +1960-01-01 2.31 2847.699 5.2 +``` + +请注意,`pivot`等同于使用`set_index`创建一个分层索引,然后调用`unstack`: + +```py +In [164]: unstacked = long_data.set_index(["date", "item"]).unstack(level="item") + +In [165]: unstacked.head() +Out[165]: + value value2 +item infl realgdp unemp infl realgdp unemp +date +1959-01-01 0.00 2710.349 5.8 0.575721 0.802926 1.381918 +1959-04-01 2.34 2778.801 5.1 -0.143492 0.000992 -0.206282 +1959-07-01 2.74 2775.488 5.3 -1.682403 -0.222392 1.811659 +1959-10-01 0.27 2785.204 5.6 0.128317 -0.351305 -1.313554 +1960-01-01 2.31 2847.699 5.2 -0.615939 0.498327 0.174072 +``` + +### 从“宽”格式到“长”格式的旋转 + +DataFrame 的`pivot`的逆操作是`pandas.melt`。与在新的 DataFrame 中将一个列转换为多个不同,它将多个列合并为一个,生成一个比输入更长的 DataFrame。让我们看一个例子: + +```py +In [167]: df = pd.DataFrame({"key": ["foo", "bar", "baz"], + .....: "A": [1, 2, 3], + .....: "B": [4, 5, 6], + .....: "C": [7, 8, 9]}) + +In [168]: df +Out[168]: + key A B C +0 foo 1 4 7 +1 bar 2 5 8 +2 baz 3 6 9 +``` + +`"key"`列可以是一个组指示器,其他列是数据值。在使用`pandas.melt`时,我们必须指示哪些列(如果有的话)是组指示器。让我们在这里只使用`"key"`作为唯一的组指示器: + +```py +In [169]: melted = pd.melt(df, id_vars="key") + +In [170]: melted +Out[170]: + key variable value +0 foo A 1 +1 bar A 2 +2 baz A 3 +3 foo B 4 +4 bar B 5 +5 baz B 6 +6 foo C 7 +7 bar C 8 +8 baz C 9 +``` + +使用`pivot`,我们可以重新塑造回原始布局: + +```py +In [171]: reshaped = melted.pivot(index="key", columns="variable", + .....: values="value") + +In [172]: reshaped +Out[172]: +variable A B C +key +bar 2 5 8 +baz 3 6 9 +foo 1 4 7 +``` + +由于`pivot`的结果从用作行标签的列创建索引,我们可能希望使用`reset_index`将数据移回到列中: + +```py +In [173]: reshaped.reset_index() +Out[173]: +variable key A B C +0 bar 2 5 8 +1 baz 3 6 9 +2 foo 1 4 7 +``` + +您还可以指定要用作“值”列的列的子集: + +```py +In [174]: pd.melt(df, id_vars="key", value_vars=["A", "B"]) +Out[174]: + key variable value +0 foo A 1 +1 bar A 2 +2 baz A 3 +3 foo B 4 +4 bar B 5 +5 baz B 6 +``` + +`pandas.melt`也可以在没有任何组标识符的情况下使用: + +```py +In [175]: pd.melt(df, value_vars=["A", "B", "C"]) +Out[175]: + variable value +0 A 1 +1 A 2 +2 A 3 +3 B 4 +4 B 5 +5 B 6 +6 C 7 +7 C 8 +8 C 9 + +In [176]: pd.melt(df, value_vars=["key", "A", "B"]) +Out[176]: + variable value +0 key foo +1 key bar +2 key baz +3 A 1 +4 A 2 +5 A 3 +6 B 4 +7 B 5 +8 B 6 +``` + +## 8.4 结论 + +现在您已经掌握了一些关于 pandas 的基础知识,用于数据导入、清理和重新组织,我们准备继续使用 matplotlib 进行数据可视化。当我们讨论更高级的分析时,我们将回到书中的其他领域来探索 pandas 的更多功能。 diff --git a/translations/cn/pyda3e_12.md b/translations/cn/pyda3e_12.md new file mode 100644 index 000000000..f852be185 --- /dev/null +++ b/translations/cn/pyda3e_12.md @@ -0,0 +1,823 @@ +# 九、绘图和可视化 + +> 原文:[`wesmckinney.com/book/plotting-and-visualization`](https://wesmckinney.com/book/plotting-and-visualization) +> +> 译者:[飞龙](https://github.com/wizardforcel) +> +> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) + + +> 此开放访问网络版本的《Python 数据分析第三版》现已作为[印刷版和数字版](https://amzn.to/3DyLaJc)的伴侣提供。如果您发现任何勘误,请[在此处报告](https://oreilly.com/catalog/0636920519829/errata)。请注意,由 Quarto 生成的本站点的某些方面与 O'Reilly 的印刷版和电子书版本的格式不同。 +> +> 如果您发现本书的在线版本有用,请考虑[订购纸质版](https://amzn.to/3DyLaJc)或[无 DRM 的电子书](https://www.ebooks.com/en-us/book/210644288/python-for-data-analysis/wes-mckinney/?affId=WES398681F)以支持作者。本网站的内容不得复制或再生产。代码示例采用 MIT 许可,可在 GitHub 或 Gitee 上找到。 + +制作信息丰富的可视化(有时称为*图)是数据分析中最重要的任务之一。它可能是探索过程的一部分,例如,帮助识别异常值或所需的数据转换,或者作为生成模型想法的一种方式。对于其他人,构建用于网络的交互式可视化可能是最终目标。Python 有许多附加库用于制作静态或动态可视化,但我主要关注[matplotlib](https://matplotlib.org)和构建在其之上的库。 + +matplotlib 是一个桌面绘图包,旨在创建适合出版的图形和图表。该项目由 John Hunter 于 2002 年发起,旨在在 Python 中实现类似 MATLAB 的绘图界面。matplotlib 和 IPython 社区合作简化了从 IPython shell(现在是 Jupyter 笔记本)进行交互式绘图。matplotlib 支持所有操作系统上的各种 GUI 后端,并且可以将可视化导出为所有常见的矢量和光栅图形格式(PDF、SVG、JPG、PNG、BMP、GIF 等)。除了一些图表外,本书中几乎所有的图形都是使用 matplotlib 生成的。 + +随着时间的推移,matplotlib 衍生出了许多用于数据可视化的附加工具包,这些工具包使用 matplotlib 进行底层绘图。其中之一是[seaborn](http://seaborn.pydata.org),我们将在本章后面探讨。 + +在本章中跟随代码示例的最简单方法是在 Jupyter 笔记本中输出图形。要设置这个,可以在 Jupyter 笔记本中执行以下语句: + +```py +%matplotlib inline +``` + +注意 + +自 2012 年第一版以来,已经创建了许多新的数据可视化库,其中一些(如 Bokeh 和 Altair)利用现代网络技术创建交互式可视化,与 Jupyter 笔记本很好地集成。与在本书中使用多个可视化工具不同,我决定坚持使用 matplotlib 来教授基础知识,特别是因为 pandas 与 matplotlib 有很好的集成。您可以根据本章的原则学习如何使用其他可视化库。 + +## 9.1 简要的 matplotlib API 入门 + +使用 matplotlib 时,我们使用以下导入约定: + +```py +In [13]: import matplotlib.pyplot as plt +``` + +在 Jupyter 中运行`%matplotlib notebook`(或在 IPython 中运行`%matplotlib`),我们可以尝试创建一个简单的图。如果一切设置正确,应该会出现一个类似 Simple line plot 的线图: + +```py +In [14]: data = np.arange(10) + +In [15]: data +Out[15]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + +In [16]: plt.plot(data) +``` + +![](img/33f467ae0ee1820d490756dfcb2a77fb.png) + +图 9.1:简单线图 + +虽然像 seaborn 和 pandas 内置绘图函数将处理许多制作图形的琐碎细节,但如果您希望自定义超出提供的函数选项之外的内容,您需要了解一些关于 matplotlib API 的知识。 + +注意 + +本书中没有足够的空间来全面介绍 matplotlib 的功能广度和深度。它应该足以教会您如何上手。matplotlib 图库和文档是学习高级功能的最佳资源。 + +### 图和子图 + +matplotlib 中的绘图位于 `Figure` 对象中。您可以使用 `plt.figure` 创建一个新的图: + +```py +In [17]: fig = plt.figure() +``` + +在 IPython 中,如果您首先运行 `%matplotlib` 来设置 matplotlib 集成,将会出现一个空白绘图窗口,但在 Jupyter 中,直到我们使用更多命令之前,什么都不会显示。 + +`plt.figure` 有许多选项;特别是,如果保存到磁盘,`figsize` 将保证图的特定大小和纵横比。 + +您不能在空白图中制作绘图。您必须使用 `add_subplot` 创建一个或多个 `subplots`: + +```py +In [18]: ax1 = fig.add_subplot(2, 2, 1) +``` + +这意味着图应该是 2 × 2(因此总共最多四个绘图),我们选择了四个子图中的第一个(从 1 编号)。如果您创建下两个子图,您将得到一个看起来像 一个空的 matplotlib 图,带有三个子图 的可视化: + +```py +In [19]: ax2 = fig.add_subplot(2, 2, 2) + +In [20]: ax3 = fig.add_subplot(2, 2, 3) +``` + +![](img/3ffe09420f821e4d9cb950f2e5bb6e84.png) + +图 9.2:一个空的 matplotlib 图,带有三个子图 + +提示: + +使用 Jupyter 笔记本的一个细微之处是,每次评估单元格后绘图都会重置,因此您必须将所有绘图命令放在一个单独的笔记本单元格中。 + +在这里,我们在同一个单元格中运行所有这些命令: + +```py +fig = plt.figure() +ax1 = fig.add_subplot(2, 2, 1) +ax2 = fig.add_subplot(2, 2, 2) +ax3 = fig.add_subplot(2, 2, 3) +``` + +这些绘图轴对象有各种方法,可以创建不同类型的绘图,最好使用轴方法而不是像 `plt.plot` 这样的顶级绘图函数。例如,我们可以使用 `plot` 方法制作一条线图(参见单个绘图后的数据可视化): + +```py +In [21]: ax3.plot(np.random.standard_normal(50).cumsum(), color="black", + ....: linestyle="dashed") +``` + +![](img/e930d33b55aed88698b1dbfd5bd03cbf.png) + +图 9.3:单个绘图后的数据可视化 + +当您运行此命令时,您可能会注意到类似 `` 的输出。matplotlib 返回引用刚刚添加的绘图子组件的对象。大多数情况下,您可以安全地忽略此输出,或者您可以在行末加上分号以抑制输出。 + +附加选项指示 matplotlib 绘制一条黑色虚线。这里由 `fig.add_subplot` 返回的对象是 `AxesSubplot` 对象,您可以通过调用每个实例方法直接在其他空子图上绘制(参见添加额外绘图后的数据可视化): + +```py +In [22]: ax1.hist(np.random.standard_normal(100), bins=20, color="black", alpha=0 +.3); +In [23]: ax2.scatter(np.arange(30), np.arange(30) + 3 * np.random.standard_normal +(30)); +``` + +![](img/c61e1e89fed6ca0718719066a3fad5d6.png) + +图 9.4:添加额外绘图后的数据可视化 + +`alpha=0.3` 样式选项设置了叠加绘图的透明度。 + +您可以在 [matplotlib 文档](https://matplotlib.org) 中找到绘图类型的全面目录。 + +为了更方便地创建子图网格,matplotlib 包括一个 `plt.subplots` 方法,它创建一个新图并返回一个包含创建的子图对象的 NumPy 数组: + +```py +In [25]: fig, axes = plt.subplots(2, 3) + +In [26]: axes +Out[26]: +array([[, , ], + [, , ]], dtype=object) +``` + +然后,`axes` 数组可以像二维数组一样索引;例如,`axes[0, 1]` 指的是顶部行中心的子图。您还可以使用 `sharex` 和 `sharey` 指示子图应具有相同的 x 或 y 轴。当您在相同比例上比较数据时,这可能很有用;否则,matplotlib 会独立自动缩放绘图限制。有关此方法的更多信息,请参见 表 9.1。 + +表 9.1:`matplotlib.pyplot.subplots` 选项 + +| 参数 | 描述 | +| --- | --- | +| `nrows` | 子图的行数 | +| `ncols` | 子图的列数 | +| `sharex` | 所有子图应使用相同的 x 轴刻度(调整 `xlim` 将影响所有子图) | +| `sharey` | 所有子图应使用相同的 y 轴刻度(调整 `ylim` 将影响所有子图) | +| `subplot_kw` | 传递给 `add_subplot` 调用的关键字字典,用于创建每个子图 | +| `**fig_kw` | 创建图时使用`subplots`的附加关键字,例如`plt.subplots(2, 2, figsize=(8, 6))` | + +#### 调整子图周围的间距 + +默认情况下,matplotlib 在子图周围留有一定量的填充和子图之间的间距。这些间距都是相对于绘图的高度和宽度指定的,因此如果您通过编程或使用 GUI 窗口手动调整绘图大小,绘图将动态调整自身。您可以使用`Figure`对象上的`subplots_adjust`方法更改间距: + +```py +subplots_adjust(left=None, bottom=None, right=None, top=None, + wspace=None, hspace=None) +``` + +`wspace`和`hspace`控制子图之间使用的百分比图宽度和图高度的间距。这里是一个您可以在 Jupyter 中执行的小例子,我将间距缩小到零(参见没有子图间距的数据可视化): + +```py +fig, axes = plt.subplots(2, 2, sharex=True, sharey=True) +for i in range(2): + for j in range(2): + axes[i, j].hist(np.random.standard_normal(500), bins=50, + color="black", alpha=0.5) +fig.subplots_adjust(wspace=0, hspace=0) +``` + +![](img/c29739a3af9d3f69ced8617625eb559c.png) + +图 9.5:没有子图间距的数据可视化 + +您可能会注意到轴标签重叠。matplotlib 不会检查标签是否重叠,因此在这种情况下,您需要通过指定显式刻度位置和刻度标签自行修复标签(我们将在后面的部分刻度、标签和图例中看到如何做到这一点)。 + +### 颜色、标记和线型 + +matplotlib 的线`plot`函数接受 x 和 y 坐标数组以及可选的颜色样式选项。例如,要用绿色虚线绘制`x`与`y`,您可以执行: + +```py +ax.plot(x, y, linestyle="--", color="green") +``` + +提供了许多常用颜色的颜色名称,但您可以通过指定其十六进制代码(例如,`"#CECECE"`)来使用光谱上的任何颜色。您可以查看`plt.plot`的文档字符串以查看一些支持的线型。在线文档中提供了更全面的参考资料。 + +线图还可以具有*标记*来突出实际数据点。由于 matplotlib 的`plot`函数创建连续线图,插值点之间的插值,有时可能不清楚点位于何处。标记可以作为附加样式选项提供(参见带有标记的线图): + +```py +In [31]: ax = fig.add_subplot() + +In [32]: ax.plot(np.random.standard_normal(30).cumsum(), color="black", + ....: linestyle="dashed", marker="o"); +``` + +![](img/057b871e61b23676a12ca85ac841d012.png) + +图 9.6:带有标记的线图 + +对于线图,您会注意到默认情况下后续点是线性插值的。这可以通过`drawstyle`选项进行更改(参见带有不同 drawstyle 选项的线图): + +```py +In [34]: fig = plt.figure() + +In [35]: ax = fig.add_subplot() + +In [36]: data = np.random.standard_normal(30).cumsum() + +In [37]: ax.plot(data, color="black", linestyle="dashed", label="Default"); +In [38]: ax.plot(data, color="black", linestyle="dashed", + ....: drawstyle="steps-post", label="steps-post"); +In [39]: ax.legend() +``` + +![](img/3e63e6a6357b24a6c73084b155cbc27b.png) + +图 9.7:带有不同 drawstyle 选项的线图 + +在这里,由于我们将`label`参数传递给`plot`,我们能够使用`ax.legend`创建一个图例,以标识每条线。我在刻度、标签和图例中更多地讨论图例。 + +注意 + +无论您在绘制数据时是否传递了`label`选项,都必须调用`ax.legend`来创建图例。 + +### 刻度、标签和图例 + +大多数类型的绘图装饰都可以通过 matplotlib 轴对象上的方法访问。这包括`xlim`、`xticks`和`xticklabels`等方法。它们分别控制绘图范围、刻度位置和刻度标签。它们可以以两种方式使用: + ++ 不带参数调用返回当前参数值(例如,`ax.xlim()`返回当前 x 轴绘图范围) + ++ 带参数调用设置参数值(例如,`ax.xlim([0, 10])`将 x 轴范围设置为 0 到 10) + +所有这些方法都作用于活动或最近创建的`AxesSubplot`。每个对应于 subplot 对象本身的两种方法;在`xlim`的情况下,这些方法是`ax.get_xlim`和`ax.set_xlim`。 + +#### 设置标题、轴标签、刻度和刻度标签 + +为了说明如何自定义坐标轴,我将创建一个简单的图和一个随机漫步的绘图(参见用于说明 xticks 的简单绘图(带有默认标签)): + +```py +In [40]: fig, ax = plt.subplots() + +In [41]: ax.plot(np.random.standard_normal(1000).cumsum()); +``` + +![](img/5a99b09f27faef68b4b1150598d87c05.png) + +图 9.8:用于说明 xticks 的简单图表(带有默认标签) + +要更改 x 轴刻度,最简单的方法是使用`set_xticks`和`set_xticklabels`。前者指示 matplotlib 在数据范围内放置刻度的位置;默认情况下,这些位置也将是标签。但是我们可以使用`set_xticklabels`设置任何其他值作为标签: + +```py +In [42]: ticks = ax.set_xticks([0, 250, 500, 750, 1000]) + +In [43]: labels = ax.set_xticklabels(["one", "two", "three", "four", "five"], + ....: rotation=30, fontsize=8) +``` + +`rotation`选项将 x 轴刻度标签设置为 30 度旋转。最后,`set_xlabel`为 x 轴命名,`set_title`为子图标题(请参见用于说明自定义 xticks 的简单图表以查看生成的图): + +```py +In [44]: ax.set_xlabel("Stages") +Out[44]: Text(0.5, 6.666666666666652, 'Stages') + +In [45]: ax.set_title("My first matplotlib plot") +``` + +![](img/8592bb9fc63f4f28333b53d403105a57.png) + +图 9.9:用于说明自定义 xticks 的简单图表 + +修改 y 轴的过程与此示例中的`x`替换为`y`相同。axes 类有一个`set`方法,允许批量设置绘图属性。从前面的示例中,我们也可以这样写: + +```py +ax.set(title="My first matplotlib plot", xlabel="Stages") +``` + +#### 添加图例 + +图例是识别图表元素的另一个关键元素。有几种方法可以添加图例。最简单的方法是在添加每个图表元素时传递`label`参数: + +```py +In [46]: fig, ax = plt.subplots() + +In [47]: ax.plot(np.random.randn(1000).cumsum(), color="black", label="one"); +In [48]: ax.plot(np.random.randn(1000).cumsum(), color="black", linestyle="dashed +", + ....: label="two"); +In [49]: ax.plot(np.random.randn(1000).cumsum(), color="black", linestyle="dotted +", + ....: label="three"); +``` + +一旦您完成了这一步,您可以调用`ax.legend()`来自动创建图例。生成的图表在带有三条线和图例的简单图表中: + +```py +In [50]: ax.legend() +``` + +![](img/b305e27a26b0ff06c2d3130576a72f42.png) + +图 9.10:带有三条线和图例的简单图表 + +`legend`方法有几个其他选项可用于位置`loc`参数。有关更多信息,请参阅文档字符串(使用`ax.legend?`)。 + +`loc`图例选项告诉 matplotlib 在哪里放置图例。默认值是`"best"`,它会尝试选择一个最不起眼的位置。要从图例中排除一个或多个元素,请不传递标签或传递`label="_nolegend_"`。 + +### 注释和在子图上绘制 + +除了标准的绘图类型,您可能希望绘制自己的绘图注释,这可能包括文本、箭头或其他形状。您可以使用`text`、`arrow`和`annotate`函数添加注释和文本。`text`在给定坐标`(x, y)`处绘制文本,可选的自定义样式: + +```py +ax.text(x, y, "Hello world!", + family="monospace", fontsize=10) +``` + +注释可以绘制文本和箭头,并适当排列。例如,让我们绘制自 2007 年以来的标准普尔 500 指数收盘价(从 Yahoo! Finance 获取),并用 2008-2009 年金融危机的一些重要日期进行注释。您可以在 Jupyter 笔记本中的单个单元格中运行此代码示例。查看 2008-2009 年金融危机中的重要日期以查看结果: + +```py +from datetime import datetime + +fig, ax = plt.subplots() + +data = pd.read_csv("examples/spx.csv", index_col=0, parse_dates=True) +spx = data["SPX"] + +spx.plot(ax=ax, color="black") + +crisis_data = [ + (datetime(2007, 10, 11), "Peak of bull market"), + (datetime(2008, 3, 12), "Bear Stearns Fails"), + (datetime(2008, 9, 15), "Lehman Bankruptcy") +] + +for date, label in crisis_data: + ax.annotate(label, xy=(date, spx.asof(date) + 75), + xytext=(date, spx.asof(date) + 225), + arrowprops=dict(facecolor="black", headwidth=4, width=2, + headlength=4), + horizontalalignment="left", verticalalignment="top") + +# Zoom in on 2007-2010 +ax.set_xlim(["1/1/2007", "1/1/2011"]) +ax.set_ylim([600, 1800]) + +ax.set_title("Important dates in the 2008–2009 financial crisis") +``` + +![](img/b8b372e58ab7916dbb3ef4c71569c5be.png) + +图 9.11:2008-2009 年金融危机中的重要日期 + +在这个图表中有几个重要的要点需要强调。`ax.annotate`方法可以在指定的 x 和 y 坐标处绘制标签。我们使用`set_xlim`和`set_ylim`方法手动设置绘图的起始和结束边界,而不是使用 matplotlib 的默认值。最后,`ax.set_title`为绘图添加了一个主标题。 + +请查看在线 matplotlib 画廊,了解更多注释示例以供学习。 + +绘制形状需要更多的注意。matplotlib 有代表许多常见形状的对象,称为*patches*。其中一些,如`Rectangle`和`Circle`,可以在`matplotlib.pyplot`中找到,但完整的集合位于`matplotlib.patches`中。 + +要向图表添加形状,您需要创建补丁对象,并通过将补丁传递给`ax.add_patch`将其添加到子图`ax`中(请参见由三个不同补丁组成的数据可视化): + +```py +fig, ax = plt.subplots() + +rect = plt.Rectangle((0.2, 0.75), 0.4, 0.15, color="black", alpha=0.3) +circ = plt.Circle((0.7, 0.2), 0.15, color="blue", alpha=0.3) +pgon = plt.Polygon([[0.15, 0.15], [0.35, 0.4], [0.2, 0.6]], + color="green", alpha=0.5) + +ax.add_patch(rect) +ax.add_patch(circ) +ax.add_patch(pgon) +``` + +![](img/bbe77ca56f949e70e20ccf23975bca48.png) + +图 9.12:由三个不同补丁组成的数据可视化 + +如果您查看许多熟悉的绘图类型的实现,您会发现它们是由补丁组装而成的。 + +### 保存图表到文件 + +您可以使用图形对象的`savefig`实例方法将活动图形保存到文件。例如,要保存图形的 SVG 版本,您只需输入: + +```py +fig.savefig("figpath.svg") +``` + +文件类型是从文件扩展名中推断的。因此,如果您使用`.pdf`,您将得到一个 PDF。我经常用于发布图形的一个重要选项是`dpi`,它控制每英寸的分辨率。要获得相同的图形作为 400 DPI 的 PNG,您可以执行: + +```py +fig.savefig("figpath.png", dpi=400) +``` + +有关`savefig`的一些其他选项,请参见表 9.2。要获取全面的列表,请参考 IPython 或 Jupyter 中的文档字符串。 + +表 9.2:一些`fig.savefig`选项 + +| 参数 | 描述 | +| --- | --- | +| `fname` | 包含文件路径或 Python 文件对象的字符串。图形格式从文件扩展名中推断(例如,`.pdf`表示 PDF,`.png`表示 PNG)。 | +| `dpi` | 每英寸点数的图形分辨率;在 IPython 中默认为 100,在 Jupyter 中默认为 72,但可以进行配置。 | +| `facecolor, edgecolor` | 子图外部的图形背景颜色;默认为`"w"`(白色)。 | +| `format` | 要使用的显式文件格式(`"png"`、`"pdf"`、`"svg"`、`"ps"`、`"eps"`等)。 | + +### matplotlib 配置 + +matplotlib 预先配置了色彩方案和默认设置,主要用于准备出版图。幸运的是,几乎所有默认行为都可以通过全局参数进行自定义,这些参数控制图形大小、子图间距、颜色、字体大小、网格样式等。从 Python 编程方式修改配置的一种方法是使用`rc`方法;例如,要将全局默认图形大小设置为 10×10,可以输入: + +```py +plt.rc("figure", figsize=(10, 10)) +``` + +所有当前的配置设置都可以在`plt.rcParams`字典中找到,并且可以通过调用`plt.rcdefaults()`函数将其恢复为默认值。 + +`rc`的第一个参数是您希望自定义的组件,例如`"figure"`、`"axes"`、`"xtick"`、`"ytick"`、`"grid"`、`"legend"`或其他许多选项。之后可以跟随一系列关键字参数,指示新的参数。在程序中写下选项的便捷方式是作为一个字典: + +```py +plt.rc("font", family="monospace", weight="bold", size=8) +``` + +要进行更广泛的自定义并查看所有选项列表,matplotlib 附带了一个配置文件*matplotlibrc*,位于*matplotlib/mpl-data*目录中。如果您自定义此文件并将其放在名为*.matplotlibrc*的主目录中,每次使用 matplotlib 时都会加载它。 + +正如我们将在下一节中看到的,seaborn 包具有几个内置的绘图主题或*样式*,这些主题或样式在内部使用 matplotlib 的配置系统。 + +## 9.2 使用 pandas 和 seaborn 绘图 + +matplotlib 可以是一个相当低级的工具。您可以从其基本组件中组装图表:数据显示(即绘图类型:线条、柱状图、箱线图、散点图、等高线图等)、图例、标题、刻度标签和其他注释。 + +在 pandas 中,我们可能有多列数据,以及行和列标签。pandas 本身具有内置方法,简化了从 DataFrame 和 Series 对象创建可视化的过程。另一个库是[`seaborn`](https://seaborn.pydata.org),这是一个建立在 matplotlib 之上的高级统计图形库。seaborn 简化了创建许多常见可视化类型的过程。 + +### 线图 + +Series 和 DataFrame 具有`plot`属性,用于创建一些基本的绘图类型。默认情况下,`plot()`生成线图(参见简单 Series 绘图): + +```py +In [61]: s = pd.Series(np.random.standard_normal(10).cumsum(), index=np.arange(0, + 100, 10)) + +In [62]: s.plot() +``` + +![](img/94759a9f5e61afbdf151f5410cb1d6ca.png) + +图 9.13:简单 Series 绘图 + +Series 对象的索引被传递给 matplotlib 以在 x 轴上绘制,尽管您可以通过传递 `use_index=False` 来禁用此功能。x 轴刻度和限制可以通过 `xticks` 和 `xlim` 选项进行调整,y 轴分别通过 `yticks` 和 `ylim` 进行调整。请参见 表 9.3 以获取 `plot` 选项的部分列表。我将在本节中评论其中一些,并留下其余的供您探索。 + +表 9.3:`Series.plot` 方法参数 + +| 参数 | 描述 | +| --- | --- | +| `label` | 图例标签 | +| `ax` | 要绘制的 matplotlib 子图对象;如果未传递任何内容,则使用活动的 matplotlib 子图 | +| `style` | 样式字符串,如 `"ko--"`,传递给 matplotlib | +| `alpha` | 图形填充不透明度(从 0 到 1) | +| `kind` | 可以是 `"area"`, `"bar"`, `"barh"`, `"density"`, `"hist"`, `"kde"`, `"line"`, 或 `"pie"`;默认为 `"line"` | +| `figsize` | 要创建的图形对象的大小 | +| `logx` | 在 x 轴上进行对数缩放,传递 `True`;传递 `"sym"` 以进行允许负值的对称对数缩放 | +| `logy` | 在 y 轴上进行对数缩放,传递 `True`;传递 `"sym"` 以进行允许负值的对称对数缩放 | +| `title` | 用于图的标题 | +| `use_index` | 使用对象索引作为刻度标签 | +| `rot` | 刻度标签的旋转(0 到 360) | +| `xticks` | 用于 x 轴刻度的值 | +| `yticks` | 用于 y 轴刻度的值 | +| `xlim` | x 轴限制(例如,`[0, 10]`) | +| `ylim` | y 轴限制 | +| `grid` | 显示坐标轴网格(默认关闭) | + +大多数 pandas 的绘图方法都接受一个可选的 `ax` 参数,可以是一个 matplotlib 子图对象。这样可以在网格布局中更灵活地放置子图。 + +DataFrame 的 `plot` 方法将其每列作为不同的线绘制在同一个子图上,自动创建图例(请参见 简单的 DataFrame 绘图): + +```py +In [63]: df = pd.DataFrame(np.random.standard_normal((10, 4)).cumsum(0), + ....: columns=["A", "B", "C", "D"], + ....: index=np.arange(0, 100, 10)) + +In [64]: plt.style.use('grayscale') + +In [65]: df.plot() +``` + +![](img/fdaeef8acbb8ceb4025d1c1bd91d31b1.png) + +图 9.14:简单的 DataFrame 绘图 + +注意 + +这里我使用了 `plt.style.use('grayscale')` 来切换到更适合黑白出版的颜色方案,因为一些读者可能无法看到完整的彩色图。 + +`plot` 属性包含不同绘图类型的方法“家族”。例如,`df.plot()` 等同于 `df.plot.line()`。我们将在接下来探索其中一些方法。 + +注意 + +`plot` 的其他关键字参数会传递给相应的 matplotlib 绘图函数,因此您可以通过学习更多关于 matplotlib API 的知识来进一步自定义这些图。 + +DataFrame 有许多选项,允许对列的处理方式进行一定的灵活性,例如,是否将它们全部绘制在同一个子图上,还是创建单独的子图。更多信息请参见 表 9.4。 + +表 9.4:DataFrame 特定的绘图参数 + +| 参数 | 描述 | +| --- | --- | +| `subplots` | 在单独的子图中绘制每个 DataFrame 列 | +| `layouts` | 2 元组(行数,列数),提供子图的布局 | +| `sharex` | 如果 `subplots=True`,共享相同的 x 轴,链接刻度和限制 | +| `sharey` | 如果 `subplots=True`,共享相同的 y 轴 | +| `legend` | 添加子图图例(默认为 `True`) | +| `sort_columns` | 按字母顺序绘制列;默认使用现有列顺序 | + +注意 + +有关时间序列绘图,请参见 第十一章:时间序列。 + +### 条形图 + +`plot.bar()` 和 `plot.barh()` 分别绘制垂直和水平条形图。在这种情况下,Series 或 DataFrame 的索引将用作 x(`bar`)或 y(`barh`)刻度(请参见 水平和垂直条形图): + +```py +In [66]: fig, axes = plt.subplots(2, 1) + +In [67]: data = pd.Series(np.random.uniform(size=16), index=list("abcdefghijklmno +p")) + +In [68]: data.plot.bar(ax=axes[0], color="black", alpha=0.7) +Out[68]: + +In [69]: data.plot.barh(ax=axes[1], color="black", alpha=0.7) +``` + +![](img/baca2f4ab210ab7fbb1363acd5366b42.png) + +图 9.15:水平和垂直条形图 + +使用 DataFrame,条形图将每行中的值分组在条形图中,侧边显示,每个值一个条形图。请参见 DataFrame 条形图: + +```py +In [71]: df = pd.DataFrame(np.random.uniform(size=(6, 4)), + ....: index=["one", "two", "three", "four", "five", "six"], + ....: columns=pd.Index(["A", "B", "C", "D"], name="Genus")) + +In [72]: df +Out[72]: +Genus A B C D +one 0.370670 0.602792 0.229159 0.486744 +two 0.420082 0.571653 0.049024 0.880592 +three 0.814568 0.277160 0.880316 0.431326 +four 0.374020 0.899420 0.460304 0.100843 +five 0.433270 0.125107 0.494675 0.961825 +six 0.601648 0.478576 0.205690 0.560547 + +In [73]: df.plot.bar() +``` + +![](img/7b3fc44566d76e7e8e544437c2dffbcc.png) + +图 9.16:DataFrame 条形图 + +请注意,DataFrame 列上的“种属”名称用于标题图例。 + +我们通过传递`stacked=True`从 DataFrame 创建堆叠条形图,导致每行中的值水平堆叠在一起(参见 DataFrame 堆叠条形图): + +```py +In [75]: df.plot.barh(stacked=True, alpha=0.5) +``` + +![](img/f28517ea0c78eaca45d3a1d21b743605.png) + +图 9.17:DataFrame 堆叠条形图 + +注意 + +一个有用的条形图的制作方法是使用`value_counts`来可视化 Series 的值频率:`s.value_counts().plot.bar()`。 + +让我们看一个关于餐厅小费的示例数据集。假设我们想要制作一个堆叠条形图,显示每天每个派对规模的数据点的百分比。我使用`read_csv`加载数据,并通过日期和派对规模进行交叉制表。`pandas.crosstab`函数是从两个 DataFrame 列计算简单频率表的便捷方法: + +```py +In [77]: tips = pd.read_csv("examples/tips.csv") + +In [78]: tips.head() +Out[78]: + total_bill tip smoker day time size +0 16.99 1.01 No Sun Dinner 2 +1 10.34 1.66 No Sun Dinner 3 +2 21.01 3.50 No Sun Dinner 3 +3 23.68 3.31 No Sun Dinner 2 +4 24.59 3.61 No Sun Dinner 4 + +In [79]: party_counts = pd.crosstab(tips["day"], tips["size"]) + +In [80]: party_counts = party_counts.reindex(index=["Thur", "Fri", "Sat", "Sun"]) + +In [81]: party_counts +Out[81]: +size 1 2 3 4 5 6 +day +Thur 1 48 4 5 1 3 +Fri 1 16 1 1 0 0 +Sat 2 53 18 13 1 0 +Sun 0 39 15 18 3 1 +``` + +由于没有很多一人和六人的派对,我在这里删除它们: + +```py +In [82]: party_counts = party_counts.loc[:, 2:5] +``` + +然后,对每一行进行归一化,使总和为 1,并绘制图表(参见每天各尺寸派对的比例): + +```py +# Normalize to sum to 1 +In [83]: party_pcts = party_counts.div(party_counts.sum(axis="columns"), + ....: axis="index") + +In [84]: party_pcts +Out[84]: +size 2 3 4 5 +day +Thur 0.827586 0.068966 0.086207 0.017241 +Fri 0.888889 0.055556 0.055556 0.000000 +Sat 0.623529 0.211765 0.152941 0.011765 +Sun 0.520000 0.200000 0.240000 0.040000 + +In [85]: party_pcts.plot.bar(stacked=True) +``` + +![](img/fecc24f715268acabec45f889288da9c.png) + +图 9.18:每天各尺寸派对的比例 + +因此,您可以看到在这个数据集中,派对规模似乎在周末增加。 + +对于需要在制作图表之前进行聚合或总结的数据,使用`seaborn`包可以使事情变得更简单(使用`conda install seaborn`进行安装)。现在让我们用 seaborn 查看小费百分比按天的情况(查看带误差条的每日小费百分比以查看结果图): + +```py +In [87]: import seaborn as sns + +In [88]: tips["tip_pct"] = tips["tip"] / (tips["total_bill"] - tips["tip"]) + +In [89]: tips.head() +Out[89]: + total_bill tip smoker day time size tip_pct +0 16.99 1.01 No Sun Dinner 2 0.063204 +1 10.34 1.66 No Sun Dinner 3 0.191244 +2 21.01 3.50 No Sun Dinner 3 0.199886 +3 23.68 3.31 No Sun Dinner 2 0.162494 +4 24.59 3.61 No Sun Dinner 4 0.172069 + +In [90]: sns.barplot(x="tip_pct", y="day", data=tips, orient="h") +``` + +![](img/1c644fda5940269dac22103d8713dae4.png) + +图 9.19:每日小费百分比带误差条 + +seaborn 中的绘图函数接受一个`data`参数,它可以是一个 pandas DataFrame。其他参数是指列名。因为在`day`的每个值中有多个观察值,所以条形图是`tip_pct`的平均值。在条形图上画的黑线代表 95%的置信区间(可以通过可选参数进行配置)。 + +`seaborn.barplot`有一个`hue`选项,可以使我们按照额外的分类值进行拆分(参见每日和时间的小费百分比): + +```py +In [92]: sns.barplot(x="tip_pct", y="day", hue="time", data=tips, orient="h") +``` + +![](img/7921e3ccd70b031f36c44b85107918d7.png) + +图 9.20:每日和时间的小费百分比 + +请注意,seaborn 自动更改了图表的美学特征:默认颜色调色板、图表背景和网格线颜色。您可以使用`seaborn.set_style`在不同的图表外观之间切换: + +```py +In [94]: sns.set_style("whitegrid") +``` + +在为黑白打印媒介制作图表时,您可能会发现设置灰度调色板很有用,如下所示: + +```py +sns.set_palette("Greys_r") +``` + +### 直方图和密度图 + +*直方图*是一种显示值频率的离散化条形图。数据点被分成离散的、均匀间隔的箱子,并绘制每个箱子中的数据点数。使用之前的小费数据,我们可以使用 Series 的`plot.hist`方法制作总账单的小费百分比的直方图(参见小费百分比的直方图): + +```py +In [96]: tips["tip_pct"].plot.hist(bins=50) +``` + +![](img/ac1010ef5553eff57302677c1ac29851.png) + +图 9.21:小费百分比的直方图 + +一个相关的图表类型是*密度图*,它是通过计算可能生成观察数据的连续概率分布的估计而形成的。通常的做法是将这个分布近似为“核”混合——即,像正态分布这样的简单分布。因此,密度图也被称为核密度估计(KDE)图。使用`plot.density`可以使用传统的正态混合估计制作密度图(参见小费百分比的密度图): + +```py +In [98]: tips["tip_pct"].plot.density() +``` + +![](img/53903a6f57aed96b5029b4bcb265ceb7.png) + +图 9.22:小费百分比的密度图 + +这种情节需要 SciPy,所以如果您还没有安装它,可以暂停一下然后安装: + +```py +conda install scipy +``` + +通过其`histplot`方法,seaborn 使直方图和密度图更加容易,可以同时绘制直方图和连续密度估计。例如,考虑一个由两个不同标准正态分布的抽样组成的双峰分布(请参见 Normalized histogram of normal mixture): + +```py +In [100]: comp1 = np.random.standard_normal(200) + +In [101]: comp2 = 10 + 2 * np.random.standard_normal(200) + +In [102]: values = pd.Series(np.concatenate([comp1, comp2])) + +In [103]: sns.histplot(values, bins=100, color="black") +``` + +![](img/a4c5cd6fc9e8e06951e967c00791ae0c.png) + +图 9.23:正态混合的归一化直方图 + +### 散点图或点图 + +点图或散点图可以是检查两个一维数据系列之间关系的有用方法。例如,这里我们从 statsmodels 项目加载`macrodata`数据集,选择几个变量,然后计算对数差异: + +```py +In [104]: macro = pd.read_csv("examples/macrodata.csv") + +In [105]: data = macro[["cpi", "m1", "tbilrate", "unemp"]] + +In [106]: trans_data = np.log(data).diff().dropna() + +In [107]: trans_data.tail() +Out[107]: + cpi m1 tbilrate unemp +198 -0.007904 0.045361 -0.396881 0.105361 +199 -0.021979 0.066753 -2.277267 0.139762 +200 0.002340 0.010286 0.606136 0.160343 +201 0.008419 0.037461 -0.200671 0.127339 +202 0.008894 0.012202 -0.405465 0.042560 +``` + +然后我们可以使用 seaborn 的`regplot`方法,它可以制作散点图并拟合线性回归线(参见 A seaborn regression/scatter plot): + +```py +In [109]: ax = sns.regplot(x="m1", y="unemp", data=trans_data) + +In [110]: ax.set_title("Changes in log(m1) versus log(unemp)") +``` + +![](img/794c774025d4e8e044c70ae0199ae555.png) + +图 9.24:一个 seaborn 回归/散点图 + +在探索性数据分析中,查看一组变量之间的所有散点图是有帮助的;这被称为*pairs*图或*scatter plot matrix*。从头开始制作这样的图需要一些工作,因此 seaborn 有一个方便的`pairplot`函数,支持将每个变量的直方图或密度估计放在对角线上(请参见 Pair plot matrix of statsmodels macro data 以查看生成的图): + +```py +In [111]: sns.pairplot(trans_data, diag_kind="kde", plot_kws={"alpha": 0.2}) +``` + +![](img/dff02874e6216c043e03726ae580d604.png) + +图 9.25:statsmodels 宏数据的 pairs 图矩阵 + +您可能会注意到`plot_kws`参数。这使我们能够将配置选项传递给对角线元素上的各个绘图调用。查看`seaborn.pairplot`文档字符串以获取更详细的配置选项。 + +### Facet Grids 和分类数据 + +那么对于具有额外分组维度的数据集呢?一种可视化具有许多分类变量的数据的方法是使用*facet grid*,这是一个二维布局的图,其中数据根据某个变量的不同值在每个轴上分割到各个图中。seaborn 有一个有用的内置函数`catplot`,简化了根据分类变量拆分的许多种 facet 图的制作(请参见 Tipping percentage by day/time/smoker 以查看生成的图): + +```py +In [112]: sns.catplot(x="day", y="tip_pct", hue="time", col="smoker", + .....: kind="bar", data=tips[tips.tip_pct < 1]) +``` + +![](img/ebc8b1612dde160033c19bae91791567.png) + +图 9.26:按天/时间/吸烟者的小费百分比 + +与在 facet 内通过不同的条形颜色对“时间”进行分组不同,我们还可以通过为每个`time`值添加一行来扩展 facet grid(请参见 Tipping percentage by day split by time/smoker): + +```py +In [113]: sns.catplot(x="day", y="tip_pct", row="time", + .....: col="smoker", + .....: kind="bar", data=tips[tips.tip_pct < 1]) +``` + +![](img/492765c1f52237f69c502e7807e289aa.png) + +图 9.27:按天分割的小费百分比按时间/吸烟者 + +`catplot`支持其他可能有用的绘图类型,具体取决于您要显示的内容。例如,*箱线图*(显示中位数、四分位数和异常值)可以是一种有效的可视化类型(请参见 Box plot of tipping percentage by day): + +```py +In [114]: sns.catplot(x="tip_pct", y="day", kind="box", + .....: data=tips[tips.tip_pct < 0.5]) +``` + +![](img/1acd381f491d15f26798c13e930adffc.png) + +图 9.28:按天的小费百分比箱线图 + +您可以使用更通用的`seaborn.FacetGrid`类创建自己的 facet grid 图。有关更多信息,请参阅[seaborn 文档](https://seaborn.pydata.org/)。 + +## 9.3 其他 Python 可视化工具 + +与开源软件一样,Python 中有许多用于创建图形的选项(太多了无法列出)。自 2010 年以来,许多开发工作都集中在为在网页上发布的交互式图形创建工具上。使用诸如[Altair](https://altair-viz.github.io)、[Bokeh](http://bokeh.pydata.org)和[Plotly](https://plotly.com/python)等工具,现在可以在 Python 中指定动态、交互式图形,用于与 Web 浏览器一起使用。 + +对于为印刷品或网络创建静态图形,我建议使用 matplotlib 以及构建在 matplotlib 基础上的库,如 pandas 和 seaborn,以满足您的需求。对于其他数据可视化需求,学习如何使用其他可用工具可能会有所帮助。我鼓励您探索这个生态系统,因为它将继续发展和创新。 + +数据可视化方面的一本优秀书籍是 Claus O. Wilke 的《数据可视化基础》(O'Reilly),可以在印刷版或 Claus 的网站[`clauswilke.com/dataviz`](https://clauswilke.com/dataviz)上找到。 + +## 9.4 结论 + +本章的目标是通过使用 pandas、matplotlib 和 seaborn 进行一些基本数据可视化,让您初步了解。如果在您的工作中视觉传达数据分析结果很重要,我鼓励您寻找资源,了解更多关于有效数据可视化的知识。这是一个活跃的研究领域,您可以通过在线和印刷的许多优秀学习资源进行实践。 + +在下一章中,我们将关注使用 pandas 进行数据聚合和分组操作。 diff --git a/translations/cn/pyda3e_13.md b/translations/cn/pyda3e_13.md new file mode 100644 index 000000000..23fa3d070 --- /dev/null +++ b/translations/cn/pyda3e_13.md @@ -0,0 +1,1814 @@ +# 十、数据聚合和组操作 + +> 原文:[`wesmckinney.com/book/data-aggregation`](https://wesmckinney.com/book/data-aggregation) +> +> 译者:[飞龙](https://github.com/wizardforcel) +> +> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) + + +> 此开放访问网络版本的《Python 数据分析第三版》现已作为[印刷版和数字版](https://amzn.to/3DyLaJc)的伴侣提供。如果您发现任何勘误,请[在此处报告](https://oreilly.com/catalog/0636920519829/errata)。请注意,由 Quarto 生成的本站点的某些方面与 O'Reilly 的印刷版和电子书版本的格式不同。 +> +> 如果您发现本书的在线版本有用,请考虑[订购纸质版](https://amzn.to/3DyLaJc)或[无 DRM 的电子书](https://www.ebooks.com/en-us/book/210644288/python-for-data-analysis/wes-mckinney/?affId=WES398681F)以支持作者。本网站的内容不得复制或再生产。代码示例采用 MIT 许可,可在 GitHub 或 Gitee 上找到。 + +对数据集进行分类并对每个组应用函数,无论是聚合还是转换,都可能是数据分析工作流程的关键组成部分。加载、合并和准备数据集后,您可能需要计算组统计信息或可能需要为报告或可视化目的计算*数据透视表*。pandas 提供了一个多功能的`groupby`接口,使您能够以自然的方式切片、切块和总结数据集。 + +关系数据库和 SQL(结构化查询语言)的流行原因之一是数据可以很容易地进行连接、过滤、转换和聚合。然而,像 SQL 这样的查询语言对可以执行的组操作类型施加了一定的限制。正如您将看到的,借助 Python 和 pandas 的表达力,我们可以通过将它们表达为自定义 Python 函数来执行相当复杂的组操作,这些函数操作与每个组相关联的数据。在本章中,您将学习如何: + ++ 使用一个或多个键(以函数、数组或 DataFrame 列名的形式)将 pandas 对象分成片段 + ++ 计算组摘要统计信息,如计数、均值或标准差,或用户定义的函数 + ++ 应用组内转换或其他操作,如归一化、线性回归、排名或子集选择 + ++ 计算数据透视表和交叉制表 + ++ 执行分位数分析和其他统计组分析 + +注意 + +对时间序列数据进行基于时间的聚合,是`groupby`的一个特殊用例,在本书中被称为*重新采样*,将在第十一章:时间序列中单独处理。*与其他章节一样,我们首先导入 NumPy 和 pandas: + +```py +In [12]: import numpy as np + +In [13]: import pandas as pd +``` + +## 10.1 如何思考组操作 + +Hadley Wickham,R 编程语言许多流行包的作者,为描述组操作创造了术语*split-apply-combine*。在过程的第一阶段中,包含在 pandas 对象中的数据,无论是 Series、DataFrame 还是其他形式,都根据您提供的一个或多个*键*被*分割*成组。分割是在对象的特定轴上执行的。例如,DataFrame 可以根据其行(`axis="index"`)或列(`axis="columns"`)进行分组。完成此操作后,将*应用*一个函数到每个组,生成一个新值。最后,所有这些函数应用的结果将*合并*成一个结果对象。结果对象的形式通常取决于对数据的操作。请参见图 10.1 以查看简单组聚合的模拟。 + +每个分组键可以采用多种形式,键不必是相同类型的: + ++ 一个与被分组的轴长度相同的值列表或数组 + ++ DataFrame 中表示列名的值 + ++ 一个字典或 Series,给出了被分组的轴上的值与组名之间的对应关系 + ++ 要在轴索引或索引中的个别标签上调用的函数 + +![](img/568aea090be607a174816297616d3b0d.png) + +图 10.1:组聚合的示例 + +请注意,后三种方法是用于生成用于拆分对象的值数组的快捷方式。如果这一切看起来很抽象,不要担心。在本章中,我将给出所有这些方法的许多示例。为了开始,这里是一个作为 DataFrame 的小表格数据集: + +```py +In [14]: df = pd.DataFrame({"key1" : ["a", "a", None, "b", "b", "a", None], + ....: "key2" : pd.Series([1, 2, 1, 2, 1, None, 1], + ....: dtype="Int64"), + ....: "data1" : np.random.standard_normal(7), + ....: "data2" : np.random.standard_normal(7)}) + +In [15]: df +Out[15]: + key1 key2 data1 data2 +0 a 1 -0.204708 0.281746 +1 a 2 0.478943 0.769023 +2 None 1 -0.519439 1.246435 +3 b 2 -0.555730 1.007189 +4 b 1 1.965781 -1.296221 +5 a 1.393406 0.274992 +6 None 1 0.092908 0.228913 +``` + +假设你想使用 `key1` 标签计算 `data1` 列的均值。有多种方法可以做到这一点。一种方法是访问 `data1` 并使用 `key1` 列(一个 Series)调用 `groupby`: + +```py +In [16]: grouped = df["data1"].groupby(df["key1"]) + +In [17]: grouped +Out[17]: +``` + +这个 `grouped` 变量现在是一个特殊的 *"GroupBy"* 对象。除了一些关于组键 `df["key1"]` 的中间数据之外,它实际上还没有计算任何东西。这个对象的想法是它包含了对每个组应用某些操作所需的所有信息。例如,要计算组均值,我们可以调用 GroupBy 的 `mean` 方法: + +```py +In [18]: grouped.mean() +Out[18]: +key1 +a 0.555881 +b 0.705025 +Name: data1, dtype: float64 +``` + +稍后在 数据聚合 中,我将更详细地解释当你调用 `.mean()` 时会发生什么。这里重要的是,数据(一个 Series)已经通过在组键上拆分数据进行聚合,产生了一个新的 Series,现在由 `key1` 列中的唯一值进行索引。结果索引的名称是 `"key1"`,因为 DataFrame 列 `df["key1"]` 是这样的。 + +如果我们传递了多个数组作为列表,将会得到不同的结果: + +```py +In [19]: means = df["data1"].groupby([df["key1"], df["key2"]]).mean() + +In [20]: means +Out[20]: +key1 key2 +a 1 -0.204708 + 2 0.478943 +b 1 1.965781 + 2 -0.555730 +Name: data1, dtype: float64 +``` + +在这里,我们使用两个键对数据进行分组,结果 Series 现在具有由观察到的唯一键对组成的分层索引: + +```py +In [21]: means.unstack() +Out[21]: +key2 1 2 +key1 +a -0.204708 0.478943 +b 1.965781 -0.555730 +``` + +在这个例子中,组键都是 Series,尽管它们可以是任何正确长度的数组: + +```py +In [22]: states = np.array(["OH", "CA", "CA", "OH", "OH", "CA", "OH"]) + +In [23]: years = [2005, 2005, 2006, 2005, 2006, 2005, 2006] + +In [24]: df["data1"].groupby([states, years]).mean() +Out[24]: +CA 2005 0.936175 + 2006 -0.519439 +OH 2005 -0.380219 + 2006 1.029344 +Name: data1, dtype: float64 +``` + +通常,分组信息在与你要处理的数据相同的 DataFrame 中找到。在这种情况下,你可以将列名(无论是字符串、数字还是其他 Python 对象)作为组键传递: + +```py +In [25]: df.groupby("key1").mean() +Out[25]: + key2 data1 data2 +key1 +a 1.5 0.555881 0.441920 +b 1.5 0.705025 -0.144516 + +In [26]: df.groupby("key2").mean(numeric_only=True) +Out[26]: + data1 data2 +key2 +1 0.333636 0.115218 +2 -0.038393 0.888106 + +In [27]: df.groupby(["key1", "key2"]).mean() +Out[27]: + data1 data2 +key1 key2 +a 1 -0.204708 0.281746 + 2 0.478943 0.769023 +b 1 1.965781 -1.296221 + 2 -0.555730 1.007189 +``` + +你可能会注意到,在第二种情况下,有必要传递 `numeric_only=True`,因为 `key1` 列不是数值列,因此不能使用 `mean()` 进行聚合。 + +无论使用 `groupby` 的目的是什么,一个通常有用的 GroupBy 方法是 `size`,它返回一个包含组大小的 Series: + +```py +In [28]: df.groupby(["key1", "key2"]).size() +Out[28]: +key1 key2 +a 1 1 + 2 1 +b 1 1 + 2 1 +dtype: int64 +``` + +请注意,默认情况下,组键中的任何缺失值都会被排除在结果之外。通过将 `dropna=False` 传递给 `groupby` 可以禁用此行为: + +```py +In [29]: df.groupby("key1", dropna=False).size() +Out[29]: +key1 +a 3 +b 2 +NaN 2 +dtype: int64 + +In [30]: df.groupby(["key1", "key2"], dropna=False).size() +Out[30]: +key1 key2 +a 1 1 + 2 1 + 1 +b 1 1 + 2 1 +NaN 1 2 +dtype: int64 +``` + +一种类似于 `size` 的组函数是 count,它计算每个组中的非空值的数量: + +```py +In [31]: df.groupby("key1").count() +Out[31]: + key2 data1 data2 +key1 +a 2 3 3 +b 2 2 2 +``` + +### 遍历组 + +`groupby` 返回的对象支持迭代,生成一个包含组名和数据块的 2 元组序列。考虑以下内容: + +```py +In [32]: for name, group in df.groupby("key1"): + ....: print(name) + ....: print(group) + ....: +a + key1 key2 data1 data2 +0 a 1 -0.204708 0.281746 +1 a 2 0.478943 0.769023 +5 a 1.393406 0.274992 +b + key1 key2 data1 data2 +3 b 2 -0.555730 1.007189 +4 b 1 1.965781 -1.296221 +``` + +在多个键的情况下,元组中的第一个元素将是一个键值的元组: + +```py +In [33]: for (k1, k2), group in df.groupby(["key1", "key2"]): + ....: print((k1, k2)) + ....: print(group) + ....: +('a', 1) + key1 key2 data1 data2 +0 a 1 -0.204708 0.281746 +('a', 2) + key1 key2 data1 data2 +1 a 2 0.478943 0.769023 +('b', 1) + key1 key2 data1 data2 +4 b 1 1.965781 -1.296221 +('b', 2) + key1 key2 data1 data2 +3 b 2 -0.55573 1.007189 +``` + +当然,你可以选择对数据块做任何你想做的事情。一个你可能会发现有用的方法是将数据块计算为一个字典: + +```py +In [34]: pieces = {name: group for name, group in df.groupby("key1")} + +In [35]: pieces["b"] +Out[35]: + key1 key2 data1 data2 +3 b 2 -0.555730 1.007189 +4 b 1 1.965781 -1.296221 +``` + +默认情况下,`groupby` 在 `axis="index"` 上进行分组,但你可以在任何其他轴上进行分组。例如,我们可以按照我们的示例 `df` 的列是否以 `"key"` 或 `"data"` 开头进行分组: + +```py +In [36]: grouped = df.groupby({"key1": "key", "key2": "key", + ....: "data1": "data", "data2": "data"}, axis="columns") +``` + +我们可以这样打印出组: + +```py +In [37]: for group_key, group_values in grouped: + ....: print(group_key) + ....: print(group_values) + ....: +data + data1 data2 +0 -0.204708 0.281746 +1 0.478943 0.769023 +2 -0.519439 1.246435 +3 -0.555730 1.007189 +4 1.965781 -1.296221 +5 1.393406 0.274992 +6 0.092908 0.228913 +key + key1 key2 +0 a 1 +1 a 2 +2 None 1 +3 b 2 +4 b 1 +5 a +6 None 1 +``` + +### 选择列或列的子集 + +从 DataFrame 创建的 GroupBy 对象进行索引,使用列名或列名数组会对聚合进行列子集操作。这意味着: + +```py +df.groupby("key1")["data1"] +df.groupby("key1")[["data2"]] +``` + +是方便的: + +```py +df["data1"].groupby(df["key1"]) +df[["data2"]].groupby(df["key1"]) +``` + +特别是对于大型数据集,可能只需要聚合几列。例如,在前面的数据集中,仅计算 `data2` 列的均值并将结果作为 DataFrame 获取,我们可以这样写: + +```py +In [38]: df.groupby(["key1", "key2"])[["data2"]].mean() +Out[38]: + data2 +key1 key2 +a 1 0.281746 + 2 0.769023 +b 1 -1.296221 + 2 1.007189 +``` + +通过这种索引操作返回的对象是一个分组的 DataFrame(如果传递了列表或数组),或者是一个分组的 Series(如果只传递了一个列名作为标量): + +```py +In [39]: s_grouped = df.groupby(["key1", "key2"])["data2"] + +In [40]: s_grouped +Out[40]: + +In [41]: s_grouped.mean() +Out[41]: +key1 key2 +a 1 0.281746 + 2 0.769023 +b 1 -1.296221 + 2 1.007189 +Name: data2, dtype: float64 +``` + +### 使用字典和 Series 进行分组 + +分组信息可能以其他形式存在,而不仅仅是数组。让我们考虑另一个示例 DataFrame: + +```py +In [42]: people = pd.DataFrame(np.random.standard_normal((5, 5)), + ....: columns=["a", "b", "c", "d", "e"], + ....: index=["Joe", "Steve", "Wanda", "Jill", "Trey"]) + +In [43]: people.iloc[2:3, [1, 2]] = np.nan # Add a few NA values + +In [44]: people +Out[44]: + a b c d e +Joe 1.352917 0.886429 -2.001637 -0.371843 1.669025 +Steve -0.438570 -0.539741 0.476985 3.248944 -1.021228 +Wanda -0.577087 NaN NaN 0.523772 0.000940 +Jill 1.343810 -0.713544 -0.831154 -2.370232 -1.860761 +Trey -0.860757 0.560145 -1.265934 0.119827 -1.063512 +``` + +现在,假设我有列的分组对应关系,并且想要按组对列求和: + +```py +In [45]: mapping = {"a": "red", "b": "red", "c": "blue", + ....: "d": "blue", "e": "red", "f" : "orange"} +``` + +现在,您可以从这个字典构造一个数组传递给`groupby`,但我们可以直接传递字典(我包含了键`"f"`来突出显示未使用的分组键是可以的): + +```py +In [46]: by_column = people.groupby(mapping, axis="columns") + +In [47]: by_column.sum() +Out[47]: + blue red +Joe -2.373480 3.908371 +Steve 3.725929 -1.999539 +Wanda 0.523772 -0.576147 +Jill -3.201385 -1.230495 +Trey -1.146107 -1.364125 +``` + +相同的功能也适用于 Series,它可以被视为一个固定大小的映射: + +```py +In [48]: map_series = pd.Series(mapping) + +In [49]: map_series +Out[49]: +a red +b red +c blue +d blue +e red +f orange +dtype: object + +In [50]: people.groupby(map_series, axis="columns").count() +Out[50]: + blue red +Joe 2 3 +Steve 2 3 +Wanda 1 2 +Jill 2 3 +Trey 2 3 +``` + +### 使用函数分组 + +使用 Python 函数比使用字典或 Series 定义分组映射更通用。作为分组键传递的任何函数将针对每个索引值(或者如果使用`axis="columns"`则是每个列值)调用一次,返回值将用作分组名称。更具体地,考虑前一节中的示例 DataFrame,其中人们的名字作为索引值。假设您想按名称长度分组。虽然您可以计算一个字符串长度的数组,但更简单的方法是只传递`len`函数: + +```py +In [51]: people.groupby(len).sum() +Out[51]: + a b c d e +3 1.352917 0.886429 -2.001637 -0.371843 1.669025 +4 0.483052 -0.153399 -2.097088 -2.250405 -2.924273 +5 -1.015657 -0.539741 0.476985 3.772716 -1.020287 +``` + +将函数与数组、字典或 Series 混合在一起不是问题,因为所有内容在内部都会转换为数组: + +```py +In [52]: key_list = ["one", "one", "one", "two", "two"] + +In [53]: people.groupby([len, key_list]).min() +Out[53]: + a b c d e +3 one 1.352917 0.886429 -2.001637 -0.371843 1.669025 +4 two -0.860757 -0.713544 -1.265934 -2.370232 -1.860761 +5 one -0.577087 -0.539741 0.476985 0.523772 -1.021228 +``` + +### 按索引级别分组 + +对于具有层次索引的数据集,最后一个便利之处是能够使用轴索引的一个级别进行聚合。让我们看一个例子: + +```py +In [54]: columns = pd.MultiIndex.from_arrays([["US", "US", "US", "JP", "JP"], + ....: [1, 3, 5, 1, 3]], + ....: names=["cty", "tenor"]) + +In [55]: hier_df = pd.DataFrame(np.random.standard_normal((4, 5)), columns=column +s) + +In [56]: hier_df +Out[56]: +cty US JP +tenor 1 3 5 1 3 +0 0.332883 -2.359419 -0.199543 -1.541996 -0.970736 +1 -1.307030 0.286350 0.377984 -0.753887 0.331286 +2 1.349742 0.069877 0.246674 -0.011862 1.004812 +3 1.327195 -0.919262 -1.549106 0.022185 0.758363 +``` + +要按级别分组,请使用`level`关键字传递级别编号或名称: + +```py +In [57]: hier_df.groupby(level="cty", axis="columns").count() +Out[57]: +cty JP US +0 2 3 +1 2 3 +2 2 3 +3 2 3 +``` + +## 10.2 数据聚合 + +*聚合*指的是从数组中产生标量值的任何数据转换。前面的示例中使用了其中几个,包括`mean`、`count`、`min`和`sum`。当您在 GroupBy 对象上调用`mean()`时,您可能会想知道发生了什么。许多常见的聚合,如表 10.1 中找到的那些,都有优化的实现。但是,您不仅限于这组方法。 + +表 10.1:优化的`groupby`方法 + +| 函数名称 | 描述 | +| --- | --- | +| `any, all` | 如果任何(一个或多个值)或所有非 NA 值为“真值”则返回`True` | +| `count` | 非 NA 值的数量 | +| `cummin, cummax` | 非 NA 值的累积最小值和最大值 | +| `cumsum` | 非 NA 值的累积和 | +| `cumprod` | 非 NA 值的累积乘积 | +| `first, last` | 首个和最后一个非 NA 值 | +| `mean` | 非 NA 值的均值 | +| `median` | 非 NA 值的算术中位数 | +| `min, max` | 非 NA 值的最小值和最大值 | +| `nth` | 检索在排序顺序中出现在位置`n`的值 | +| `ohlc` | 为类似时间序列的数据计算四个“开盘-最高-最低-收盘”统计数据 | +| `prod` | 非 NA 值的乘积 | +| `quantile` | 计算样本分位数 | +| `rank` | 非 NA 值的序数排名,类似于调用`Series.rank` | +| `size` | 计算组大小,将结果返回为 Series | +| `sum` | 非 NA 值的总和 | +| `std, var` | 样本标准差和方差 | + +您可以使用自己设计的聚合,并额外调用任何也在被分组对象上定义的方法。例如,`nsmallest` Series 方法从数据中选择请求的最小数量的值。虽然`nsmallest`没有明确为 GroupBy 实现,但我们仍然可以使用它与非优化的实现。在内部,GroupBy 将 Series 切片,为每个片段调用`piece.nsmallest(n)`,然后将这些结果组装成结果对象: + +```py +In [58]: df +Out[58]: + key1 key2 data1 data2 +0 a 1 -0.204708 0.281746 +1 a 2 0.478943 0.769023 +2 None 1 -0.519439 1.246435 +3 b 2 -0.555730 1.007189 +4 b 1 1.965781 -1.296221 +5 a 1.393406 0.274992 +6 None 1 0.092908 0.228913 + +In [59]: grouped = df.groupby("key1") + +In [60]: grouped["data1"].nsmallest(2) +Out[60]: +key1 +a 0 -0.204708 + 1 0.478943 +b 3 -0.555730 + 4 1.965781 +Name: data1, dtype: float64 +``` + +要使用自己的聚合函数,只需将任何聚合数组的函数传递给`aggregate`方法或其简短别名`agg`: + +```py +In [61]: def peak_to_peak(arr): + ....: return arr.max() - arr.min() + +In [62]: grouped.agg(peak_to_peak) +Out[62]: + key2 data1 data2 +key1 +a 1 1.598113 0.494031 +b 1 2.521511 2.303410 +``` + +您可能会注意到一些方法,比如`describe`,即使严格来说它们不是聚合也可以工作: + +```py +In [63]: grouped.describe() +Out[63]: + key2 data1 ... + count mean std min 25% 50% 75% max count mean ... +key1 ... +a 2.0 1.5 0.707107 1.0 1.25 1.5 1.75 2.0 3.0 0.555881 ... \ +b 2.0 1.5 0.707107 1.0 1.25 1.5 1.75 2.0 2.0 0.705025 ... + data2 + 75% max count mean std min 25% +key1 +a 0.936175 1.393406 3.0 0.441920 0.283299 0.274992 0.278369 \ +b 1.335403 1.965781 2.0 -0.144516 1.628757 -1.296221 -0.720368 + + 50% 75% max +key1 +a 0.281746 0.525384 0.769023 +b -0.144516 0.431337 1.007189 +[2 rows x 24 columns] +``` + +我将在应用:通用的分割-应用-合并中更详细地解释这里发生了什么。 + +注意 + +自定义聚合函数通常比在 Table 10.1 中找到的优化函数慢得多。这是因为在构建中间组数据块时存在一些额外开销(函数调用,数据重新排列)*### 按列和多函数应用 + +让我们回到上一章中使用的小费数据集。在使用`pandas.read_csv`加载后,我们添加一个小费百分比列: + +```py +In [64]: tips = pd.read_csv("examples/tips.csv") + +In [65]: tips.head() +Out[65]: + total_bill tip smoker day time size +0 16.99 1.01 No Sun Dinner 2 +1 10.34 1.66 No Sun Dinner 3 +2 21.01 3.50 No Sun Dinner 3 +3 23.68 3.31 No Sun Dinner 2 +4 24.59 3.61 No Sun Dinner 4 +``` + +现在我将添加一个`tip_pct`列,其中包含总账单的小费百分比: + +```py +In [66]: tips["tip_pct"] = tips["tip"] / tips["total_bill"] + +In [67]: tips.head() +Out[67]: + total_bill tip smoker day time size tip_pct +0 16.99 1.01 No Sun Dinner 2 0.059447 +1 10.34 1.66 No Sun Dinner 3 0.160542 +2 21.01 3.50 No Sun Dinner 3 0.166587 +3 23.68 3.31 No Sun Dinner 2 0.139780 +4 24.59 3.61 No Sun Dinner 4 0.146808 +``` + +正如您已经看到的,聚合 Series 或 DataFrame 的所有列是使用`aggregate`(或`agg`)与所需函数或调用`mean`或`std`方法的问题。但是,您可能希望根据列使用不同的函数进行聚合,或者一次使用多个函数。幸运的是,这是可能的,我将通过一些示例来说明。首先,我将按`day`和`smoker`对`tips`进行分组: + +```py +In [68]: grouped = tips.groupby(["day", "smoker"]) +``` + +请注意,对于像 Table 10.1 中的描述性统计数据,您可以将函数的名称作为字符串传递: + +```py +In [69]: grouped_pct = grouped["tip_pct"] + +In [70]: grouped_pct.agg("mean") +Out[70]: +day smoker +Fri No 0.151650 + Yes 0.174783 +Sat No 0.158048 + Yes 0.147906 +Sun No 0.160113 + Yes 0.187250 +Thur No 0.160298 + Yes 0.163863 +Name: tip_pct, dtype: float64 +``` + +如果您传递的是函数或函数名称的列表,您将获得一个列名从函数中获取的 DataFrame: + +```py +In [71]: grouped_pct.agg(["mean", "std", peak_to_peak]) +Out[71]: + mean std peak_to_peak +day smoker +Fri No 0.151650 0.028123 0.067349 + Yes 0.174783 0.051293 0.159925 +Sat No 0.158048 0.039767 0.235193 + Yes 0.147906 0.061375 0.290095 +Sun No 0.160113 0.042347 0.193226 + Yes 0.187250 0.154134 0.644685 +Thur No 0.160298 0.038774 0.193350 + Yes 0.163863 0.039389 0.151240 +``` + +在这里,我们将一系列聚合函数传递给`agg`,以独立评估数据组。 + +您不需要接受 GroupBy 为列提供的名称;特别是,`lambda`函数的名称为`""`,这使得它们难以识别(您可以通过查看函数的`__name__`属性来自行查看)。因此,如果您传递一个`(name, function)`元组的列表,每个元组的第一个元素将被用作 DataFrame 列名(您可以将 2 元组的列表视为有序映射): + +```py +In [72]: grouped_pct.agg([("average", "mean"), ("stdev", np.std)]) +Out[72]: + average stdev +day smoker +Fri No 0.151650 0.028123 + Yes 0.174783 0.051293 +Sat No 0.158048 0.039767 + Yes 0.147906 0.061375 +Sun No 0.160113 0.042347 + Yes 0.187250 0.154134 +Thur No 0.160298 0.038774 + Yes 0.163863 0.039389 +``` + +使用 DataFrame,您有更多的选项,因为您可以指定要应用于所有列或不同列的不同函数的函数列表。首先,假设我们想要计算`tip_pct`和`total_bill`列的相同三个统计数据: + +```py +In [73]: functions = ["count", "mean", "max"] + +In [74]: result = grouped[["tip_pct", "total_bill"]].agg(functions) + +In [75]: result +Out[75]: + tip_pct total_bill + count mean max count mean max +day smoker +Fri No 4 0.151650 0.187735 4 18.420000 22.75 + Yes 15 0.174783 0.263480 15 16.813333 40.17 +Sat No 45 0.158048 0.291990 45 19.661778 48.33 + Yes 42 0.147906 0.325733 42 21.276667 50.81 +Sun No 57 0.160113 0.252672 57 20.506667 48.17 + Yes 19 0.187250 0.710345 19 24.120000 45.35 +Thur No 45 0.160298 0.266312 45 17.113111 41.19 + Yes 17 0.163863 0.241255 17 19.190588 43.11 +``` + +如您所见,生成的 DataFrame 具有分层列,与分别聚合每列并使用列名作为`keys`参数使用`concat`粘合结果时获得的结果相同: + +```py +In [76]: result["tip_pct"] +Out[76]: + count mean max +day smoker +Fri No 4 0.151650 0.187735 + Yes 15 0.174783 0.263480 +Sat No 45 0.158048 0.291990 + Yes 42 0.147906 0.325733 +Sun No 57 0.160113 0.252672 + Yes 19 0.187250 0.710345 +Thur No 45 0.160298 0.266312 + Yes 17 0.163863 0.241255 +``` + +与以前一样,可以传递具有自定义名称的元组列表: + +```py +In [77]: ftuples = [("Average", "mean"), ("Variance", np.var)] + +In [78]: grouped[["tip_pct", "total_bill"]].agg(ftuples) +Out[78]: + tip_pct total_bill + Average Variance Average Variance +day smoker +Fri No 0.151650 0.000791 18.420000 25.596333 + Yes 0.174783 0.002631 16.813333 82.562438 +Sat No 0.158048 0.001581 19.661778 79.908965 + Yes 0.147906 0.003767 21.276667 101.387535 +Sun No 0.160113 0.001793 20.506667 66.099980 + Yes 0.187250 0.023757 24.120000 109.046044 +Thur No 0.160298 0.001503 17.113111 59.625081 + Yes 0.163863 0.001551 19.190588 69.808518 +``` + +现在,假设您想要对一个或多个列应用可能不同的函数。为此,请将包含列名到迄今为止列出的任何函数规范的映射的字典传递给`agg`: + +```py +In [79]: grouped.agg({"tip" : np.max, "size" : "sum"}) +Out[79]: + tip size +day smoker +Fri No 3.50 9 + Yes 4.73 31 +Sat No 9.00 115 + Yes 10.00 104 +Sun No 6.00 167 + Yes 6.50 49 +Thur No 6.70 112 + Yes 5.00 40 + +In [80]: grouped.agg({"tip_pct" : ["min", "max", "mean", "std"], + ....: "size" : "sum"}) +Out[80]: + tip_pct size + min max mean std sum +day smoker +Fri No 0.120385 0.187735 0.151650 0.028123 9 + Yes 0.103555 0.263480 0.174783 0.051293 31 +Sat No 0.056797 0.291990 0.158048 0.039767 115 + Yes 0.035638 0.325733 0.147906 0.061375 104 +Sun No 0.059447 0.252672 0.160113 0.042347 167 + Yes 0.065660 0.710345 0.187250 0.154134 49 +Thur No 0.072961 0.266312 0.160298 0.038774 112 + Yes 0.090014 0.241255 0.163863 0.039389 40 +``` + +只有在至少对一列应用多个函数时,DataFrame 才会具有分层列。 + +### 返回不带行索引的聚合数据 + +到目前为止的所有示例中,聚合数据都带有一个索引,可能是分层的,由唯一的组键组合组成。由于这并不总是理想的,您可以通过在大多数情况下将`as_index=False`传递给`groupby`来禁用此行为: + +```py +In [81]: grouped = tips.groupby(["day", "smoker"], as_index=False) + +In [82]: grouped.mean(numeric_only=True) +Out[82]: + day smoker total_bill tip size tip_pct +0 Fri No 18.420000 2.812500 2.250000 0.151650 +1 Fri Yes 16.813333 2.714000 2.066667 0.174783 +2 Sat No 19.661778 3.102889 2.555556 0.158048 +3 Sat Yes 21.276667 2.875476 2.476190 0.147906 +4 Sun No 20.506667 3.167895 2.929825 0.160113 +5 Sun Yes 24.120000 3.516842 2.578947 0.187250 +6 Thur No 17.113111 2.673778 2.488889 0.160298 +7 Thur Yes 19.190588 3.030000 2.352941 0.163863 +``` + +当然,通过在结果上调用`reset_index`,总是可以以这种格式获得结果。使用`as_index=False`参数可以避免一些不必要的计算。*## 10.3 应用:通用的分割-应用-合并 + +最通用的 GroupBy 方法是`apply`,这是本节的主题。`apply`将被操作的对象分割成片段,对每个片段调用传递的函数,然后尝试连接这些片段。 + +回到以前的小费数据集,假设您想要按组选择前五个`tip_pct`值。首先,编写一个函数,该函数选择特定列中最大值的行: + +```py +In [83]: def top(df, n=5, column="tip_pct"): + ....: return df.sort_values(column, ascending=False)[:n] + +In [84]: top(tips, n=6) +Out[84]: + total_bill tip smoker day time size tip_pct +172 7.25 5.15 Yes Sun Dinner 2 0.710345 +178 9.60 4.00 Yes Sun Dinner 2 0.416667 +67 3.07 1.00 Yes Sat Dinner 1 0.325733 +232 11.61 3.39 No Sat Dinner 2 0.291990 +183 23.17 6.50 Yes Sun Dinner 4 0.280535 +109 14.31 4.00 Yes Sat Dinner 2 0.279525 +``` + +现在,如果我们按`smoker`分组,并使用此函数调用`apply`,我们将得到以下结果: + +```py +In [85]: tips.groupby("smoker").apply(top) +Out[85]: + total_bill tip smoker day time size tip_pct +smoker +No 232 11.61 3.39 No Sat Dinner 2 0.291990 + 149 7.51 2.00 No Thur Lunch 2 0.266312 + 51 10.29 2.60 No Sun Dinner 2 0.252672 + 185 20.69 5.00 No Sun Dinner 5 0.241663 + 88 24.71 5.85 No Thur Lunch 2 0.236746 +Yes 172 7.25 5.15 Yes Sun Dinner 2 0.710345 + 178 9.60 4.00 Yes Sun Dinner 2 0.416667 + 67 3.07 1.00 Yes Sat Dinner 1 0.325733 + 183 23.17 6.50 Yes Sun Dinner 4 0.280535 + 109 14.31 4.00 Yes Sat Dinner 2 0.279525 +``` + +这里发生了什么?首先,根据`smoker`的值将`tips` DataFrame 分成组。然后在每个组上调用`top`函数,并使用`pandas.concat`将每个函数调用的结果粘合在一起,用组名标记各个部分。因此,结果具有一个具有内部级别的分层索引,该级别包含原始 DataFrame 的索引值。 + +如果您将一个接受其他参数或关键字的函数传递给`apply`,则可以在函数之后传递这些参数: + +```py +In [86]: tips.groupby(["smoker", "day"]).apply(top, n=1, column="total_bill") +Out[86]: + total_bill tip smoker day time size tip_pct +smoker day +No Fri 94 22.75 3.25 No Fri Dinner 2 0.142857 + Sat 212 48.33 9.00 No Sat Dinner 4 0.186220 + Sun 156 48.17 5.00 No Sun Dinner 6 0.103799 + Thur 142 41.19 5.00 No Thur Lunch 5 0.121389 +Yes Fri 95 40.17 4.73 Yes Fri Dinner 4 0.117750 + Sat 170 50.81 10.00 Yes Sat Dinner 3 0.196812 + Sun 182 45.35 3.50 Yes Sun Dinner 3 0.077178 + Thur 197 43.11 5.00 Yes Thur Lunch 4 0.115982 +``` + +除了这些基本的使用机制外,要充分利用`apply`可能需要一些创造力。传递的函数内部发生的事情取决于你;它必须返回一个 pandas 对象或一个标量值。本章的其余部分主要将包含示例,向您展示如何使用`groupby`解决各种问题。 + +例如,你可能还记得我之前在 GroupBy 对象上调用`describe`: + +```py +In [87]: result = tips.groupby("smoker")["tip_pct"].describe() + +In [88]: result +Out[88]: + count mean std min 25% 50% 75% +smoker +No 151.0 0.159328 0.039910 0.056797 0.136906 0.155625 0.185014 \ +Yes 93.0 0.163196 0.085119 0.035638 0.106771 0.153846 0.195059 + max +smoker +No 0.291990 +Yes 0.710345 + +In [89]: result.unstack("smoker") +Out[89]: + smoker +count No 151.000000 + Yes 93.000000 +mean No 0.159328 + Yes 0.163196 +std No 0.039910 + Yes 0.085119 +min No 0.056797 + Yes 0.035638 +25% No 0.136906 + Yes 0.106771 +50% No 0.155625 + Yes 0.153846 +75% No 0.185014 + Yes 0.195059 +max No 0.291990 + Yes 0.710345 +dtype: float64 +``` + +在 GroupBy 中,当你调用像`describe`这样的方法时,实际上只是一个快捷方式: + +```py +def f(group): + return group.describe() + +grouped.apply(f) +``` + +### 抑制组键 + +在前面的示例中,您可以看到生成的对象具有从组键形成的分层索引,以及原始对象的每个部分的索引。您可以通过将`group_keys=False`传递给`groupby`来禁用这一点: + +```py +In [90]: tips.groupby("smoker", group_keys=False).apply(top) +Out[90]: + total_bill tip smoker day time size tip_pct +232 11.61 3.39 No Sat Dinner 2 0.291990 +149 7.51 2.00 No Thur Lunch 2 0.266312 +51 10.29 2.60 No Sun Dinner 2 0.252672 +185 20.69 5.00 No Sun Dinner 5 0.241663 +88 24.71 5.85 No Thur Lunch 2 0.236746 +172 7.25 5.15 Yes Sun Dinner 2 0.710345 +178 9.60 4.00 Yes Sun Dinner 2 0.416667 +67 3.07 1.00 Yes Sat Dinner 1 0.325733 +183 23.17 6.50 Yes Sun Dinner 4 0.280535 +109 14.31 4.00 Yes Sat Dinner 2 0.279525 +``` + +### 分位数和桶分析 + +正如你可能从第八章:数据整理:连接、合并和重塑中记得的那样,pandas 有一些工具,特别是`pandas.cut`和`pandas.qcut`,可以将数据切分成您选择的桶或样本分位数。将这些函数与`groupby`结合起来,可以方便地对数据集进行桶或分位数分析。考虑一个简单的随机数据集和使用`pandas.cut`进行等长度桶分类: + +```py +In [91]: frame = pd.DataFrame({"data1": np.random.standard_normal(1000), + ....: "data2": np.random.standard_normal(1000)}) + +In [92]: frame.head() +Out[92]: + data1 data2 +0 -0.660524 -0.612905 +1 0.862580 0.316447 +2 -0.010032 0.838295 +3 0.050009 -1.034423 +4 0.670216 0.434304 + +In [93]: quartiles = pd.cut(frame["data1"], 4) + +In [94]: quartiles.head(10) +Out[94]: +0 (-1.23, 0.489] +1 (0.489, 2.208] +2 (-1.23, 0.489] +3 (-1.23, 0.489] +4 (0.489, 2.208] +5 (0.489, 2.208] +6 (-1.23, 0.489] +7 (-1.23, 0.489] +8 (-2.956, -1.23] +9 (-1.23, 0.489] +Name: data1, dtype: category +Categories (4, interval[float64, right]): [(-2.956, -1.23] < (-1.23, 0.489] < (0. +489, 2.208] < + (2.208, 3.928]] +``` + +`cut`返回的`Categorical`对象可以直接传递给`groupby`。因此,我们可以计算四分位数的一组组统计信息,如下所示: + +```py +In [95]: def get_stats(group): + ....: return pd.DataFrame( + ....: {"min": group.min(), "max": group.max(), + ....: "count": group.count(), "mean": group.mean()} + ....: ) + +In [96]: grouped = frame.groupby(quartiles) + +In [97]: grouped.apply(get_stats) +Out[97]: + min max count mean +data1 +(-2.956, -1.23] data1 -2.949343 -1.230179 94 -1.658818 + data2 -3.399312 1.670835 94 -0.033333 +(-1.23, 0.489] data1 -1.228918 0.488675 598 -0.329524 + data2 -2.989741 3.260383 598 -0.002622 +(0.489, 2.208] data1 0.489965 2.200997 298 1.065727 + data2 -3.745356 2.954439 298 0.078249 +(2.208, 3.928] data1 2.212303 3.927528 10 2.644253 + data2 -1.929776 1.765640 10 0.024750 +``` + +请记住,同样的结果可以更简单地计算为: + +```py +In [98]: grouped.agg(["min", "max", "count", "mean"]) +Out[98]: + data1 data2 + min max count mean min max count +data1 +(-2.956, -1.23] -2.949343 -1.230179 94 -1.658818 -3.399312 1.670835 94 \ +(-1.23, 0.489] -1.228918 0.488675 598 -0.329524 -2.989741 3.260383 598 +(0.489, 2.208] 0.489965 2.200997 298 1.065727 -3.745356 2.954439 298 +(2.208, 3.928] 2.212303 3.927528 10 2.644253 -1.929776 1.765640 10 + + mean +data1 +(-2.956, -1.23] -0.033333 +(-1.23, 0.489] -0.002622 +(0.489, 2.208] 0.078249 +(2.208, 3.928] 0.024750 +``` + +这些是等长度的桶;要基于样本分位数计算等大小的桶,使用`pandas.qcut`。我们可以将`4`作为桶的数量计算样本四分位数,并传递`labels=False`以仅获取四分位数索引而不是间隔: + +```py +In [99]: quartiles_samp = pd.qcut(frame["data1"], 4, labels=False) + +In [100]: quartiles_samp.head() +Out[100]: +0 1 +1 3 +2 2 +3 2 +4 3 +Name: data1, dtype: int64 + +In [101]: grouped = frame.groupby(quartiles_samp) + +In [102]: grouped.apply(get_stats) +Out[102]: + min max count mean +data1 +0 data1 -2.949343 -0.685484 250 -1.212173 + data2 -3.399312 2.628441 250 -0.027045 +1 data1 -0.683066 -0.030280 250 -0.368334 + data2 -2.630247 3.260383 250 -0.027845 +2 data1 -0.027734 0.618965 250 0.295812 + data2 -3.056990 2.458842 250 0.014450 +3 data1 0.623587 3.927528 250 1.248875 + data2 -3.745356 2.954439 250 0.115899 +``` + +### 示例:使用组特定值填充缺失值 + +在清理缺失数据时,有些情况下您将使用`dropna`删除数据观察值,但在其他情况下,您可能希望使用固定值或从数据中派生的某个值填充空(NA)值。`fillna`是正确的工具;例如,这里我用均值填充了空值: + +```py +In [103]: s = pd.Series(np.random.standard_normal(6)) + +In [104]: s[::2] = np.nan + +In [105]: s +Out[105]: +0 NaN +1 0.227290 +2 NaN +3 -2.153545 +4 NaN +5 -0.375842 +dtype: float64 + +In [106]: s.fillna(s.mean()) +Out[106]: +0 -0.767366 +1 0.227290 +2 -0.767366 +3 -2.153545 +4 -0.767366 +5 -0.375842 +dtype: float64 +``` + +假设您需要填充值根据组而变化。一种方法是对数据进行分组,并使用调用`fillna`的函数在每个数据块上使用`apply`。这里是一些关于美国各州的样本数据,分为东部和西部地区: + +```py +In [107]: states = ["Ohio", "New York", "Vermont", "Florida", + .....: "Oregon", "Nevada", "California", "Idaho"] + +In [108]: group_key = ["East", "East", "East", "East", + .....: "West", "West", "West", "West"] + +In [109]: data = pd.Series(np.random.standard_normal(8), index=states) + +In [110]: data +Out[110]: +Ohio 0.329939 +New York 0.981994 +Vermont 1.105913 +Florida -1.613716 +Oregon 1.561587 +Nevada 0.406510 +California 0.359244 +Idaho -0.614436 +dtype: float64 +``` + +让我们将数据中的一些值设置为缺失: + +```py +In [111]: data[["Vermont", "Nevada", "Idaho"]] = np.nan + +In [112]: data +Out[112]: +Ohio 0.329939 +New York 0.981994 +Vermont NaN +Florida -1.613716 +Oregon 1.561587 +Nevada NaN +California 0.359244 +Idaho NaN +dtype: float64 + +In [113]: data.groupby(group_key).size() +Out[113]: +East 4 +West 4 +dtype: int64 + +In [114]: data.groupby(group_key).count() +Out[114]: +East 3 +West 2 +dtype: int64 + +In [115]: data.groupby(group_key).mean() +Out[115]: +East -0.100594 +West 0.960416 +dtype: float64 +``` + +我们可以使用组均值填充 NA 值,如下所示: + +```py +In [116]: def fill_mean(group): + .....: return group.fillna(group.mean()) + +In [117]: data.groupby(group_key).apply(fill_mean) +Out[117]: +East Ohio 0.329939 + New York 0.981994 + Vermont -0.100594 + Florida -1.613716 +West Oregon 1.561587 + Nevada 0.960416 + California 0.359244 + Idaho 0.960416 +dtype: float64 +``` + +在另一种情况下,您可能在代码中预定义了根据组变化的填充值。由于组内部设置了`name`属性,我们可以使用它: + +```py +In [118]: fill_values = {"East": 0.5, "West": -1} + +In [119]: def fill_func(group): + .....: return group.fillna(fill_values[group.name]) + +In [120]: data.groupby(group_key).apply(fill_func) +Out[120]: +East Ohio 0.329939 + New York 0.981994 + Vermont 0.500000 + Florida -1.613716 +West Oregon 1.561587 + Nevada -1.000000 + California 0.359244 + Idaho -1.000000 +dtype: float64 +``` + +### 示例:随机抽样和排列 + +假设您想要从大型数据集中随机抽取(有或没有替换)用于蒙特卡洛模拟或其他应用。有许多执行“抽取”的方法;在这里,我们使用 Series 的`sample`方法。 + +为了演示,这里有一种构建一副英式扑克牌的方法: + +```py +suits = ["H", "S", "C", "D"] # Hearts, Spades, Clubs, Diamonds +card_val = (list(range(1, 11)) + [10] * 3) * 4 +base_names = ["A"] + list(range(2, 11)) + ["J", "K", "Q"] +cards = [] +for suit in suits: + cards.extend(str(num) + suit for num in base_names) + +deck = pd.Series(card_val, index=cards) +``` + +现在我们有一个长度为 52 的 Series,其索引包含牌名,值是在二十一点和其他游戏中使用的值(为了简单起见,我让 ace `"A"`为 1): + +```py +In [122]: deck.head(13) +Out[122]: +AH 1 +2H 2 +3H 3 +4H 4 +5H 5 +6H 6 +7H 7 +8H 8 +9H 9 +10H 10 +JH 10 +KH 10 +QH 10 +dtype: int64 +``` + +现在,根据我之前说的,从牌组中抽取五张牌可以写成: + +```py +In [123]: def draw(deck, n=5): + .....: return deck.sample(n) + +In [124]: draw(deck) +Out[124]: +4D 4 +QH 10 +8S 8 +7D 7 +9C 9 +dtype: int64 +``` + +假设你想要从每种花色中抽取两张随机牌。因为花色是每张牌名称的最后一个字符,我们可以根据这个进行分组,并使用`apply`: + +```py +In [125]: def get_suit(card): + .....: # last letter is suit + .....: return card[-1] + +In [126]: deck.groupby(get_suit).apply(draw, n=2) +Out[126]: +C 6C 6 + KC 10 +D 7D 7 + 3D 3 +H 7H 7 + 9H 9 +S 2S 2 + QS 10 +dtype: int64 +``` + +或者,我们可以传递`group_keys=False`以删除外部套索索引,只留下所选的卡: + +```py +In [127]: deck.groupby(get_suit, group_keys=False).apply(draw, n=2) +Out[127]: +AC 1 +3C 3 +5D 5 +4D 4 +10H 10 +7H 7 +QS 10 +7S 7 +dtype: int64 +``` + +### 示例:组加权平均和相关性 + +在`groupby`的分割-应用-组合范式下,DataFrame 或两个 Series 中的列之间的操作,例如组加权平均,是可能的。例如,考虑包含组键、值和一些权重的数据集: + +```py +In [128]: df = pd.DataFrame({"category": ["a", "a", "a", "a", + .....: "b", "b", "b", "b"], + .....: "data": np.random.standard_normal(8), + .....: "weights": np.random.uniform(size=8)}) + +In [129]: df +Out[129]: + category data weights +0 a -1.691656 0.955905 +1 a 0.511622 0.012745 +2 a -0.401675 0.137009 +3 a 0.968578 0.763037 +4 b -1.818215 0.492472 +5 b 0.279963 0.832908 +6 b -0.200819 0.658331 +7 b -0.217221 0.612009 +``` + +按`category`加权平均值将是: + +```py +In [130]: grouped = df.groupby("category") + +In [131]: def get_wavg(group): + .....: return np.average(group["data"], weights=group["weights"]) + +In [132]: grouped.apply(get_wavg) +Out[132]: +category +a -0.495807 +b -0.357273 +dtype: float64 +``` + +另一个例子是,考虑一个最初从 Yahoo! Finance 获取的金融数据集,其中包含一些股票的日终价格和标准普尔 500 指数(`SPX`符号): + +```py +In [133]: close_px = pd.read_csv("examples/stock_px.csv", parse_dates=True, + .....: index_col=0) + +In [134]: close_px.info() + +DatetimeIndex: 2214 entries, 2003-01-02 to 2011-10-14 +Data columns (total 4 columns): + # Column Non-Null Count Dtype +--- ------ -------------- ----- + 0 AAPL 2214 non-null float64 + 1 MSFT 2214 non-null float64 + 2 XOM 2214 non-null float64 + 3 SPX 2214 non-null float64 +dtypes: float64(4) +memory usage: 86.5 KB + +In [135]: close_px.tail(4) +Out[135]: + AAPL MSFT XOM SPX +2011-10-11 400.29 27.00 76.27 1195.54 +2011-10-12 402.19 26.96 77.16 1207.25 +2011-10-13 408.43 27.18 76.37 1203.66 +2011-10-14 422.00 27.27 78.11 1224.58 +``` + +这里的 DataFrame `info()`方法是获取 DataFrame 内容概述的便捷方式。 + +一个感兴趣的任务可能是计算一个由每日收益(从百分比变化计算)与`SPX`的年度相关性组成的 DataFrame。作为一种方法,我们首先创建一个函数,计算每列与`"SPX"`列的成对相关性: + +```py +In [136]: def spx_corr(group): + .....: return group.corrwith(group["SPX"]) +``` + +接下来,我们使用`pct_change`计算`close_px`的百分比变化: + +```py +In [137]: rets = close_px.pct_change().dropna() +``` + +最后,我们按年将这些百分比变化分组,可以使用一个一行函数从每个行标签中提取`datetime`标签的`year`属性: + +```py +In [138]: def get_year(x): + .....: return x.year + +In [139]: by_year = rets.groupby(get_year) + +In [140]: by_year.apply(spx_corr) +Out[140]: + AAPL MSFT XOM SPX +2003 0.541124 0.745174 0.661265 1.0 +2004 0.374283 0.588531 0.557742 1.0 +2005 0.467540 0.562374 0.631010 1.0 +2006 0.428267 0.406126 0.518514 1.0 +2007 0.508118 0.658770 0.786264 1.0 +2008 0.681434 0.804626 0.828303 1.0 +2009 0.707103 0.654902 0.797921 1.0 +2010 0.710105 0.730118 0.839057 1.0 +2011 0.691931 0.800996 0.859975 1.0 +``` + +您还可以计算列间的相关性。这里我们计算苹果和微软之间的年度相关性: + +```py +In [141]: def corr_aapl_msft(group): + .....: return group["AAPL"].corr(group["MSFT"]) + +In [142]: by_year.apply(corr_aapl_msft) +Out[142]: +2003 0.480868 +2004 0.259024 +2005 0.300093 +2006 0.161735 +2007 0.417738 +2008 0.611901 +2009 0.432738 +2010 0.571946 +2011 0.581987 +dtype: float64 +``` + +### 示例:组内线性回归 + +与前面的示例相同,您可以使用`groupby`执行更复杂的组内统计分析,只要函数返回一个 pandas 对象或标量值。例如,我可以定义以下`regress`函数(使用`statsmodels`计量经济学库),它在每个数据块上执行普通最小二乘(OLS)回归: + +```py +import statsmodels.api as sm +def regress(data, yvar=None, xvars=None): + Y = data[yvar] + X = data[xvars] + X["intercept"] = 1. + result = sm.OLS(Y, X).fit() + return result.params +``` + +如果您尚未安装`statsmodels`,可以使用 conda 安装它: + +```py +conda install statsmodels +``` + +现在,要在`AAPL`对`SPX`回报的年度线性回归中执行: + +```py +In [144]: by_year.apply(regress, yvar="AAPL", xvars=["SPX"]) +Out[144]: + SPX intercept +2003 1.195406 0.000710 +2004 1.363463 0.004201 +2005 1.766415 0.003246 +2006 1.645496 0.000080 +2007 1.198761 0.003438 +2008 0.968016 -0.001110 +2009 0.879103 0.002954 +2010 1.052608 0.001261 +2011 0.806605 0.001514 +``` + +## 10.4 组转换和“展开”的 GroupBys + +在 Apply: General split-apply-combine 中,我们看了一下在分组操作中执行转换的`apply`方法。还有另一个内置方法叫做`transform`,它类似于`apply`,但对您可以使用的函数种类施加了更多的约束: + ++ 它可以生成一个标量值广播到组的形状。 + ++ 它可以生成与输入组相同形状的对象。 + ++ 它不能改变其输入。 + +让我们考虑一个简单的例子以说明: + +```py +In [145]: df = pd.DataFrame({'key': ['a', 'b', 'c'] * 4, + .....: 'value': np.arange(12.)}) + +In [146]: df +Out[146]: + key value +0 a 0.0 +1 b 1.0 +2 c 2.0 +3 a 3.0 +4 b 4.0 +5 c 5.0 +6 a 6.0 +7 b 7.0 +8 c 8.0 +9 a 9.0 +10 b 10.0 +11 c 11.0 +``` + +这里是按键的组平均值: + +```py +In [147]: g = df.groupby('key')['value'] + +In [148]: g.mean() +Out[148]: +key +a 4.5 +b 5.5 +c 6.5 +Name: value, dtype: float64 +``` + +假设我们想要生成一个与`df['value']`相同形状的 Series,但值被按`'key'`分组后的平均值替换。我们可以传递一个计算单个组平均值的函数给`transform`: + +```py +In [149]: def get_mean(group): + .....: return group.mean() + +In [150]: g.transform(get_mean) +Out[150]: +0 4.5 +1 5.5 +2 6.5 +3 4.5 +4 5.5 +5 6.5 +6 4.5 +7 5.5 +8 6.5 +9 4.5 +10 5.5 +11 6.5 +Name: value, dtype: float64 +``` + +对于内置的聚合函数,我们可以像 GroupBy `agg`方法一样传递一个字符串别名: + +```py +In [151]: g.transform('mean') +Out[151]: +0 4.5 +1 5.5 +2 6.5 +3 4.5 +4 5.5 +5 6.5 +6 4.5 +7 5.5 +8 6.5 +9 4.5 +10 5.5 +11 6.5 +Name: value, dtype: float64 +``` + +与`apply`一样,`transform`适用于返回 Series 的函数,但结果必须与输入的大小相同。例如,我们可以使用一个辅助函数将每个组乘以 2: + +```py +In [152]: def times_two(group): + .....: return group * 2 + +In [153]: g.transform(times_two) +Out[153]: +0 0.0 +1 2.0 +2 4.0 +3 6.0 +4 8.0 +5 10.0 +6 12.0 +7 14.0 +8 16.0 +9 18.0 +10 20.0 +11 22.0 +Name: value, dtype: float64 +``` + +作为一个更复杂的例子,我们可以计算每个组按降序排名: + +```py +In [154]: def get_ranks(group): + .....: return group.rank(ascending=False) + +In [155]: g.transform(get_ranks) +Out[155]: +0 4.0 +1 4.0 +2 4.0 +3 3.0 +4 3.0 +5 3.0 +6 2.0 +7 2.0 +8 2.0 +9 1.0 +10 1.0 +11 1.0 +Name: value, dtype: float64 +``` + +考虑一个由简单聚合组成的组转换函数: + +```py +In [156]: def normalize(x): + .....: return (x - x.mean()) / x.std() +``` + +在这种情况下,我们可以使用`transform`或`apply`获得等效的结果: + +```py +In [157]: g.transform(normalize) +Out[157]: +0 -1.161895 +1 -1.161895 +2 -1.161895 +3 -0.387298 +4 -0.387298 +5 -0.387298 +6 0.387298 +7 0.387298 +8 0.387298 +9 1.161895 +10 1.161895 +11 1.161895 +Name: value, dtype: float64 + +In [158]: g.apply(normalize) +Out[158]: +key +a 0 -1.161895 + 3 -0.387298 + 6 0.387298 + 9 1.161895 +b 1 -1.161895 + 4 -0.387298 + 7 0.387298 + 10 1.161895 +c 2 -1.161895 + 5 -0.387298 + 8 0.387298 + 11 1.161895 +Name: value, dtype: float64 +``` + +内置的聚合函数如`'mean'`或`'sum'`通常比一般的`apply`函数快得多。当与`transform`一起使用时,这些函数也有一个“快速路径”。这使我们能够执行所谓的*展开*组操作: + +```py +In [159]: g.transform('mean') +Out[159]: +0 4.5 +1 5.5 +2 6.5 +3 4.5 +4 5.5 +5 6.5 +6 4.5 +7 5.5 +8 6.5 +9 4.5 +10 5.5 +11 6.5 +Name: value, dtype: float64 + +In [160]: normalized = (df['value'] - g.transform('mean')) / g.transform('std') + +In [161]: normalized +Out[161]: +0 -1.161895 +1 -1.161895 +2 -1.161895 +3 -0.387298 +4 -0.387298 +5 -0.387298 +6 0.387298 +7 0.387298 +8 0.387298 +9 1.161895 +10 1.161895 +11 1.161895 +Name: value, dtype: float64 +``` + +在这里,我们在多个 GroupBy 操作的输出之间进行算术运算,而不是编写一个函数并将其传递给`groupby(...).apply`。这就是所谓的“展开”。 + +尽管展开的组操作可能涉及多个组聚合,但矢量化操作的整体效益通常超过了这一点。 + +## 10.5 透视表和交叉制表 + +*透视表*是一种经常在电子表格程序和其他数据分析软件中找到的数据汇总工具。它通过一个或多个键对数据表进行聚合,将数据排列在一个矩形中,其中一些组键沿行排列,另一些沿列排列。在 Python 中,通过本章描述的`groupby`功能以及利用分层索引进行重塑操作,可以实现使用 pandas 的透视表。DataFrame 还有一个`pivot_table`方法,还有一个顶级的`pandas.pivot_table`函数。除了提供一个方便的`groupby`接口外,`pivot_table`还可以添加部分总计,也称为*边际*。 + +返回到小费数据集,假设您想要计算按`day`和`smoker`排列的组平均值的表格(默认的`pivot_table`聚合类型): + +```py +In [162]: tips.head() +Out[162]: + total_bill tip smoker day time size tip_pct +0 16.99 1.01 No Sun Dinner 2 0.059447 +1 10.34 1.66 No Sun Dinner 3 0.160542 +2 21.01 3.50 No Sun Dinner 3 0.166587 +3 23.68 3.31 No Sun Dinner 2 0.139780 +4 24.59 3.61 No Sun Dinner 4 0.146808 + +In [163]: tips.pivot_table(index=["day", "smoker"], + .....: values=["size", "tip", "tip_pct", "total_bill"]) +Out[163]: + size tip tip_pct total_bill +day smoker +Fri No 2.250000 2.812500 0.151650 18.420000 + Yes 2.066667 2.714000 0.174783 16.813333 +Sat No 2.555556 3.102889 0.158048 19.661778 + Yes 2.476190 2.875476 0.147906 21.276667 +Sun No 2.929825 3.167895 0.160113 20.506667 + Yes 2.578947 3.516842 0.187250 24.120000 +Thur No 2.488889 2.673778 0.160298 17.113111 + Yes 2.352941 3.030000 0.163863 19.190588 +``` + +这可以直接使用`groupby`生成,使用`tips.groupby(["day", "smoker"]).mean()`。现在,假设我们只想计算`tip_pct`和`size`的平均值,并另外按`time`分组。我将`smoker`放在表格列中,`time`和`day`放在行中: + +```py +In [164]: tips.pivot_table(index=["time", "day"], columns="smoker", + .....: values=["tip_pct", "size"]) +Out[164]: + size tip_pct +smoker No Yes No Yes +time day +Dinner Fri 2.000000 2.222222 0.139622 0.165347 + Sat 2.555556 2.476190 0.158048 0.147906 + Sun 2.929825 2.578947 0.160113 0.187250 + Thur 2.000000 NaN 0.159744 NaN +Lunch Fri 3.000000 1.833333 0.187735 0.188937 + Thur 2.500000 2.352941 0.160311 0.163863 +``` + +我们可以通过传递`margins=True`来增加此表,以包括部分总计。这将添加`All`行和列标签,相应的值是单个层次内所有数据的组统计信息: + +```py +In [165]: tips.pivot_table(index=["time", "day"], columns="smoker", + .....: values=["tip_pct", "size"], margins=True) +Out[165]: + size tip_pct +smoker No Yes All No Yes All +time day +Dinner Fri 2.000000 2.222222 2.166667 0.139622 0.165347 0.158916 + Sat 2.555556 2.476190 2.517241 0.158048 0.147906 0.153152 + Sun 2.929825 2.578947 2.842105 0.160113 0.187250 0.166897 + Thur 2.000000 NaN 2.000000 0.159744 NaN 0.159744 +Lunch Fri 3.000000 1.833333 2.000000 0.187735 0.188937 0.188765 + Thur 2.500000 2.352941 2.459016 0.160311 0.163863 0.161301 +All 2.668874 2.408602 2.569672 0.159328 0.163196 0.160803 +``` + +这里,`All`值是没有考虑吸烟者与非吸烟者(`All`列)或行中的两个级别分组的平均值(`All`行)。 + +要使用除`mean`之外的聚合函数,请将其传递给`aggfunc`关键字参数。例如,`"count"`或`len`将为您提供组大小的交叉制表(计数或频率)(尽管`"count"`将在数据组内排除空值的计数,而`len`不会): + +```py +In [166]: tips.pivot_table(index=["time", "smoker"], columns="day", + .....: values="tip_pct", aggfunc=len, margins=True) +Out[166]: +day Fri Sat Sun Thur All +time smoker +Dinner No 3.0 45.0 57.0 1.0 106 + Yes 9.0 42.0 19.0 NaN 70 +Lunch No 1.0 NaN NaN 44.0 45 + Yes 6.0 NaN NaN 17.0 23 +All 19.0 87.0 76.0 62.0 244 +``` + +如果某些组合为空(或其他 NA),您可能希望传递一个`fill_value`: + +```py +In [167]: tips.pivot_table(index=["time", "size", "smoker"], columns="day", + .....: values="tip_pct", fill_value=0) +Out[167]: +day Fri Sat Sun Thur +time size smoker +Dinner 1 No 0.000000 0.137931 0.000000 0.000000 + Yes 0.000000 0.325733 0.000000 0.000000 + 2 No 0.139622 0.162705 0.168859 0.159744 + Yes 0.171297 0.148668 0.207893 0.000000 + 3 No 0.000000 0.154661 0.152663 0.000000 +... ... ... ... ... +Lunch 3 Yes 0.000000 0.000000 0.000000 0.204952 + 4 No 0.000000 0.000000 0.000000 0.138919 + Yes 0.000000 0.000000 0.000000 0.155410 + 5 No 0.000000 0.000000 0.000000 0.121389 + 6 No 0.000000 0.000000 0.000000 0.173706 +[21 rows x 4 columns] +``` + +请参阅表 10.2 以获取`pivot_table`选项的摘要。 + +表 10.2:`pivot_table`选项 + +| 参数 | 描述 | +| --- | --- | +| `values` | 要聚合的列名;默认情况下,聚合所有数值列 | +| `index` | 要在生成的透视表的行上分组的列名或其他组键 | +| `columns` | 要在生成的透视表的列上分组的列名或其他组键 | +| `aggfunc` | 聚合函数或函数列表(默认为`"mean"`);可以是在`groupby`上下文中有效的任何函数 | +| `fill_value` | 替换结果表中的缺失值 | +| `dropna` | 如果为`True`,则不包括所有条目都为`NA`的列 | +| `margins` | 添加行/列小计和总计(默认为`False`) | +| `margins_name` | 在传递`margins=True`时用于边缘行/列标签的名称;默认为`"All"` | +| `observed` | 使用分类组键,如果为`True`,则仅显示键中的观察类别值,而不是所有类别 | + +### 交叉制表:交叉制表 + +*交叉制表*(或简称为*交叉制表*)是计算组频率的透视表的一种特殊情况。这里是一个例子: + +```py +In [168]: from io import StringIO + +In [169]: data = """Sample Nationality Handedness + .....: 1 USA Right-handed + .....: 2 Japan Left-handed + .....: 3 USA Right-handed + .....: 4 Japan Right-handed + .....: 5 Japan Left-handed + .....: 6 Japan Right-handed + .....: 7 USA Right-handed + .....: 8 USA Left-handed + .....: 9 Japan Right-handed + .....: 10 USA Right-handed""" + .....: + +In [170]: data = pd.read_table(StringIO(data), sep="\s+") +``` + +```py +In [171]: data +Out[171]: + Sample Nationality Handedness +0 1 USA Right-handed +1 2 Japan Left-handed +2 3 USA Right-handed +3 4 Japan Right-handed +4 5 Japan Left-handed +5 6 Japan Right-handed +6 7 USA Right-handed +7 8 USA Left-handed +8 9 Japan Right-handed +9 10 USA Right-handed +``` + +作为一些调查分析的一部分,我们可能希望按国籍和惯用手总结这些数据。您可以使用`pivot_table`来做到这一点,但`pandas.crosstab`函数可能更方便: + +```py +In [172]: pd.crosstab(data["Nationality"], data["Handedness"], margins=True) +Out[172]: +Handedness Left-handed Right-handed All +Nationality +Japan 2 3 5 +USA 1 4 5 +All 3 7 10 +``` + +`crosstab`的前两个参数可以是数组、Series 或数组列表。就像在小费数据中一样: + +```py +In [173]: pd.crosstab([tips["time"], tips["day"]], tips["smoker"], margins=True) +Out[173]: +smoker No Yes All +time day +Dinner Fri 3 9 12 + Sat 45 42 87 + Sun 57 19 76 + Thur 1 0 1 +Lunch Fri 1 6 7 + Thur 44 17 61 +All 151 93 244 +``` + +## 10.6 结论 + +掌握 pandas 的数据分组工具可以帮助数据清洗和建模或统计分析工作。在 Ch 13:数据分析示例中,我们将查看几个更多实际数据上使用`groupby`的示例用例。 + +在下一章中,我们将把注意力转向时间序列数据。 diff --git a/translations/cn/pyda3e_14.md b/translations/cn/pyda3e_14.md new file mode 100644 index 000000000..a59532f0b --- /dev/null +++ b/translations/cn/pyda3e_14.md @@ -0,0 +1,2124 @@ +# 十一、时间序列 + +> 原文:[`wesmckinney.com/book/time-series`](https://wesmckinney.com/book/time-series) +> +> 译者:[飞龙](https://github.com/wizardforcel) +> +> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) + + +> 此开放访问网络版本的《Python 数据分析第三版》现已作为[印刷版和数字版](https://amzn.to/3DyLaJc)的伴侣提供。如果您发现任何勘误,请[在此处报告](https://oreilly.com/catalog/0636920519829/errata)。请注意,由 Quarto 生成的本站点的某些方面与 O'Reilly 的印刷版和电子书版本的格式不同。 +> +> 如果您发现本书的在线版本有用,请考虑[订购纸质版](https://amzn.to/3DyLaJc)或[无 DRM 的电子书](https://www.ebooks.com/en-us/book/210644288/python-for-data-analysis/wes-mckinney/?affId=WES398681F)以支持作者。本网站的内容不得复制或再生产。代码示例采用 MIT 许可,可在 GitHub 或 Gitee 上找到。 + +时间序列数据是许多不同领域中的结构化数据的重要形式,如金融、经济、生态学、神经科学和物理学。任何在许多时间点重复记录的东西都构成一个时间序列。许多时间序列是*固定频率*的,也就是说,数据点按照某种规则定期发生,例如每 15 秒、每 5 分钟或每月一次。时间序列也可以是*不规则*的,没有固定的时间单位或单位之间的偏移。如何标记和引用时间序列数据取决于应用程序,您可能有以下之一: + +时间戳 + +特定的时间点。 + +固定周期 + +例如 2017 年 1 月的整个月,或 2020 年的整年。 + +时间间隔 + +由开始和结束时间戳指示。周期可以被视为间隔的特殊情况。 + +实验或经过的时间 + +每个时间戳都是相对于特定开始时间的时间度量(例如,自放入烤箱以来每秒烘烤的饼干的直径),从 0 开始。 + +在本章中,我主要关注前三类时间序列,尽管许多技术也可以应用于实验时间序列,其中索引可能是整数或浮点数,表示从实验开始经过的时间。最简单的时间序列是由时间戳索引的。 + +提示: + +pandas 还支持基于时间差的索引,这是一种表示实验或经过时间的有用方式。我们在本书中没有探讨时间差索引,但您可以在[pandas 文档](https://pandas.pydata.org)中了解更多。 + +pandas 提供了许多内置的时间序列工具和算法。您可以高效地处理大型时间序列,对不规则和固定频率的时间序列进行切片、聚合和重采样。其中一些工具对金融和经济应用很有用,但您当然也可以用它们来分析服务器日志数据。 + +与其他章节一样,我们首先导入 NumPy 和 pandas: + +```py +In [12]: import numpy as np + +In [13]: import pandas as pd +``` + +## 11.1 日期和时间数据类型和工具 + +Python 标准库包括用于日期和时间数据以及与日历相关的功能的数据类型。`datetime`、`time`和`calendar`模块是主要的起点。`datetime.datetime`类型,或简称`datetime`,被广泛使用: + +```py +In [14]: from datetime import datetime + +In [15]: now = datetime.now() + +In [16]: now +Out[16]: datetime.datetime(2023, 4, 12, 13, 9, 16, 484533) + +In [17]: now.year, now.month, now.day +Out[17]: (2023, 4, 12) +``` + +`datetime` 存储日期和时间,精确到微秒。`datetime.timedelta`,或简称`timedelta`,表示两个`datetime`对象之间的时间差: + +```py +In [18]: delta = datetime(2011, 1, 7) - datetime(2008, 6, 24, 8, 15) + +In [19]: delta +Out[19]: datetime.timedelta(days=926, seconds=56700) + +In [20]: delta.days +Out[20]: 926 + +In [21]: delta.seconds +Out[21]: 56700 +``` + +您可以将`timedelta`或其倍数添加(或减去)到`datetime`对象中,以产生一个新的偏移对象: + +```py +In [22]: from datetime import timedelta + +In [23]: start = datetime(2011, 1, 7) + +In [24]: start + timedelta(12) +Out[24]: datetime.datetime(2011, 1, 19, 0, 0) + +In [25]: start - 2 * timedelta(12) +Out[25]: datetime.datetime(2010, 12, 14, 0, 0) +``` + +表 11.1 总结了`datetime`模块中的数据类型。虽然本章主要关注 pandas 中的数据类型和高级时间序列操作,但您可能会在 Python 的许多其他地方遇到基于`datetime`的类型。 + +表 11.1:`datetime`模块中的类型 + +| 类型 | 描述 | +| --- | --- | +| `date` | 使用公历存储日期(年,月,日) | +| `time` | 以小时,分钟,秒和微秒存储一天中的时间 | +| `datetime` | 存储日期和时间 | +| `timedelta` | 两个`datetime`值之间的差异(以天,秒和微秒计) | +| `tzinfo` | 存储时区信息的基本类型 | + +### 在字符串和日期时间之间转换 + +您可以使用`str`或`strftime`方法对`datetime`对象和 pandas 的`Timestamp`对象进行格式化为字符串,传递格式规范: + +```py +In [26]: stamp = datetime(2011, 1, 3) + +In [27]: str(stamp) +Out[27]: '2011-01-03 00:00:00' + +In [28]: stamp.strftime("%Y-%m-%d") +Out[28]: '2011-01-03' +``` + +请参阅表 11.2 以获取完整的格式代码列表。 + +表 11.2:`datetime`格式规范(ISO C89 兼容) + +| 类型 | 描述 | +| --- | --- | +| `%Y` | 四位数年份 | +| `%y` | 两位数年份 | +| `%m` | 两位数月份[01, 12] | +| `%d` | 两位数日期[01, 31] | +| `%H` | 小时(24 小时制)[00, 23] | +| `%I` | 小时(12 小时制)[01, 12] | +| `%M` | 两位数分钟[00, 59] | +| `%S` | 秒[00, 61](秒 60, 61 表示闰秒) | +| `%f` | 微秒作为整数,零填充(从 000000 到 999999) | +| `%j` | 一年中的日期作为零填充的整数(从 001 到 336) | +| `%w` | 星期几作为整数[0(星期日),6] | +| `%u` | 从 1 开始的星期几整数,其中 1 是星期一。 | +| `%U` | 一年中的周数[00, 53]; 星期日被认为是一周的第一天,年初第一个星期日之前的日子被称为“第 0 周” | +| `%W` | 一年中的周数[00, 53]; 星期一被认为是一周的第一天,年初第一个星期一之前的日子被称为“第 0 周” | +| `%z` | UTC 时区偏移为`+HHMM`或`-HHMM`; 如果时区是 naive,则为空 | +| `%Z` | 时区名称作为字符串,如果没有时区则为空字符串 | +| `%F` | `%Y-%m-%d`的快捷方式(例如,`2012-4-18`) | +| `%D` | `%m/%d/%y`的快捷方式(例如,`04/18/12`) | + +您可以使用许多相同的格式代码使用`datetime.strptime`将字符串转换为日期(但是一些代码,如`%F`,不能使用): + +```py +In [29]: value = "2011-01-03" + +In [30]: datetime.strptime(value, "%Y-%m-%d") +Out[30]: datetime.datetime(2011, 1, 3, 0, 0) + +In [31]: datestrs = ["7/6/2011", "8/6/2011"] + +In [32]: [datetime.strptime(x, "%m/%d/%Y") for x in datestrs] +Out[32]: +[datetime.datetime(2011, 7, 6, 0, 0), + datetime.datetime(2011, 8, 6, 0, 0)] +``` + +`datetime.strptime` 是一种解析具有已知格式的日期的方法。 + +pandas 通常面向处理日期数组,无论是作为轴索引还是数据框中的列。`pandas.to_datetime`方法解析许多不同类型的日期表示。标准日期格式如 ISO 8601 可以快速解析: + +```py +In [33]: datestrs = ["2011-07-06 12:00:00", "2011-08-06 00:00:00"] + +In [34]: pd.to_datetime(datestrs) +Out[34]: DatetimeIndex(['2011-07-06 12:00:00', '2011-08-06 00:00:00'], dtype='dat +etime64[ns]', freq=None) +``` + +它还处理应被视为缺失的值(`None`,空字符串等): + +```py +In [35]: idx = pd.to_datetime(datestrs + [None]) + +In [36]: idx +Out[36]: DatetimeIndex(['2011-07-06 12:00:00', '2011-08-06 00:00:00', 'NaT'], dty +pe='datetime64[ns]', freq=None) + +In [37]: idx[2] +Out[37]: NaT + +In [38]: pd.isna(idx) +Out[38]: array([False, False, True]) +``` + +`NaT`(不是时间)是 pandas 中的时间戳数据的空值。 + +注意 + +`dateutil.parser`是一个有用但不完美的工具。值得注意的是,它会将一些字符串识别为日期,而您可能希望它不会;例如,`"42"`将被解析为年份`2042`与今天的日历日期相对应。 + +`datetime`对象还具有许多针对其他国家或语言系统的特定于区域的格式选项。例如,德国或法国系统上的缩写月份名称与英语系统上的不同。请参阅表 11.3 以获取列表。 + +表 11.3:特定于区域的日期格式化 + +| 类型 | 描述 | +| --- | --- | +| `%a` | 缩写的星期几名称 | +| `%A` | 完整的星期几名称 | +| `%b` | 缩写的月份名称 | +| `%B` | 完整的月份名称 | +| `%c` | 完整的日期和时间(例如,‘周二 2012 年 5 月 1 日 下午 04:20:57’) | +| `%p` | AM 或 PM 的本地等效 | +| `%x` | 本地适用的格式化日期(例如,在美国,2012 年 5 月 1 日为‘05/01/2012’) | + +| `%X` | 本地适用的时间(例如,‘下午 04:24:12’) | + +## 11.2 时间序列基础知识 + +pandas 中的一种基本类型的时间序列对象是由时间戳索引的 Series,通常在 pandas 之外表示为 Python 字符串或`datetime`对象: + +```py +In [39]: dates = [datetime(2011, 1, 2), datetime(2011, 1, 5), + ....: datetime(2011, 1, 7), datetime(2011, 1, 8), + ....: datetime(2011, 1, 10), datetime(2011, 1, 12)] + +In [40]: ts = pd.Series(np.random.standard_normal(6), index=dates) + +In [41]: ts +Out[41]: +2011-01-02 -0.204708 +2011-01-05 0.478943 +2011-01-07 -0.519439 +2011-01-08 -0.555730 +2011-01-10 1.965781 +2011-01-12 1.393406 +dtype: float64 +``` + +在幕后,这些`datetime`对象已被放入`DatetimeIndex`中: + +```py +In [42]: ts.index +Out[42]: +DatetimeIndex(['2011-01-02', '2011-01-05', '2011-01-07', '2011-01-08', + '2011-01-10', '2011-01-12'], + dtype='datetime64[ns]', freq=None) +``` + +与其他 Series 一样,不同索引的时间序列之间的算术运算会自动对齐日期: + +```py +In [43]: ts + ts[::2] +Out[43]: +2011-01-02 -0.409415 +2011-01-05 NaN +2011-01-07 -1.038877 +2011-01-08 NaN +2011-01-10 3.931561 +2011-01-12 NaN +dtype: float64 +``` + +请记住,`ts[::2]`选择`ts`中的每个第二个元素。 + +pandas 使用 NumPy 的`datetime64`数据类型以纳秒分辨率存储时间戳: + +```py +In [44]: ts.index.dtype +Out[44]: dtype(' +``` + +字符串`"D"`被解释为每日频率。 + +在频率之间的转换或*重新采样*是一个足够大的主题,后面会有自己的部分(重新采样和频率转换)。在这里,我将向您展示如何使用基本频率及其倍数。 + +### 生成日期范围 + +虽然我之前没有解释过,但`pandas.date_range`负责根据特定频率生成具有指定长度的`DatetimeIndex`: + +```py +In [74]: index = pd.date_range("2012-04-01", "2012-06-01") + +In [75]: index +Out[75]: +DatetimeIndex(['2012-04-01', '2012-04-02', '2012-04-03', '2012-04-04', + '2012-04-05', '2012-04-06', '2012-04-07', '2012-04-08', + '2012-04-09', '2012-04-10', '2012-04-11', '2012-04-12', + '2012-04-13', '2012-04-14', '2012-04-15', '2012-04-16', + '2012-04-17', '2012-04-18', '2012-04-19', '2012-04-20', + '2012-04-21', '2012-04-22', '2012-04-23', '2012-04-24', + '2012-04-25', '2012-04-26', '2012-04-27', '2012-04-28', + '2012-04-29', '2012-04-30', '2012-05-01', '2012-05-02', + '2012-05-03', '2012-05-04', '2012-05-05', '2012-05-06', + '2012-05-07', '2012-05-08', '2012-05-09', '2012-05-10', + '2012-05-11', '2012-05-12', '2012-05-13', '2012-05-14', + '2012-05-15', '2012-05-16', '2012-05-17', '2012-05-18', + '2012-05-19', '2012-05-20', '2012-05-21', '2012-05-22', + '2012-05-23', '2012-05-24', '2012-05-25', '2012-05-26', + '2012-05-27', '2012-05-28', '2012-05-29', '2012-05-30', + '2012-05-31', '2012-06-01'], + dtype='datetime64[ns]', freq='D') +``` + +默认情况下,`pandas.date_range`生成每日时间戳。如果只传递开始或结束日期,必须传递一个周期数来生成: + +```py +In [76]: pd.date_range(start="2012-04-01", periods=20) +Out[76]: +DatetimeIndex(['2012-04-01', '2012-04-02', '2012-04-03', '2012-04-04', + '2012-04-05', '2012-04-06', '2012-04-07', '2012-04-08', + '2012-04-09', '2012-04-10', '2012-04-11', '2012-04-12', + '2012-04-13', '2012-04-14', '2012-04-15', '2012-04-16', + '2012-04-17', '2012-04-18', '2012-04-19', '2012-04-20'], + dtype='datetime64[ns]', freq='D') + +In [77]: pd.date_range(end="2012-06-01", periods=20) +Out[77]: +DatetimeIndex(['2012-05-13', '2012-05-14', '2012-05-15', '2012-05-16', + '2012-05-17', '2012-05-18', '2012-05-19', '2012-05-20', + '2012-05-21', '2012-05-22', '2012-05-23', '2012-05-24', + '2012-05-25', '2012-05-26', '2012-05-27', '2012-05-28', + '2012-05-29', '2012-05-30', '2012-05-31', '2012-06-01'], + dtype='datetime64[ns]', freq='D') +``` + +开始和结束日期为生成的日期索引定义了严格的边界。例如,如果您想要一个包含每个月最后一个工作日的日期索引,您将传递 `"BM"` 频率(月底的工作日;请参阅 Table 11.4 中更完整的频率列表),只有落在日期区间内或日期区间内的日期将被包括: + +```py +In [78]: pd.date_range("2000-01-01", "2000-12-01", freq="BM") +Out[78]: +DatetimeIndex(['2000-01-31', '2000-02-29', '2000-03-31', '2000-04-28', + '2000-05-31', '2000-06-30', '2000-07-31', '2000-08-31', + '2000-09-29', '2000-10-31', '2000-11-30'], + dtype='datetime64[ns]', freq='BM') +``` + +Table 11.4: 基础时间序列频率(不全面) + +| 别名 | 偏移类型 | 描述 | +| --- | --- | --- | +| `D` | `Day` | 日历日 | +| `B` | `BusinessDay` | 工作日 | +| `H` | `Hour` | 每小时 | +| `T` 或 `min` | `Minute` | 每分钟一次 | +| `S` | `Second` | 每秒一次 | +| `L` 或 `ms` | `Milli` | 毫秒(1 秒的 1/1,000) | +| `U` | `Micro` | 微秒(1 秒的 1/1,000,000) | +| `M` | `MonthEnd` | 月份的最后一个日历日 | +| `BM` | `BusinessMonthEnd` | 月份的最后一个工作日(工作日) | +| `MS` | `MonthBegin` | 月份的第一个日历日 | +| `BMS` | `BusinessMonthBegin` | 月份的第一个工作日 | +| `W-MON, W-TUE, ...` | `Week` | 每周在给定星期的某一天(MON、TUE、WED、THU、FRI、SAT 或 SUN) | +| `WOM-1MON, WOM-2MON, ...` | `WeekOfMonth` | 在月份的第一、第二、第三或第四周生成每周日期(例如,每月的第三个星期五为 `WOM-3FRI`) | +| `Q-JAN, Q-FEB, ...` | `QuarterEnd` | 季度日期锚定在每个月的最后一个日历日,年终在指定月份(JAN、FEB、MAR、APR、MAY、JUN、JUL、AUG、SEP、OCT、NOV 或 DEC) | +| `BQ-JAN, BQ-FEB, ...` | `BusinessQuarterEnd` | 季度日期锚定在每个月的最后一个工作日,年终在指定月份 | +| `QS-JAN, QS-FEB, ...` | `QuarterBegin` | 季度日期锚定在每个月的第一个日历日,年终在指定月份 | +| `BQS-JAN, BQS-FEB, ...` | `BusinessQuarterBegin` | 季度日期锚定在每个月的第一个工作日,年终在指定月份 | +| `A-JAN, A-FEB, ...` | `YearEnd` | 年度日期锚定在给定月份的最后一个日历日(JAN、FEB、MAR、APR、MAY、JUN、JUL、AUG、SEP、OCT、NOV 或 DEC) | +| `BA-JAN, BA-FEB, ...` | `BusinessYearEnd` | 年度日期锚定在给定月份的最后一个工作日 | +| `AS-JAN, AS-FEB, ...` | `YearBegin` | 年度日期锚定在给定月份的第一天 | +| `BAS-JAN, BAS-FEB, ...` | `BusinessYearBegin` | 年度日期锚定在给定月份的第一个工作日 | + +`pandas.date_range` 默认保留开始或结束时间戳的时间(如果有): + +```py +In [79]: pd.date_range("2012-05-02 12:56:31", periods=5) +Out[79]: +DatetimeIndex(['2012-05-02 12:56:31', '2012-05-03 12:56:31', + '2012-05-04 12:56:31', '2012-05-05 12:56:31', + '2012-05-06 12:56:31'], + dtype='datetime64[ns]', freq='D') +``` + +有时您会有带有时间信息的开始或结束日期,但希望生成一组时间戳,*规范化* 为午夜作为约定。为此,有一个 `normalize` 选项: + +```py +In [80]: pd.date_range("2012-05-02 12:56:31", periods=5, normalize=True) +Out[80]: +DatetimeIndex(['2012-05-02', '2012-05-03', '2012-05-04', '2012-05-05', + '2012-05-06'], + dtype='datetime64[ns]', freq='D') +``` + +### 频率和日期偏移 + +在 pandas 中,频率由 *基础频率* 和一个乘数组成。基础频率通常用字符串别名表示,如 `"M"` 表示每月或 `"H"` 表示每小时。对于每个基础频率,都有一个称为 *日期偏移* 的对象。例如,小时频率可以用 `Hour` 类表示: + +```py +In [81]: from pandas.tseries.offsets import Hour, Minute + +In [82]: hour = Hour() + +In [83]: hour +Out[83]: +``` + +您可以通过传递一个整数来定义偏移的倍数: + +```py +In [84]: four_hours = Hour(4) + +In [85]: four_hours +Out[85]: <4 * Hours> +``` + +在大多数应用程序中,您通常不需要显式创建这些对象之一;而是使用类似 `"H"` 或 `"4H"` 的字符串别名。在基础频率前放置一个整数会创建一个倍数: + +```py +In [86]: pd.date_range("2000-01-01", "2000-01-03 23:59", freq="4H") +Out[86]: +DatetimeIndex(['2000-01-01 00:00:00', '2000-01-01 04:00:00', + '2000-01-01 08:00:00', '2000-01-01 12:00:00', + '2000-01-01 16:00:00', '2000-01-01 20:00:00', + '2000-01-02 00:00:00', '2000-01-02 04:00:00', + '2000-01-02 08:00:00', '2000-01-02 12:00:00', + '2000-01-02 16:00:00', '2000-01-02 20:00:00', + '2000-01-03 00:00:00', '2000-01-03 04:00:00', + '2000-01-03 08:00:00', '2000-01-03 12:00:00', + '2000-01-03 16:00:00', '2000-01-03 20:00:00'], + dtype='datetime64[ns]', freq='4H') +``` + +许多偏移可以通过加法组合: + +```py +In [87]: Hour(2) + Minute(30) +Out[87]: <150 * Minutes> +``` + +同样,您可以传递频率字符串,如 `"1h30min"`,这将有效地解析为相同的表达式: + +```py +In [88]: pd.date_range("2000-01-01", periods=10, freq="1h30min") +Out[88]: +DatetimeIndex(['2000-01-01 00:00:00', '2000-01-01 01:30:00', + '2000-01-01 03:00:00', '2000-01-01 04:30:00', + '2000-01-01 06:00:00', '2000-01-01 07:30:00', + '2000-01-01 09:00:00', '2000-01-01 10:30:00', + '2000-01-01 12:00:00', '2000-01-01 13:30:00'], + dtype='datetime64[ns]', freq='90T') +``` + +一些频率描述的是时间点,这些时间点不是均匀间隔的。例如,`"M"`(日历月底)和 `"BM"`(月底的最后一个工作日/工作日)取决于一个月的天数,以及在后一种情况下,月份是否在周末结束。我们将这些称为 *锚定* 偏移。 + +请参考 Table 11.4 以获取 pandas 中可用的频率代码和日期偏移类的列表。 + +注意 + +用户可以定义自己的自定义频率类,以提供 pandas 中不可用的日期逻辑,但这些完整的细节超出了本书的范围。 + +#### 月份周日期 + +一个有用的频率类是“月份周”,从`WOM`开始。这使您可以获得每个月的第三个星期五这样的日期: + +```py +In [89]: monthly_dates = pd.date_range("2012-01-01", "2012-09-01", freq="WOM-3FRI +") + +In [90]: list(monthly_dates) +Out[90]: +[Timestamp('2012-01-20 00:00:00'), + Timestamp('2012-02-17 00:00:00'), + Timestamp('2012-03-16 00:00:00'), + Timestamp('2012-04-20 00:00:00'), + Timestamp('2012-05-18 00:00:00'), + Timestamp('2012-06-15 00:00:00'), + Timestamp('2012-07-20 00:00:00'), + Timestamp('2012-08-17 00:00:00')] +``` + +### 移动(领先和滞后)数据 + +*移动*指的是通过时间向后和向前移动数据。Series 和 DataFrame 都有一个`shift`方法,用于进行简单的向前或向后移位,保持索引不变: + +```py +In [91]: ts = pd.Series(np.random.standard_normal(4), + ....: index=pd.date_range("2000-01-01", periods=4, freq="M")) + +In [92]: ts +Out[92]: +2000-01-31 -0.066748 +2000-02-29 0.838639 +2000-03-31 -0.117388 +2000-04-30 -0.517795 +Freq: M, dtype: float64 + +In [93]: ts.shift(2) +Out[93]: +2000-01-31 NaN +2000-02-29 NaN +2000-03-31 -0.066748 +2000-04-30 0.838639 +Freq: M, dtype: float64 + +In [94]: ts.shift(-2) +Out[94]: +2000-01-31 -0.117388 +2000-02-29 -0.517795 +2000-03-31 NaN +2000-04-30 NaN +Freq: M, dtype: float64 +``` + +当我们这样移动时,缺失数据会在时间序列的开始或结束引入。 + +`shift`的一个常见用法是计算时间序列或多个时间序列的连续百分比变化作为 DataFrame 列。这表示为: + +```py +ts / ts.shift(1) - 1 +``` + +因为无时区移位会保持索引不变,所以会丢失一些数据。因此,如果知道频率,可以将其传递给`shift`以推进时间戳,而不仅仅是数据: + +```py +In [95]: ts.shift(2, freq="M") +Out[95]: +2000-03-31 -0.066748 +2000-04-30 0.838639 +2000-05-31 -0.117388 +2000-06-30 -0.517795 +Freq: M, dtype: float64 +``` + +也可以传递其他频率,这样可以在如何领先和滞后数据方面提供一些灵活性: + +```py +In [96]: ts.shift(3, freq="D") +Out[96]: +2000-02-03 -0.066748 +2000-03-03 0.838639 +2000-04-03 -0.117388 +2000-05-03 -0.517795 +dtype: float64 + +In [97]: ts.shift(1, freq="90T") +Out[97]: +2000-01-31 01:30:00 -0.066748 +2000-02-29 01:30:00 0.838639 +2000-03-31 01:30:00 -0.117388 +2000-04-30 01:30:00 -0.517795 +dtype: float64 +``` + +这里的`T`代表分钟。请注意,这里的`freq`参数表示要应用于时间戳的偏移量,但它不会改变数据的基础频率(如果有的话)。 + +#### 使用偏移移动日期 + +pandas 日期偏移也可以与`datetime`或`Timestamp`对象一起使用: + +```py +In [98]: from pandas.tseries.offsets import Day, MonthEnd + +In [99]: now = datetime(2011, 11, 17) + +In [100]: now + 3 * Day() +Out[100]: Timestamp('2011-11-20 00:00:00') +``` + +如果添加像`MonthEnd`这样的锚定偏移,第一个增量将根据频率规则“向前滚动”日期到下一个日期: + +```py +In [101]: now + MonthEnd() +Out[101]: Timestamp('2011-11-30 00:00:00') + +In [102]: now + MonthEnd(2) +Out[102]: Timestamp('2011-12-31 00:00:00') +``` + +锚定偏移可以通过简单使用它们的`rollforward`和`rollback`方法明确地“滚动”日期向前或向后: + +```py +In [103]: offset = MonthEnd() + +In [104]: offset.rollforward(now) +Out[104]: Timestamp('2011-11-30 00:00:00') + +In [105]: offset.rollback(now) +Out[105]: Timestamp('2011-10-31 00:00:00') +``` + +日期偏移的一个创造性用法是将这些方法与`groupby`一起使用: + +```py +In [106]: ts = pd.Series(np.random.standard_normal(20), + .....: index=pd.date_range("2000-01-15", periods=20, freq="4D") +) + +In [107]: ts +Out[107]: +2000-01-15 -0.116696 +2000-01-19 2.389645 +2000-01-23 -0.932454 +2000-01-27 -0.229331 +2000-01-31 -1.140330 +2000-02-04 0.439920 +2000-02-08 -0.823758 +2000-02-12 -0.520930 +2000-02-16 0.350282 +2000-02-20 0.204395 +2000-02-24 0.133445 +2000-02-28 0.327905 +2000-03-03 0.072153 +2000-03-07 0.131678 +2000-03-11 -1.297459 +2000-03-15 0.997747 +2000-03-19 0.870955 +2000-03-23 -0.991253 +2000-03-27 0.151699 +2000-03-31 1.266151 +Freq: 4D, dtype: float64 + +In [108]: ts.groupby(MonthEnd().rollforward).mean() +Out[108]: +2000-01-31 -0.005833 +2000-02-29 0.015894 +2000-03-31 0.150209 +dtype: float64 +``` + +当然,更简单更快的方法是使用`resample`(我们将在重新采样和频率转换中更深入地讨论这个问题): + +```py +In [109]: ts.resample("M").mean() +Out[109]: +2000-01-31 -0.005833 +2000-02-29 0.015894 +2000-03-31 0.150209 +Freq: M, dtype: float64 +``` + +## 11.4 时区处理 + +与时区一起工作可能是时间序列操作中最不愉快的部分之一。因此,许多时间序列用户选择在*协调世界时*或*UTC*中处理时间序列,这是地理独立的国际标准。时区表示为与 UTC 的偏移;例如,纽约在夏令时(DST)期间比 UTC 晚四个小时,在其他时间比 UTC 晚五个小时。 + +在 Python 中,时区信息来自第三方`pytz`库(可通过 pip 或 conda 安装),该库公开了*Olson 数据库*,这是世界时区信息的编译。这对于历史数据尤为重要,因为夏令时转换日期(甚至 UTC 偏移)已根据地区法律多次更改。在美国,自 1900 年以来,夏令时转换时间已经多次更改! + +有关`pytz`库的详细信息,您需要查看该库的文档。就本书而言,pandas 封装了`pytz`的功能,因此您可以忽略其 API 以外的时区名称。由于 pandas 对`pytz`有硬性依赖,因此不需要单独安装它。时区名称可以在交互式和文档中找到: + +```py +In [110]: import pytz + +In [111]: pytz.common_timezones[-5:] +Out[111]: ['US/Eastern', 'US/Hawaii', 'US/Mountain', 'US/Pacific', 'UTC'] +``` + +要从`pytz`中获取时区对象,请使用`pytz.timezone`: + +```py +In [112]: tz = pytz.timezone("America/New_York") + +In [113]: tz +Out[113]: +``` + +pandas 中的方法将接受时区名称或这些对象。 + +### 时区本地化和转换 + +默认情况下,pandas 中的时间序列是*时区无关*的。例如,考虑以下时间序列: + +```py +In [114]: dates = pd.date_range("2012-03-09 09:30", periods=6) + +In [115]: ts = pd.Series(np.random.standard_normal(len(dates)), index=dates) + +In [116]: ts +Out[116]: +2012-03-09 09:30:00 -0.202469 +2012-03-10 09:30:00 0.050718 +2012-03-11 09:30:00 0.639869 +2012-03-12 09:30:00 0.597594 +2012-03-13 09:30:00 -0.797246 +2012-03-14 09:30:00 0.472879 +Freq: D, dtype: float64 +``` + +索引的`tz`字段为`None`: + +```py +In [117]: print(ts.index.tz) +None +``` + +可以生成带有时区设置的日期范围: + +```py +In [118]: pd.date_range("2012-03-09 09:30", periods=10, tz="UTC") +Out[118]: +DatetimeIndex(['2012-03-09 09:30:00+00:00', '2012-03-10 09:30:00+00:00', + '2012-03-11 09:30:00+00:00', '2012-03-12 09:30:00+00:00', + '2012-03-13 09:30:00+00:00', '2012-03-14 09:30:00+00:00', + '2012-03-15 09:30:00+00:00', '2012-03-16 09:30:00+00:00', + '2012-03-17 09:30:00+00:00', '2012-03-18 09:30:00+00:00'], + dtype='datetime64[ns, UTC]', freq='D') +``` + +从无时区转换为*本地化*(重新解释为在特定时区中观察到)由`tz_localize`方法处理: + +```py +In [119]: ts +Out[119]: +2012-03-09 09:30:00 -0.202469 +2012-03-10 09:30:00 0.050718 +2012-03-11 09:30:00 0.639869 +2012-03-12 09:30:00 0.597594 +2012-03-13 09:30:00 -0.797246 +2012-03-14 09:30:00 0.472879 +Freq: D, dtype: float64 + +In [120]: ts_utc = ts.tz_localize("UTC") + +In [121]: ts_utc +Out[121]: +2012-03-09 09:30:00+00:00 -0.202469 +2012-03-10 09:30:00+00:00 0.050718 +2012-03-11 09:30:00+00:00 0.639869 +2012-03-12 09:30:00+00:00 0.597594 +2012-03-13 09:30:00+00:00 -0.797246 +2012-03-14 09:30:00+00:00 0.472879 +Freq: D, dtype: float64 + +In [122]: ts_utc.index +Out[122]: +DatetimeIndex(['2012-03-09 09:30:00+00:00', '2012-03-10 09:30:00+00:00', + '2012-03-11 09:30:00+00:00', '2012-03-12 09:30:00+00:00', + '2012-03-13 09:30:00+00:00', '2012-03-14 09:30:00+00:00'], + dtype='datetime64[ns, UTC]', freq='D') +``` + +一旦时间序列被本地化到特定的时区,它可以使用`tz_convert`转换为另一个时区: + +```py +In [123]: ts_utc.tz_convert("America/New_York") +Out[123]: +2012-03-09 04:30:00-05:00 -0.202469 +2012-03-10 04:30:00-05:00 0.050718 +2012-03-11 05:30:00-04:00 0.639869 +2012-03-12 05:30:00-04:00 0.597594 +2012-03-13 05:30:00-04:00 -0.797246 +2012-03-14 05:30:00-04:00 0.472879 +Freq: D, dtype: float64 +``` + +在前述时间序列的情况下,该时间序列跨越了`America/New_York`时区的夏令时转换,我们可以将其本地化为美国东部时间,然后转换为 UTC 或柏林时间: + +```py +In [124]: ts_eastern = ts.tz_localize("America/New_York") + +In [125]: ts_eastern.tz_convert("UTC") +Out[125]: +2012-03-09 14:30:00+00:00 -0.202469 +2012-03-10 14:30:00+00:00 0.050718 +2012-03-11 13:30:00+00:00 0.639869 +2012-03-12 13:30:00+00:00 0.597594 +2012-03-13 13:30:00+00:00 -0.797246 +2012-03-14 13:30:00+00:00 0.472879 +dtype: float64 + +In [126]: ts_eastern.tz_convert("Europe/Berlin") +Out[126]: +2012-03-09 15:30:00+01:00 -0.202469 +2012-03-10 15:30:00+01:00 0.050718 +2012-03-11 14:30:00+01:00 0.639869 +2012-03-12 14:30:00+01:00 0.597594 +2012-03-13 14:30:00+01:00 -0.797246 +2012-03-14 14:30:00+01:00 0.472879 +dtype: float64 +``` + +`tz_localize`和`tz_convert`也是`DatetimeIndex`的实例方法: + +```py +In [127]: ts.index.tz_localize("Asia/Shanghai") +Out[127]: +DatetimeIndex(['2012-03-09 09:30:00+08:00', '2012-03-10 09:30:00+08:00', + '2012-03-11 09:30:00+08:00', '2012-03-12 09:30:00+08:00', + '2012-03-13 09:30:00+08:00', '2012-03-14 09:30:00+08:00'], + dtype='datetime64[ns, Asia/Shanghai]', freq=None) +``` + +注意 + +本地化无时区时间戳还会检查夏令时转换周围的模糊或不存在的时间。 + +### 与时区感知时间戳对象的操作 + +类似于时间序列和日期范围,个别`Timestamp`对象也可以从无时区转换为时区感知,并从一个时区转换为另一个时区: + +```py +In [128]: stamp = pd.Timestamp("2011-03-12 04:00") + +In [129]: stamp_utc = stamp.tz_localize("utc") + +In [130]: stamp_utc.tz_convert("America/New_York") +Out[130]: Timestamp('2011-03-11 23:00:00-0500', tz='America/New_York') +``` + +创建`Timestamp`时也可以传递时区: + +```py +In [131]: stamp_moscow = pd.Timestamp("2011-03-12 04:00", tz="Europe/Moscow") + +In [132]: stamp_moscow +Out[132]: Timestamp('2011-03-12 04:00:00+0300', tz='Europe/Moscow') +``` + +时区感知的`Timestamp`对象在内部以自 Unix 纪元(1970 年 1 月 1 日)以来的纳秒为单位存储 UTC 时间戳值,因此更改时区不会改变内部 UTC 值: + +```py +In [133]: stamp_utc.value +Out[133]: 1299902400000000000 + +In [134]: stamp_utc.tz_convert("America/New_York").value +Out[134]: 1299902400000000000 +``` + +在使用 pandas 的`DateOffset`对象执行时间算术时,pandas 会尽可能尊重夏令时转换。这里我们构造了发生在夏令时转换之前的时间戳(向前和向后)。首先,在转换为夏令时前 30 分钟: + +```py +In [135]: stamp = pd.Timestamp("2012-03-11 01:30", tz="US/Eastern") + +In [136]: stamp +Out[136]: Timestamp('2012-03-11 01:30:00-0500', tz='US/Eastern') + +In [137]: stamp + Hour() +Out[137]: Timestamp('2012-03-11 03:30:00-0400', tz='US/Eastern') +``` + +然后,在夏令时转换前 90 分钟: + +```py +In [138]: stamp = pd.Timestamp("2012-11-04 00:30", tz="US/Eastern") + +In [139]: stamp +Out[139]: Timestamp('2012-11-04 00:30:00-0400', tz='US/Eastern') + +In [140]: stamp + 2 * Hour() +Out[140]: Timestamp('2012-11-04 01:30:00-0500', tz='US/Eastern') +``` + +### 不同时区之间的操作 + +如果将具有不同时区的两个时间序列组合,结果将是 UTC。由于时间戳在 UTC 下存储,这是一个简单的操作,不需要转换: + +```py +In [141]: dates = pd.date_range("2012-03-07 09:30", periods=10, freq="B") + +In [142]: ts = pd.Series(np.random.standard_normal(len(dates)), index=dates) + +In [143]: ts +Out[143]: +2012-03-07 09:30:00 0.522356 +2012-03-08 09:30:00 -0.546348 +2012-03-09 09:30:00 -0.733537 +2012-03-12 09:30:00 1.302736 +2012-03-13 09:30:00 0.022199 +2012-03-14 09:30:00 0.364287 +2012-03-15 09:30:00 -0.922839 +2012-03-16 09:30:00 0.312656 +2012-03-19 09:30:00 -1.128497 +2012-03-20 09:30:00 -0.333488 +Freq: B, dtype: float64 + +In [144]: ts1 = ts[:7].tz_localize("Europe/London") + +In [145]: ts2 = ts1[2:].tz_convert("Europe/Moscow") + +In [146]: result = ts1 + ts2 + +In [147]: result.index +Out[147]: +DatetimeIndex(['2012-03-07 09:30:00+00:00', '2012-03-08 09:30:00+00:00', + '2012-03-09 09:30:00+00:00', '2012-03-12 09:30:00+00:00', + '2012-03-13 09:30:00+00:00', '2012-03-14 09:30:00+00:00', + '2012-03-15 09:30:00+00:00'], + dtype='datetime64[ns, UTC]', freq=None) +``` + +不支持在时区无关和时区感知数据之间进行操作,会引发异常。*## 11.5 周期和周期算术 + +*Periods*代表时间跨度,如天、月、季度或年。`pandas.Period`类表示这种数据类型,需要一个字符串或整数和一个来自 Table 11.4 的支持频率: + +```py +In [148]: p = pd.Period("2011", freq="A-DEC") + +In [149]: p +Out[149]: Period('2011', 'A-DEC') +``` + +在这种情况下,`Period`对象表示从 2011 年 1 月 1 日到 2011 年 12 月 31 日的完整时间跨度。方便的是,从周期中添加和减去整数会改变它们的频率: + +```py +In [150]: p + 5 +Out[150]: Period('2016', 'A-DEC') + +In [151]: p - 2 +Out[151]: Period('2009', 'A-DEC') +``` + +如果两个周期具有相同的频率,则它们之间的差异是单位之间的数量作为日期偏移量: + +```py +In [152]: pd.Period("2014", freq="A-DEC") - p +Out[152]: <3 * YearEnds: month=12> +``` + +可以使用`period_range`函数构建周期的常规范围: + +```py +In [153]: periods = pd.period_range("2000-01-01", "2000-06-30", freq="M") + +In [154]: periods +Out[154]: PeriodIndex(['2000-01', '2000-02', '2000-03', '2000-04', '2000-05', '20 +00-06'], dtype='period[M]') +``` + +`PeriodIndex`类存储一系列周期,并可以作为任何 pandas 数据结构中的轴索引: + +```py +In [155]: pd.Series(np.random.standard_normal(6), index=periods) +Out[155]: +2000-01 -0.514551 +2000-02 -0.559782 +2000-03 -0.783408 +2000-04 -1.797685 +2000-05 -0.172670 +2000-06 0.680215 +Freq: M, dtype: float64 +``` + +如果您有一个字符串数组,也可以使用`PeriodIndex`类,其中所有值都是周期: + +```py +In [156]: values = ["2001Q3", "2002Q2", "2003Q1"] + +In [157]: index = pd.PeriodIndex(values, freq="Q-DEC") + +In [158]: index +Out[158]: PeriodIndex(['2001Q3', '2002Q2', '2003Q1'], dtype='period[Q-DEC]') +``` + +### 周期频率转换 + +周期和`PeriodIndex`对象可以使用它们的`asfreq`方法转换为另一个频率。例如,假设我们有一个年度周期,想要将其转换为每月周期,可以在年初或年末进行。可以这样做: + +```py +In [159]: p = pd.Period("2011", freq="A-DEC") + +In [160]: p +Out[160]: Period('2011', 'A-DEC') + +In [161]: p.asfreq("M", how="start") +Out[161]: Period('2011-01', 'M') + +In [162]: p.asfreq("M", how="end") +Out[162]: Period('2011-12', 'M') + +In [163]: p.asfreq("M") +Out[163]: Period('2011-12', 'M') +``` + +您可以将`Period("2011", "A-DEC")`看作是指向一段时间的光标,由月度周期细分。参见 Figure 11.1 以了解这一点。对于以 12 月以外的月份结束的*财政年度*,相应的月度子周期是不同的: + +```py +In [164]: p = pd.Period("2011", freq="A-JUN") + +In [165]: p +Out[165]: Period('2011', 'A-JUN') + +In [166]: p.asfreq("M", how="start") +Out[166]: Period('2010-07', 'M') + +In [167]: p.asfreq("M", how="end") +Out[167]: Period('2011-06', 'M') +``` + +![](img/d8cb52110dd49d0ab8074f78afe0845d.png) + +图 11.1:周期频率转换示例 + +当您从高频率转换为低频率时,pandas 会确定子周期,取决于超级周期“属于”哪里。例如,在`A-JUN`频率中,月份`Aug-2011`实际上是`2012`周期的一部分: + +```py +In [168]: p = pd.Period("Aug-2011", "M") + +In [169]: p.asfreq("A-JUN") +Out[169]: Period('2012', 'A-JUN') +``` + +整个`PeriodIndex`对象或时间序列也可以使用相同的语义进行类似转换: + +```py +In [170]: periods = pd.period_range("2006", "2009", freq="A-DEC") + +In [171]: ts = pd.Series(np.random.standard_normal(len(periods)), index=periods) + +In [172]: ts +Out[172]: +2006 1.607578 +2007 0.200381 +2008 -0.834068 +2009 -0.302988 +Freq: A-DEC, dtype: float64 + +In [173]: ts.asfreq("M", how="start") +Out[173]: +2006-01 1.607578 +2007-01 0.200381 +2008-01 -0.834068 +2009-01 -0.302988 +Freq: M, dtype: float64 +``` + +在这里,年度周期被替换为对应于每个年度周期中第一个月的月度周期。如果我们希望每年的最后一个工作日,可以使用`"B"`频率并指示我们想要周期的结束: + +```py +In [174]: ts.asfreq("B", how="end") +Out[174]: +2006-12-29 1.607578 +2007-12-31 0.200381 +2008-12-31 -0.834068 +2009-12-31 -0.302988 +Freq: B, dtype: float64 +``` + +### 季度周期频率 + +季度数据在会计、金融和其他领域中很常见。许多季度数据是相对于*财年结束*报告的,通常是一年中的 12 个月的最后一个日历日或工作日。因此,期间 `2012Q4` 根据财年结束日期的不同具有不同的含义。pandas 支持所有 12 种可能的季度频率,从 `Q-JAN` 到 `Q-DEC`: + +```py +In [175]: p = pd.Period("2012Q4", freq="Q-JAN") + +In [176]: p +Out[176]: Period('2012Q4', 'Q-JAN') +``` + +在财年结束于一月的情况下,`2012Q4` 从 2011 年 11 月到 2012 年 1 月,您可以通过转换为每日频率来检查: + +```py +In [177]: p.asfreq("D", how="start") +Out[177]: Period('2011-11-01', 'D') + +In [178]: p.asfreq("D", how="end") +Out[178]: Period('2012-01-31', 'D') +``` + +参见 Figure 11.2 进行说明。 + +![](img/09caf3da85296866437ce8e683dc7f92.png) + +Figure 11.2: 不同的季度频率约定 + +因此,可以进行方便的期间算术;例如,要获取季度倒数第二个工作日下午 4 点的时间戳,可以执行以下操作: + +```py +In [179]: p4pm = (p.asfreq("B", how="end") - 1).asfreq("T", how="start") + 16 * 6 +0 + +In [180]: p4pm +Out[180]: Period('2012-01-30 16:00', 'T') + +In [181]: p4pm.to_timestamp() +Out[181]: Timestamp('2012-01-30 16:00:00') +``` + +`to_timestamp` 方法默认返回期间开始的 `Timestamp`。 + +您可以使用 `pandas.period_range` 生成季度范围。算术也是相同的: + +```py +In [182]: periods = pd.period_range("2011Q3", "2012Q4", freq="Q-JAN") + +In [183]: ts = pd.Series(np.arange(len(periods)), index=periods) + +In [184]: ts +Out[184]: +2011Q3 0 +2011Q4 1 +2012Q1 2 +2012Q2 3 +2012Q3 4 +2012Q4 5 +Freq: Q-JAN, dtype: int64 + +In [185]: new_periods = (periods.asfreq("B", "end") - 1).asfreq("H", "start") + 1 +6 + +In [186]: ts.index = new_periods.to_timestamp() + +In [187]: ts +Out[187]: +2010-10-28 16:00:00 0 +2011-01-28 16:00:00 1 +2011-04-28 16:00:00 2 +2011-07-28 16:00:00 3 +2011-10-28 16:00:00 4 +2012-01-30 16:00:00 5 +dtype: int64 +``` + +### 将时间戳转换为期间(以及相反) + +通过 `to_period` 方法,以时间戳索引的 Series 和 DataFrame 对象可以转换为期间: + +```py +In [188]: dates = pd.date_range("2000-01-01", periods=3, freq="M") + +In [189]: ts = pd.Series(np.random.standard_normal(3), index=dates) + +In [190]: ts +Out[190]: +2000-01-31 1.663261 +2000-02-29 -0.996206 +2000-03-31 1.521760 +Freq: M, dtype: float64 + +In [191]: pts = ts.to_period() + +In [192]: pts +Out[192]: +2000-01 1.663261 +2000-02 -0.996206 +2000-03 1.521760 +Freq: M, dtype: float64 +``` + +由于期间指的是不重叠的时间跨度,因此给定频率的时间戳只能属于一个期间。虽然新的 `PeriodIndex` 的频率默认情况下是根据时间戳推断的,但您可以指定任何支持的频率(大多数列在 Table 11.4 中列出的频率都受支持)。在结果中有重复期间也没有问题: + +```py +In [193]: dates = pd.date_range("2000-01-29", periods=6) + +In [194]: ts2 = pd.Series(np.random.standard_normal(6), index=dates) + +In [195]: ts2 +Out[195]: +2000-01-29 0.244175 +2000-01-30 0.423331 +2000-01-31 -0.654040 +2000-02-01 2.089154 +2000-02-02 -0.060220 +2000-02-03 -0.167933 +Freq: D, dtype: float64 + +In [196]: ts2.to_period("M") +Out[196]: +2000-01 0.244175 +2000-01 0.423331 +2000-01 -0.654040 +2000-02 2.089154 +2000-02 -0.060220 +2000-02 -0.167933 +Freq: M, dtype: float64 +``` + +要转换回时间戳,请使用 `to_timestamp` 方法,该方法返回一个 `DatetimeIndex`: + +```py +In [197]: pts = ts2.to_period() + +In [198]: pts +Out[198]: +2000-01-29 0.244175 +2000-01-30 0.423331 +2000-01-31 -0.654040 +2000-02-01 2.089154 +2000-02-02 -0.060220 +2000-02-03 -0.167933 +Freq: D, dtype: float64 + +In [199]: pts.to_timestamp(how="end") +Out[199]: +2000-01-29 23:59:59.999999999 0.244175 +2000-01-30 23:59:59.999999999 0.423331 +2000-01-31 23:59:59.999999999 -0.654040 +2000-02-01 23:59:59.999999999 2.089154 +2000-02-02 23:59:59.999999999 -0.060220 +2000-02-03 23:59:59.999999999 -0.167933 +Freq: D, dtype: float64 +``` + +### 从数组创建 PeriodIndex + +固定频率数据集有时会存储在跨多列的时间跨度信息中。例如,在这个宏观经济数据集中,年份和季度在不同的列中: + +```py +In [200]: data = pd.read_csv("examples/macrodata.csv") + +In [201]: data.head(5) +Out[201]: + year quarter realgdp realcons realinv realgovt realdpi cpi +0 1959 1 2710.349 1707.4 286.898 470.045 1886.9 28.98 \ +1 1959 2 2778.801 1733.7 310.859 481.301 1919.7 29.15 +2 1959 3 2775.488 1751.8 289.226 491.260 1916.4 29.35 +3 1959 4 2785.204 1753.7 299.356 484.052 1931.3 29.37 +4 1960 1 2847.699 1770.5 331.722 462.199 1955.5 29.54 + m1 tbilrate unemp pop infl realint +0 139.7 2.82 5.8 177.146 0.00 0.00 +1 141.7 3.08 5.1 177.830 2.34 0.74 +2 140.5 3.82 5.3 178.657 2.74 1.09 +3 140.0 4.33 5.6 179.386 0.27 4.06 +4 139.6 3.50 5.2 180.007 2.31 1.19 + +In [202]: data["year"] +Out[202]: +0 1959 +1 1959 +2 1959 +3 1959 +4 1960 + ... +198 2008 +199 2008 +200 2009 +201 2009 +202 2009 +Name: year, Length: 203, dtype: int64 + +In [203]: data["quarter"] +Out[203]: +0 1 +1 2 +2 3 +3 4 +4 1 + .. +198 3 +199 4 +200 1 +201 2 +202 3 +Name: quarter, Length: 203, dtype: int64 +``` + +通过将这些数组传递给 `PeriodIndex` 并指定频率,可以将它们组合成 DataFrame 的索引: + +```py +In [204]: index = pd.PeriodIndex(year=data["year"], quarter=data["quarter"], + .....: freq="Q-DEC") + +In [205]: index +Out[205]: +PeriodIndex(['1959Q1', '1959Q2', '1959Q3', '1959Q4', '1960Q1', '1960Q2', + '1960Q3', '1960Q4', '1961Q1', '1961Q2', + ... + '2007Q2', '2007Q3', '2007Q4', '2008Q1', '2008Q2', '2008Q3', + '2008Q4', '2009Q1', '2009Q2', '2009Q3'], + dtype='period[Q-DEC]', length=203) + +In [206]: data.index = index + +In [207]: data["infl"] +Out[207]: +1959Q1 0.00 +1959Q2 2.34 +1959Q3 2.74 +1959Q4 0.27 +1960Q1 2.31 + ... +2008Q3 -3.16 +2008Q4 -8.79 +2009Q1 0.94 +2009Q2 3.37 +2009Q3 3.56 +Freq: Q-DEC, Name: infl, Length: 203, dtype: float64 +``` + +## 11.6 重新采样和频率转换 + +*重新采样* 指的是将时间序列从一种频率转换为另一种频率的过程。将高频数据聚合到低频称为*下采样*,而将低频转换为高频称为*上采样*。并非所有重新采样都属于这两类;例如,将 `W-WED`(每周三)转换为 `W-FRI` 既不是上采样也不是下采样。 + +pandas 对象配备有一个 `resample` 方法,这是所有频率转换的工作函数。`resample` 具有类似于 `groupby` 的 API;您调用 `resample` 来对数据进行分组,然后调用聚合函数: + +```py +In [208]: dates = pd.date_range("2000-01-01", periods=100) + +In [209]: ts = pd.Series(np.random.standard_normal(len(dates)), index=dates) + +In [210]: ts +Out[210]: +2000-01-01 0.631634 +2000-01-02 -1.594313 +2000-01-03 -1.519937 +2000-01-04 1.108752 +2000-01-05 1.255853 + ... +2000-04-05 -0.423776 +2000-04-06 0.789740 +2000-04-07 0.937568 +2000-04-08 -2.253294 +2000-04-09 -1.772919 +Freq: D, Length: 100, dtype: float64 + +In [211]: ts.resample("M").mean() +Out[211]: +2000-01-31 -0.165893 +2000-02-29 0.078606 +2000-03-31 0.223811 +2000-04-30 -0.063643 +Freq: M, dtype: float64 + +In [212]: ts.resample("M", kind="period").mean() +Out[212]: +2000-01 -0.165893 +2000-02 0.078606 +2000-03 0.223811 +2000-04 -0.063643 +Freq: M, dtype: float64 +``` + +`resample` 是一个灵活的方法,可用于处理大型时间序列。以下部分的示例说明了其语义和用法。Table 11.5 总结了一些选项。 + +Table 11.5: `resample` 方法参数 + +| 参数 | 描述 | +| --- | --- | +| `rule` | 字符串、DateOffset 或时间增量,指示所需的重新采样频率(例如,’M'、’5min' 或 `Second(15)`) | +| `axis` | 要重新采样的轴;默认 `axis=0` | +| `fill_method` | 在上采样时如何插值,例如 `"ffill"` 或 `"bfill"`;默认情况下不进行插值 | +| `closed` | 在下采样时,每个间隔的哪一端是闭合的(包含的),`"right"` 或 `"left"` | +| `label` | 在下采样时,如何标记聚合结果,使用 `"right"` 或 `"left"` 边界(例如,9:30 到 9:35 五分钟间隔可以标记为 `9:30` 或 `9:35`) | +| `limit` | 在向前或向后填充时,要填充的最大周期数 | +| `kind` | 聚合到期间(`"period"`)或时间戳(`"timestamp"`);默认为时间序列具有的索引类型 | +| `convention` | 在重新采样周期时,用于将低频周期转换为高频的约定(`"start"`或`"end"`);默认为`"start"` | +| `origin` | 用于确定重新采样箱边缘的“基准”时间戳;也可以是`"epoch"`、`"start"`、`"start_day"`、`"end"`或`"end_day"`之一;有关完整详细信息,请参阅`resample`文档字符串 | +| `offset` | 添加到原点的偏移时间间隔;默认为`None` | + +### 下采样 + +*下采样*是将数据聚合到常规、较低的频率。您正在聚合的数据不需要经常固定;所需频率定义了用于将时间序列切片成块以进行聚合的*箱边缘*。例如,要转换为每月,`"M"`或`"BM"`,您需要将数据切割成一个月的间隔。每个间隔被称为*半开放*;数据点只能属于一个间隔,间隔的并集必须构成整个时间范围。在使用`resample`对数据进行下采样时,有几件事需要考虑: + ++ 每个间隔的哪一侧是*关闭的* + ++ 如何为每个聚合的箱子打标签,可以是间隔的开始或结束 + +为了说明,让我们看一些一分钟频率的数据: + +```py +In [213]: dates = pd.date_range("2000-01-01", periods=12, freq="T") + +In [214]: ts = pd.Series(np.arange(len(dates)), index=dates) + +In [215]: ts +Out[215]: +2000-01-01 00:00:00 0 +2000-01-01 00:01:00 1 +2000-01-01 00:02:00 2 +2000-01-01 00:03:00 3 +2000-01-01 00:04:00 4 +2000-01-01 00:05:00 5 +2000-01-01 00:06:00 6 +2000-01-01 00:07:00 7 +2000-01-01 00:08:00 8 +2000-01-01 00:09:00 9 +2000-01-01 00:10:00 10 +2000-01-01 00:11:00 11 +Freq: T, dtype: int64 +``` + +假设您想要通过将每组的总和来将这些数据聚合成五分钟的块或*条*: + +```py +In [216]: ts.resample("5min").sum() +Out[216]: +2000-01-01 00:00:00 10 +2000-01-01 00:05:00 35 +2000-01-01 00:10:00 21 +Freq: 5T, dtype: int64 +``` + +您传递的频率定义了以五分钟为增量的箱边缘。对于这个频率,默认情况下*左*箱边缘是包含的,因此`00:00`值包含在`00:00`到`00:05`间隔中,而`00:05`值不包含在该间隔中。¹ + +```py +In [217]: ts.resample("5min", closed="right").sum() +Out[217]: +1999-12-31 23:55:00 0 +2000-01-01 00:00:00 15 +2000-01-01 00:05:00 40 +2000-01-01 00:10:00 11 +Freq: 5T, dtype: int64 +``` + +生成的时间序列由每个箱子左侧的时间戳标记。通过传递`label="right"`,您可以使用右侧箱子边缘对它们进行标记: + +```py +In [218]: ts.resample("5min", closed="right", label="right").sum() +Out[218]: +2000-01-01 00:00:00 0 +2000-01-01 00:05:00 15 +2000-01-01 00:10:00 40 +2000-01-01 00:15:00 11 +Freq: 5T, dtype: int64 +``` + +请参见图 11.3,以了解将分钟频率数据重新采样为五分钟频率的示例。 + +![](img/21026aa76a40a42294e16b84e2d400ed.png) + +图 11.3:五分钟重新采样示例,显示了闭合、标签约定 + +最后,您可能希望将结果索引向前移动一定量,例如从右边减去一秒,以便更清楚地了解时间戳所指的间隔。要执行此操作,请向结果索引添加一个偏移量: + +```py +In [219]: from pandas.tseries.frequencies import to_offset + +In [220]: result = ts.resample("5min", closed="right", label="right").sum() + +In [221]: result.index = result.index + to_offset("-1s") + +In [222]: result +Out[222]: +1999-12-31 23:59:59 0 +2000-01-01 00:04:59 15 +2000-01-01 00:09:59 40 +2000-01-01 00:14:59 11 +Freq: 5T, dtype: int64 +``` + +#### 开盘-最高-最低-收盘(OHLC)重新采样 + +在金融领域,聚合时间序列的一种流行方式是为每个桶计算四个值:第一个(开盘)、最后一个(收盘)、最大值(最高)和最小值(最低)。通过使用`ohlc`聚合函数,您将获得一个包含这四个聚合值的列的 DataFrame,这四个值可以在单个函数调用中高效计算: + +```py +In [223]: ts = pd.Series(np.random.permutation(np.arange(len(dates))), index=date +s) + +In [224]: ts.resample("5min").ohlc() +Out[224]: + open high low close +2000-01-01 00:00:00 8 8 1 5 +2000-01-01 00:05:00 6 11 2 2 +2000-01-01 00:10:00 0 7 0 7 +``` + +### 上采样和插值 + +上采样是将数据从较低频率转换为较高频率,不需要聚合。让我们考虑一个包含一些周数据的 DataFrame: + +```py +In [225]: frame = pd.DataFrame(np.random.standard_normal((2, 4)), + .....: index=pd.date_range("2000-01-01", periods=2, + .....: freq="W-WED"), + .....: columns=["Colorado", "Texas", "New York", "Ohio"]) + +In [226]: frame +Out[226]: + Colorado Texas New York Ohio +2000-01-05 -0.896431 0.927238 0.482284 -0.867130 +2000-01-12 0.493841 -0.155434 1.397286 1.507055 +``` + +当您使用聚合函数处理这些数据时,每组只有一个值,缺失值会导致间隙。我们使用`asfreq`方法将其转换为更高的频率,而不进行任何聚合: + +```py +In [227]: df_daily = frame.resample("D").asfreq() + +In [228]: df_daily +Out[228]: + Colorado Texas New York Ohio +2000-01-05 -0.896431 0.927238 0.482284 -0.867130 +2000-01-06 NaN NaN NaN NaN +2000-01-07 NaN NaN NaN NaN +2000-01-08 NaN NaN NaN NaN +2000-01-09 NaN NaN NaN NaN +2000-01-10 NaN NaN NaN NaN +2000-01-11 NaN NaN NaN NaN +2000-01-12 0.493841 -0.155434 1.397286 1.507055 +``` + +假设您希望在非星期三填充每周值。与`fillna`和`reindex`方法中可用的填充或插值方法相同,对于重新采样也是可用的: + +```py +In [229]: frame.resample("D").ffill() +Out[229]: + Colorado Texas New York Ohio +2000-01-05 -0.896431 0.927238 0.482284 -0.867130 +2000-01-06 -0.896431 0.927238 0.482284 -0.867130 +2000-01-07 -0.896431 0.927238 0.482284 -0.867130 +2000-01-08 -0.896431 0.927238 0.482284 -0.867130 +2000-01-09 -0.896431 0.927238 0.482284 -0.867130 +2000-01-10 -0.896431 0.927238 0.482284 -0.867130 +2000-01-11 -0.896431 0.927238 0.482284 -0.867130 +2000-01-12 0.493841 -0.155434 1.397286 1.507055 +``` + +您也可以选择仅填充一定数量的周期,以限制使用观察值的范围: + +```py +In [230]: frame.resample("D").ffill(limit=2) +Out[230]: + Colorado Texas New York Ohio +2000-01-05 -0.896431 0.927238 0.482284 -0.867130 +2000-01-06 -0.896431 0.927238 0.482284 -0.867130 +2000-01-07 -0.896431 0.927238 0.482284 -0.867130 +2000-01-08 NaN NaN NaN NaN +2000-01-09 NaN NaN NaN NaN +2000-01-10 NaN NaN NaN NaN +2000-01-11 NaN NaN NaN NaN +2000-01-12 0.493841 -0.155434 1.397286 1.507055 +``` + +值得注意的是,新的日期索引不一定与旧的完全重合: + +```py +In [231]: frame.resample("W-THU").ffill() +Out[231]: + Colorado Texas New York Ohio +2000-01-06 -0.896431 0.927238 0.482284 -0.867130 +2000-01-13 0.493841 -0.155434 1.397286 1.507055 +``` + +### 使用周期重新采样 + +按周期索引的数据重新采样类似于时间戳: + +```py +In [232]: frame = pd.DataFrame(np.random.standard_normal((24, 4)), + .....: index=pd.period_range("1-2000", "12-2001", + .....: freq="M"), + .....: columns=["Colorado", "Texas", "New York", "Ohio"]) + +In [233]: frame.head() +Out[233]: + Colorado Texas New York Ohio +2000-01 -1.179442 0.443171 1.395676 -0.529658 +2000-02 0.787358 0.248845 0.743239 1.267746 +2000-03 1.302395 -0.272154 -0.051532 -0.467740 +2000-04 -1.040816 0.426419 0.312945 -1.115689 +2000-05 1.234297 -1.893094 -1.661605 -0.005477 + +In [234]: annual_frame = frame.resample("A-DEC").mean() + +In [235]: annual_frame +Out[235]: + Colorado Texas New York Ohio +2000 0.487329 0.104466 0.020495 -0.273945 +2001 0.203125 0.162429 0.056146 -0.103794 +``` + +上采样更加微妙,因为在重新采样之前,您必须决定将值放在新频率的时间跨度的哪一端。`convention`参数默认为`"start"`,但也可以是`"end"`: + +```py +# Q-DEC: Quarterly, year ending in December +In [236]: annual_frame.resample("Q-DEC").ffill() +Out[236]: + Colorado Texas New York Ohio +2000Q1 0.487329 0.104466 0.020495 -0.273945 +2000Q2 0.487329 0.104466 0.020495 -0.273945 +2000Q3 0.487329 0.104466 0.020495 -0.273945 +2000Q4 0.487329 0.104466 0.020495 -0.273945 +2001Q1 0.203125 0.162429 0.056146 -0.103794 +2001Q2 0.203125 0.162429 0.056146 -0.103794 +2001Q3 0.203125 0.162429 0.056146 -0.103794 +2001Q4 0.203125 0.162429 0.056146 -0.103794 + +In [237]: annual_frame.resample("Q-DEC", convention="end").asfreq() +Out[237]: + Colorado Texas New York Ohio +2000Q4 0.487329 0.104466 0.020495 -0.273945 +2001Q1 NaN NaN NaN NaN +2001Q2 NaN NaN NaN NaN +2001Q3 NaN NaN NaN NaN +2001Q4 0.203125 0.162429 0.056146 -0.103794 +``` + +由于周期指的是时间跨度,因此有关上采样和下采样的规则更为严格: + ++ 在下采样中,目标频率必须是源频率的*子周期*。 + ++ 在上采样中,目标频率必须是源频率的*超周期*。 + +如果这些规则不满足,将会引发异常。这主要影响季度、年度和每周频率;例如,由`Q-MAR`定义的时间跨度只与`A-MAR`、`A-JUN`、`A-SEP`和`A-DEC`对齐: + +```py +In [238]: annual_frame.resample("Q-MAR").ffill() +Out[238]: + Colorado Texas New York Ohio +2000Q4 0.487329 0.104466 0.020495 -0.273945 +2001Q1 0.487329 0.104466 0.020495 -0.273945 +2001Q2 0.487329 0.104466 0.020495 -0.273945 +2001Q3 0.487329 0.104466 0.020495 -0.273945 +2001Q4 0.203125 0.162429 0.056146 -0.103794 +2002Q1 0.203125 0.162429 0.056146 -0.103794 +2002Q2 0.203125 0.162429 0.056146 -0.103794 +2002Q3 0.203125 0.162429 0.056146 -0.103794 +``` + +### 分组时间重采样 + +对于时间序列数据,`resample`方法在时间间隔化的基础上是一个组操作。这里是一个小例子表: + +```py +In [239]: N = 15 + +In [240]: times = pd.date_range("2017-05-20 00:00", freq="1min", periods=N) + +In [241]: df = pd.DataFrame({"time": times, + .....: "value": np.arange(N)}) + +In [242]: df +Out[242]: + time value +0 2017-05-20 00:00:00 0 +1 2017-05-20 00:01:00 1 +2 2017-05-20 00:02:00 2 +3 2017-05-20 00:03:00 3 +4 2017-05-20 00:04:00 4 +5 2017-05-20 00:05:00 5 +6 2017-05-20 00:06:00 6 +7 2017-05-20 00:07:00 7 +8 2017-05-20 00:08:00 8 +9 2017-05-20 00:09:00 9 +10 2017-05-20 00:10:00 10 +11 2017-05-20 00:11:00 11 +12 2017-05-20 00:12:00 12 +13 2017-05-20 00:13:00 13 +14 2017-05-20 00:14:00 14 +``` + +在这里,我们可以按`"time"`索引,然后重采样: + +```py +In [243]: df.set_index("time").resample("5min").count() +Out[243]: + value +time +2017-05-20 00:00:00 5 +2017-05-20 00:05:00 5 +2017-05-20 00:10:00 5 +``` + +假设一个 DataFrame 包含多个时间序列,由额外的分组键列标记: + +```py +In [244]: df2 = pd.DataFrame({"time": times.repeat(3), + .....: "key": np.tile(["a", "b", "c"], N), + .....: "value": np.arange(N * 3.)}) + +In [245]: df2.head(7) +Out[245]: + time key value +0 2017-05-20 00:00:00 a 0.0 +1 2017-05-20 00:00:00 b 1.0 +2 2017-05-20 00:00:00 c 2.0 +3 2017-05-20 00:01:00 a 3.0 +4 2017-05-20 00:01:00 b 4.0 +5 2017-05-20 00:01:00 c 5.0 +6 2017-05-20 00:02:00 a 6.0 +``` + +为了对每个`"key"`值执行相同的重采样,我们引入`pandas.Grouper`对象: + +```py +In [246]: time_key = pd.Grouper(freq="5min") +``` + +然后我们可以设置时间索引,按`"key"`和`time_key`分组,并进行聚合: + +```py +In [247]: resampled = (df2.set_index("time") + .....: .groupby(["key", time_key]) + .....: .sum()) + +In [248]: resampled +Out[248]: + value +key time +a 2017-05-20 00:00:00 30.0 + 2017-05-20 00:05:00 105.0 + 2017-05-20 00:10:00 180.0 +b 2017-05-20 00:00:00 35.0 + 2017-05-20 00:05:00 110.0 + 2017-05-20 00:10:00 185.0 +c 2017-05-20 00:00:00 40.0 + 2017-05-20 00:05:00 115.0 + 2017-05-20 00:10:00 190.0 + +In [249]: resampled.reset_index() +Out[249]: + key time value +0 a 2017-05-20 00:00:00 30.0 +1 a 2017-05-20 00:05:00 105.0 +2 a 2017-05-20 00:10:00 180.0 +3 b 2017-05-20 00:00:00 35.0 +4 b 2017-05-20 00:05:00 110.0 +5 b 2017-05-20 00:10:00 185.0 +6 c 2017-05-20 00:00:00 40.0 +7 c 2017-05-20 00:05:00 115.0 +8 c 2017-05-20 00:10:00 190.0 +``` + +使用`pandas.Grouper`的一个限制是时间必须是 Series 或 DataFrame 的索引。 + +## 11.7 移动窗口函数 + +用于时间序列操作的一类重要的数组转换是在滑动窗口上评估统计数据和其他函数,或者使用指数衰减权重。这对于平滑嘈杂或有缺失数据的数据很有用。我将这些称为*移动窗口函数*,尽管它们包括没有固定长度窗口的函数,比如指数加权移动平均。与其他统计函数一样,这些函数也会自动排除缺失数据。 + +在深入研究之前,我们可以加载一些时间序列数据并将其重采样为工作日频率: + +```py +In [250]: close_px_all = pd.read_csv("examples/stock_px.csv", + .....: parse_dates=True, index_col=0) + +In [251]: close_px = close_px_all[["AAPL", "MSFT", "XOM"]] + +In [252]: close_px = close_px.resample("B").ffill() +``` + +我现在介绍`rolling`运算符,它的行为类似于`resample`和`groupby`。它可以与一个`window`(表示为一定数量的周期)一起在 Series 或 DataFrame 上调用(请参见 Apple 价格与 250 日移动平均创建的图): + +```py +In [253]: close_px["AAPL"].plot() +Out[253]: + +In [254]: close_px["AAPL"].rolling(250).mean().plot() +``` + +![](img/592f0eb154cb8af5292d2a938f2b873c.png) + +图 11.4:苹果价格与 250 日移动平均值 + +表达式`rolling(250)`在行为上类似于`groupby`,但不是分组,而是创建一个对象,使得可以在 250 天滑动窗口上进行分组。因此,这里是苹果股价的 250 日移动窗口平均值。 + +默认情况下,滚动函数要求窗口中的所有值都不是 NA。这种行为可以更改以考虑缺失数据,特别是在时间序列开始时将少于`window`周期的数据(请参见苹果 250 日每日回报标准差): + +```py +In [255]: plt.figure() +Out[255]:
+ +In [256]: std250 = close_px["AAPL"].pct_change().rolling(250, min_periods=10).std +() + +In [257]: std250[5:12] +Out[257]: +2003-01-09 NaN +2003-01-10 NaN +2003-01-13 NaN +2003-01-14 NaN +2003-01-15 NaN +2003-01-16 0.009628 +2003-01-17 0.013818 +Freq: B, Name: AAPL, dtype: float64 + +In [258]: std250.plot() +``` + +![](img/d8892398961900d39bbbcf3f01d8da4c.png) + +图 11.5:苹果 250 日每日回报标准差 + +要计算*扩展窗口均值*,请使用`expanding`运算符,而不是`rolling`。扩展均值从与滚动窗口相同的时间窗口开始,并增加窗口的大小,直到包含整个系列。`std250`时间序列上的扩展窗口均值如下所示: + +```py +In [259]: expanding_mean = std250.expanding().mean() +``` + +在 DataFrame 上调用移动窗口函数会将转换应用于每一列(请参见股价 60 日移动平均(对数 y 轴)): + +```py +In [261]: plt.style.use('grayscale') + +In [262]: close_px.rolling(60).mean().plot(logy=True) +``` + +![](img/e8f4e446ffcb7dd48b59497f44f85dd4.png) + +图 11.6:股价 60 日移动平均(对数 y 轴) + +`rolling`函数还接受一个字符串,指示固定大小的时间偏移`rolling()`在移动窗口函数中,而不是一组周期。使用这种表示法对于不规则的时间序列很有用。这些是您可以传递给`resample`的相同字符串。例如,我们可以这样计算 20 天的滚动均值: + +```py +In [263]: close_px.rolling("20D").mean() +Out[263]: + AAPL MSFT XOM +2003-01-02 7.400000 21.110000 29.220000 +2003-01-03 7.425000 21.125000 29.230000 +2003-01-06 7.433333 21.256667 29.473333 +2003-01-07 7.432500 21.425000 29.342500 +2003-01-08 7.402000 21.402000 29.240000 +... ... ... ... +2011-10-10 389.351429 25.602143 72.527857 +2011-10-11 388.505000 25.674286 72.835000 +2011-10-12 388.531429 25.810000 73.400714 +2011-10-13 388.826429 25.961429 73.905000 +2011-10-14 391.038000 26.048667 74.185333 +[2292 rows x 3 columns] +``` + +### 指数加权函数 + +使用固定窗口大小和等权观测值的替代方法是指定一个恒定的*衰减因子*,以赋予更多权重给最近的观测值。有几种指定衰减因子的方法。一种流行的方法是使用*跨度*,使结果与窗口大小等于跨度的简单移动窗口函数可比较。 + +由于指数加权统计对最近的观察结果赋予更大的权重,与等权重版本相比,它更快地“适应”变化。 + +pandas 有`ewm`运算符(代表指数加权移动),与`rolling`和`expanding`配合使用。以下是一个示例,比较了苹果公司股价的 30 天移动平均值与指数加权(EW)移动平均值(`span=60`)(请参阅简单移动平均与指数加权): + +```py +In [265]: aapl_px = close_px["AAPL"]["2006":"2007"] + +In [266]: ma30 = aapl_px.rolling(30, min_periods=20).mean() + +In [267]: ewma30 = aapl_px.ewm(span=30).mean() + +In [268]: aapl_px.plot(style="k-", label="Price") +Out[268]: + +In [269]: ma30.plot(style="k--", label="Simple Moving Avg") +Out[269]: + +In [270]: ewma30.plot(style="k-", label="EW MA") +Out[270]: + +In [271]: plt.legend() +``` + +![](img/597f66552cc2eb32618fc4fb8471c04d.png) + +图 11.7:简单移动平均与指数加权 + +### 二进制移动窗口函数 + +一些统计运算符,如相关性和协方差,需要在两个时间序列上操作。例如,金融分析师通常对股票与标普 500 等基准指数的相关性感兴趣。为了查看这一点,我们首先计算所有感兴趣时间序列的百分比变化: + +```py +In [273]: spx_px = close_px_all["SPX"] + +In [274]: spx_rets = spx_px.pct_change() + +In [275]: returns = close_px.pct_change() +``` + +在我们调用`rolling`之后,`corr`聚合函数可以计算与`spx_rets`的滚动相关性(请参阅苹果公司六个月回报与标普 500 的相关性以查看结果图): + +```py +In [276]: corr = returns["AAPL"].rolling(125, min_periods=100).corr(spx_rets) + +In [277]: corr.plot() +``` + +![](img/071a15ee104f8b18492d67565b62e3bc.png) + +图 11.8:苹果公司六个月回报与标普 500 的相关性 + +假设您想要计算 S&P 500 指数与多只股票的滚动相关性。您可以像我们上面为苹果公司所做的那样编写一个循环来计算每只股票的相关性,但如果每只股票是单个 DataFrame 中的一列,我们可以通过在 DataFrame 上调用`rolling`并传递`spx_rets` Series 来一次性计算所有滚动相关性。 + +请参阅与标普 500 的六个月回报相关性以查看结果图: + +```py +In [279]: corr = returns.rolling(125, min_periods=100).corr(spx_rets) + +In [280]: corr.plot() +``` + +![](img/898f6a6974d281ac4a87524fcabc2566.png) + +图 11.9:与标普 500 的六个月回报相关性 + +### 用户定义的移动窗口函数 + +`rolling`和相关方法上的`apply`方法提供了一种方法,可以在移动窗口上应用自己创建的数组函数。唯一的要求是函数从数组的每个部分产生一个单一值(一个减少)。例如,虽然我们可以使用`rolling(...).quantile(q)`计算样本分位数,但我们可能对特定值在样本中的百分位数感兴趣。`scipy.stats.percentileofscore`函数正是这样做的(请参阅 2%苹果公司回报在一年窗口内的百分位数以查看结果图): + +```py +In [282]: from scipy.stats import percentileofscore + +In [283]: def score_at_2percent(x): + .....: return percentileofscore(x, 0.02) + +In [284]: result = returns["AAPL"].rolling(250).apply(score_at_2percent) + +In [285]: result.plot() +``` + +![](img/b686da0f6b499292c1b338fdebcddfef.png) + +图 11.10:2%苹果公司回报在一年窗口内的百分位数 + +如果您尚未安装 SciPy,可以使用 conda 或 pip 进行安装: + +```py +conda install scipy +``` + +## 11.8 结论 + +时间序列数据需要不同类型的分析和数据转换工具,与我们在之前章节中探讨过的其他类型数据不同。 + +在接下来的章节中,我们将展示如何开始使用建模库,如 statsmodels 和 scikit-learn。 + +* * * + +1. 对于`closed`和`label`的默认值选择可能对一些用户来说有点奇怪。默认值为`closed="left"`,除了一组特定的值(`"M"`、`"A"`、`"Q"`、`"BM"`、`"BQ"`和`"W"`)默认为`closed="right"`。选择默认值是为了使结果更直观,但值得知道默认值并不总是一个或另一个。 diff --git a/translations/cn/pyda3e_15.md b/translations/cn/pyda3e_15.md new file mode 100644 index 000000000..d9c316ce0 --- /dev/null +++ b/translations/cn/pyda3e_15.md @@ -0,0 +1,930 @@ +# 十二、Python 建模库介绍 + +> 原文:[`wesmckinney.com/book/modeling`](https://wesmckinney.com/book/modeling) +> +> 译者:[飞龙](https://github.com/wizardforcel) +> +> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) + + +> 此开放访问网络版本的《Python 数据分析第三版》现已作为[印刷版和数字版](https://amzn.to/3DyLaJc)的伴侣提供。如果您发现任何勘误,请[在此处报告](https://oreilly.com/catalog/0636920519829/errata)。请注意,由 Quarto 生成的本站点的某些方面与 O'Reilly 的印刷版和电子书版本的格式不同。 +> +> 如果您发现本书的在线版本有用,请考虑[订购纸质版](https://amzn.to/3DyLaJc)或[无 DRM 的电子书](https://www.ebooks.com/en-us/book/210644288/python-for-data-analysis/wes-mckinney/?affId=WES398681F)以支持作者。本网站的内容不得复制或再生产。代码示例采用 MIT 许可,可在 GitHub 或 Gitee 上找到。 + +在本书中,我专注于为在 Python 中进行数据分析提供编程基础。由于数据分析师和科学家经常报告花费大量时间进行数据整理和准备,因此本书的结构反映了掌握这些技术的重要性。 + +您用于开发模型的库将取决于应用程序。许多统计问题可以通过简单的技术解决,如普通最小二乘回归,而其他问题可能需要更高级的机器学习方法。幸运的是,Python 已经成为实现分析方法的首选语言之一,因此在完成本书后,您可以探索许多工具。 + +在本章中,我将回顾一些 pandas 的特性,这些特性在您在 pandas 中进行数据整理和模型拟合和评分之间来回切换时可能会有所帮助。然后,我将简要介绍两个流行的建模工具包,[statsmodels](http://statsmodels.org)和[scikit-learn](http://scikit-learn.org)。由于这两个项目都足够庞大,值得有自己的专门书籍,因此我没有尝试全面介绍,而是建议您查阅这两个项目的在线文档,以及一些其他基于 Python 的数据科学、统计学和机器学习书籍。 + +## 12.1 pandas 与模型代码之间的接口 + +模型开发的常见工作流程是使用 pandas 进行数据加载和清理,然后切换到建模库来构建模型本身。模型开发过程中的一个重要部分被称为*特征工程*,在机器学习中。这可以描述从原始数据集中提取信息的任何数据转换或分析,这些信息在建模环境中可能有用。我们在本书中探讨的数据聚合和 GroupBy 工具经常在特征工程环境中使用。 + +虽然“好”的特征工程的细节超出了本书的范围,但我将展示一些方法,使在 pandas 中进行数据操作和建模之间的切换尽可能轻松。 + +pandas 与其他分析库之间的接触点通常是 NumPy 数组。要将 DataFrame 转换为 NumPy 数组,请使用`to_numpy`方法: + +```py +In [12]: data = pd.DataFrame({ + ....: 'x0': [1, 2, 3, 4, 5], + ....: 'x1': [0.01, -0.01, 0.25, -4.1, 0.], + ....: 'y': [-1.5, 0., 3.6, 1.3, -2.]}) + +In [13]: data +Out[13]: + x0 x1 y +0 1 0.01 -1.5 +1 2 -0.01 0.0 +2 3 0.25 3.6 +3 4 -4.10 1.3 +4 5 0.00 -2.0 + +In [14]: data.columns +Out[14]: Index(['x0', 'x1', 'y'], dtype='object') + +In [15]: data.to_numpy() +Out[15]: +array([[ 1. , 0.01, -1.5 ], + [ 2. , -0.01, 0. ], + [ 3. , 0.25, 3.6 ], + [ 4. , -4.1 , 1.3 ], + [ 5. , 0. , -2. ]]) +``` + +回到 DataFrame,正如您可能从前几章中记得的那样,您可以传递一个二维的 ndarray,其中包含可选的列名: + +```py +In [16]: df2 = pd.DataFrame(data.to_numpy(), columns=['one', 'two', 'three']) + +In [17]: df2 +Out[17]: + one two three +0 1.0 0.01 -1.5 +1 2.0 -0.01 0.0 +2 3.0 0.25 3.6 +3 4.0 -4.10 1.3 +4 5.0 0.00 -2.0 +``` + +`to_numpy`方法旨在在数据是同质的情况下使用,例如所有的数值类型。如果您有异构数据,结果将是一个 Python 对象的 ndarray: + +```py +In [18]: df3 = data.copy() + +In [19]: df3['strings'] = ['a', 'b', 'c', 'd', 'e'] + +In [20]: df3 +Out[20]: + x0 x1 y strings +0 1 0.01 -1.5 a +1 2 -0.01 0.0 b +2 3 0.25 3.6 c +3 4 -4.10 1.3 d +4 5 0.00 -2.0 e + +In [21]: df3.to_numpy() +Out[21]: +array([[1, 0.01, -1.5, 'a'], + [2, -0.01, 0.0, 'b'], + [3, 0.25, 3.6, 'c'], + [4, -4.1, 1.3, 'd'], + [5, 0.0, -2.0, 'e']], dtype=object) +``` + +对于某些模型,您可能希望仅使用部分列。我建议使用`loc`索引和`to_numpy`: + +```py +In [22]: model_cols = ['x0', 'x1'] + +In [23]: data.loc[:, model_cols].to_numpy() +Out[23]: +array([[ 1. , 0.01], + [ 2. , -0.01], + [ 3. , 0.25], + [ 4. , -4.1 ], + [ 5. , 0. ]]) +``` + +一些库原生支持 pandas,并自动完成一些工作:从 DataFrame 转换为 NumPy,并将模型参数名称附加到输出表或 Series 的列上。在其他情况下,您将不得不手动执行这种“元数据管理”。 + +在 Ch 7.5:分类数据中,我们看过 pandas 的`Categorical`类型和`pandas.get_dummies`函数。假设我们的示例数据集中有一个非数字列: + +```py +In [24]: data['category'] = pd.Categorical(['a', 'b', 'a', 'a', 'b'], + ....: categories=['a', 'b']) + +In [25]: data +Out[25]: + x0 x1 y category +0 1 0.01 -1.5 a +1 2 -0.01 0.0 b +2 3 0.25 3.6 a +3 4 -4.10 1.3 a +4 5 0.00 -2.0 b +``` + +如果我们想用虚拟变量替换`'category'`列,我们创建虚拟变量,删除`'category'`列,然后将结果连接: + +```py +In [26]: dummies = pd.get_dummies(data.category, prefix='category', + ....: dtype=float) + +In [27]: data_with_dummies = data.drop('category', axis=1).join(dummies) + +In [28]: data_with_dummies +Out[28]: + x0 x1 y category_a category_b +0 1 0.01 -1.5 1.0 0.0 +1 2 -0.01 0.0 0.0 1.0 +2 3 0.25 3.6 1.0 0.0 +3 4 -4.10 1.3 1.0 0.0 +4 5 0.00 -2.0 0.0 1.0 +``` + +使用虚拟变量拟合某些统计模型时存在一些微妙之处。当您拥有不仅仅是简单数字列时,使用 Patsy(下一节的主题)可能更简单且更不容易出错。 + +## 12.2 使用 Patsy 创建模型描述 + +[Patsy](https://patsy.readthedocs.io/)是一个用于描述统计模型(尤其是线性模型)的 Python 库,它使用基于字符串的“公式语法”,受到 R 和 S 统计编程语言使用的公式语法的启发(但并非完全相同)。在安装 statsmodels 时会自动安装它: + +```py +conda install statsmodels +``` + +Patsy 在为 statsmodels 指定线性模型方面得到很好的支持,因此我将重点介绍一些主要功能,以帮助您快速上手。Patsy 的*公式*是一种特殊的字符串语法,看起来像: + +```py +y ~ x0 + x1 +``` + +语法`a + b`并不意味着将`a`加到`b`,而是这些是为模型创建的*设计矩阵*中的*项*。`patsy.dmatrices`函数接受一个公式字符串以及一个数据集(可以是 DataFrame 或数组字典),并为线性模型生成设计矩阵: + +```py +In [29]: data = pd.DataFrame({ + ....: 'x0': [1, 2, 3, 4, 5], + ....: 'x1': [0.01, -0.01, 0.25, -4.1, 0.], + ....: 'y': [-1.5, 0., 3.6, 1.3, -2.]}) + +In [30]: data +Out[30]: + x0 x1 y +0 1 0.01 -1.5 +1 2 -0.01 0.0 +2 3 0.25 3.6 +3 4 -4.10 1.3 +4 5 0.00 -2.0 + +In [31]: import patsy + +In [32]: y, X = patsy.dmatrices('y ~ x0 + x1', data) +``` + +现在我们有: + +```py +In [33]: y +Out[33]: +DesignMatrix with shape (5, 1) + y + -1.5 + 0.0 + 3.6 + 1.3 + -2.0 + Terms: + 'y' (column 0) + +In [34]: X +Out[34]: +DesignMatrix with shape (5, 3) + Intercept x0 x1 + 1 1 0.01 + 1 2 -0.01 + 1 3 0.25 + 1 4 -4.10 + 1 5 0.00 + Terms: + 'Intercept' (column 0) + 'x0' (column 1) + 'x1' (column 2) +``` + +这些 Patsy `DesignMatrix`实例是带有附加元数据的 NumPy ndarrays: + +```py +In [35]: np.asarray(y) +Out[35]: +array([[-1.5], + [ 0. ], + [ 3.6], + [ 1.3], + [-2. ]]) + +In [36]: np.asarray(X) +Out[36]: +array([[ 1. , 1. , 0.01], + [ 1. , 2. , -0.01], + [ 1. , 3. , 0.25], + [ 1. , 4. , -4.1 ], + [ 1. , 5. , 0. ]]) +``` + +您可能会想知道`Intercept`项是从哪里来的。这是线性模型(如普通最小二乘回归)的一个约定。您可以通过在模型中添加`+ 0`项来抑制截距: + +```py +In [37]: patsy.dmatrices('y ~ x0 + x1 + 0', data)[1] +Out[37]: +DesignMatrix with shape (5, 2) + x0 x1 + 1 0.01 + 2 -0.01 + 3 0.25 + 4 -4.10 + 5 0.00 + Terms: + 'x0' (column 0) + 'x1' (column 1) +``` + +Patsy 对象可以直接传递到像`numpy.linalg.lstsq`这样的算法中,该算法执行普通最小二乘回归: + +```py +In [38]: coef, resid, _, _ = np.linalg.lstsq(X, y, rcond=None) +``` + +模型元数据保留在`design_info`属性中,因此您可以重新附加模型列名称到拟合系数以获得一个 Series,例如: + +```py +In [39]: coef +Out[39]: +array([[ 0.3129], + [-0.0791], + [-0.2655]]) + +In [40]: coef = pd.Series(coef.squeeze(), index=X.design_info.column_names) + +In [41]: coef +Out[41]: +Intercept 0.312910 +x0 -0.079106 +x1 -0.265464 +dtype: float64 +``` + +### Patsy 公式中的数据转换 + +您可以将 Python 代码混合到您的 Patsy 公式中;在评估公式时,库将尝试在封闭范围中找到您使用的函数: + +```py +In [42]: y, X = patsy.dmatrices('y ~ x0 + np.log(np.abs(x1) + 1)', data) + +In [43]: X +Out[43]: +DesignMatrix with shape (5, 3) + Intercept x0 np.log(np.abs(x1) + 1) + 1 1 0.00995 + 1 2 0.00995 + 1 3 0.22314 + 1 4 1.62924 + 1 5 0.00000 + Terms: + 'Intercept' (column 0) + 'x0' (column 1) + 'np.log(np.abs(x1) + 1)' (column 2) +``` + +一些常用的变量转换包括*标准化*(均值为 0,方差为 1)和*中心化*(减去均值)。Patsy 具有内置函数用于此目的: + +```py +In [44]: y, X = patsy.dmatrices('y ~ standardize(x0) + center(x1)', data) + +In [45]: X +Out[45]: +DesignMatrix with shape (5, 3) + Intercept standardize(x0) center(x1) + 1 -1.41421 0.78 + 1 -0.70711 0.76 + 1 0.00000 1.02 + 1 0.70711 -3.33 + 1 1.41421 0.77 + Terms: + 'Intercept' (column 0) + 'standardize(x0)' (column 1) + 'center(x1)' (column 2) +``` + +作为建模过程的一部分,您可以在一个数据集上拟合模型,然后基于另一个数据集评估模型。这可能是一个*保留*部分或稍后观察到的新数据。当应用诸如中心化和标准化之类的转换时,您在使用模型基于新数据形成预测时应当小心。这些被称为*有状态*转换,因为在转换新数据时必须使用原始数据集的统计数据,如均值或标准差。 + +`patsy.build_design_matrices`函数可以使用原始*样本内*数据的保存信息对新的*样本外*数据应用转换: + +```py +In [46]: new_data = pd.DataFrame({ + ....: 'x0': [6, 7, 8, 9], + ....: 'x1': [3.1, -0.5, 0, 2.3], + ....: 'y': [1, 2, 3, 4]}) + +In [47]: new_X = patsy.build_design_matrices([X.design_info], new_data) + +In [48]: new_X +Out[48]: +[DesignMatrix with shape (4, 3) + Intercept standardize(x0) center(x1) + 1 2.12132 3.87 + 1 2.82843 0.27 + 1 3.53553 0.77 + 1 4.24264 3.07 + Terms: + 'Intercept' (column 0) + 'standardize(x0)' (column 1) + 'center(x1)' (column 2)] +``` + +因为 Patsy 公式中加号(`+`)并不表示加法,所以当您想按名称从数据集中添加列时,您必须将它们包装在特殊的`I`函数中: + +```py +In [49]: y, X = patsy.dmatrices('y ~ I(x0 + x1)', data) + +In [50]: X +Out[50]: +DesignMatrix with shape (5, 2) + Intercept I(x0 + x1) + 1 1.01 + 1 1.99 + 1 3.25 + 1 -0.10 + 1 5.00 + Terms: + 'Intercept' (column 0) + 'I(x0 + x1)' (column 1) +``` + +Patsy 在`patsy.builtins`模块中还有几个内置转换。请查看在线文档以获取更多信息。 + +分类数据有一类特殊的转换,接下来我会解释。 + +### 分类数据和 Patsy + +非数字数据可以以多种不同的方式转换为模型设计矩阵。本书不涉及这个主题的完整处理,最好是在统计课程中学习。 + +当您在 Patsy 公式中使用非数字术语时,默认情况下它们会被转换为虚拟变量。如果有一个截距,将会有一个级别被排除以避免共线性: + +```py +In [51]: data = pd.DataFrame({ + ....: 'key1': ['a', 'a', 'b', 'b', 'a', 'b', 'a', 'b'], + ....: 'key2': [0, 1, 0, 1, 0, 1, 0, 0], + ....: 'v1': [1, 2, 3, 4, 5, 6, 7, 8], + ....: 'v2': [-1, 0, 2.5, -0.5, 4.0, -1.2, 0.2, -1.7] + ....: }) + +In [52]: y, X = patsy.dmatrices('v2 ~ key1', data) + +In [53]: X +Out[53]: +DesignMatrix with shape (8, 2) + Intercept key1[T.b] + 1 0 + 1 0 + 1 1 + 1 1 + 1 0 + 1 1 + 1 0 + 1 1 + Terms: + 'Intercept' (column 0) + 'key1' (column 1) +``` + +如果从模型中省略截距,那么每个类别值的列将包含在模型设计矩阵中: + +```py +In [54]: y, X = patsy.dmatrices('v2 ~ key1 + 0', data) + +In [55]: X +Out[55]: +DesignMatrix with shape (8, 2) + key1[a] key1[b] + 1 0 + 1 0 + 0 1 + 0 1 + 1 0 + 0 1 + 1 0 + 0 1 + Terms: + 'key1' (columns 0:2) +``` + +数值列可以使用`C`函数解释为分类列: + +```py +In [56]: y, X = patsy.dmatrices('v2 ~ C(key2)', data) + +In [57]: X +Out[57]: +DesignMatrix with shape (8, 2) + Intercept C(key2)[T.1] + 1 0 + 1 1 + 1 0 + 1 1 + 1 0 + 1 1 + 1 0 + 1 0 + Terms: + 'Intercept' (column 0) + 'C(key2)' (column 1) +``` + +当您在模型中使用多个分类项时,情况可能会更加复杂,因为您可以包括形式为`key1:key2`的交互项,例如在方差分析(ANOVA)模型中使用: + +```py +In [58]: data['key2'] = data['key2'].map({0: 'zero', 1: 'one'}) + +In [59]: data +Out[59]: + key1 key2 v1 v2 +0 a zero 1 -1.0 +1 a one 2 0.0 +2 b zero 3 2.5 +3 b one 4 -0.5 +4 a zero 5 4.0 +5 b one 6 -1.2 +6 a zero 7 0.2 +7 b zero 8 -1.7 + +In [60]: y, X = patsy.dmatrices('v2 ~ key1 + key2', data) + +In [61]: X +Out[61]: +DesignMatrix with shape (8, 3) + Intercept key1[T.b] key2[T.zero] + 1 0 1 + 1 0 0 + 1 1 1 + 1 1 0 + 1 0 1 + 1 1 0 + 1 0 1 + 1 1 1 + Terms: + 'Intercept' (column 0) + 'key1' (column 1) + 'key2' (column 2) + +In [62]: y, X = patsy.dmatrices('v2 ~ key1 + key2 + key1:key2', data) + +In [63]: X +Out[63]: +DesignMatrix with shape (8, 4) + Intercept key1[T.b] key2[T.zero] key1[T.b]:key2[T.zero] + 1 0 1 0 + 1 0 0 0 + 1 1 1 1 + 1 1 0 0 + 1 0 1 0 + 1 1 0 0 + 1 0 1 0 + 1 1 1 1 + Terms: + 'Intercept' (column 0) + 'key1' (column 1) + 'key2' (column 2) + 'key1:key2' (column 3) +``` + +Patsy 提供了其他转换分类数据的方法,包括具有特定顺序的项的转换。有关更多信息,请参阅在线文档。 + +## 12.3 statsmodels 简介 + +[statsmodels](http://www.statsmodels.org)是一个用于拟合许多种统计模型、执行统计检验以及数据探索和可视化的 Python 库。statsmodels 包含更多“经典”的频率统计方法,而贝叶斯方法和机器学习模型则在其他库中找到。 + +在 statsmodels 中找到的一些模型类型包括: + ++ 线性模型、广义线性模型和鲁棒线性模型 + ++ 线性混合效应模型 + ++ 方差分析(ANOVA)方法 + ++ 时间序列过程和状态空间模型 + ++ 广义矩估计法 + +在接下来的几页中,我们将使用 statsmodels 中的一些基本工具,并探索如何使用 Patsy 公式和 pandas DataFrame 对象的建模接口。如果您之前在 Patsy 讨论中没有安装 statsmodels,现在可以使用以下命令进行安装: + +```py +conda install statsmodels +``` + +### 估计线性模型 + +statsmodels 中有几种线性回归模型,从更基本的(例如普通最小二乘法)到更复杂的(例如迭代重新加权最小二乘法)。 + +statsmodels 中的线性模型有两种不同的主要接口:基于数组和基于公式。可以通过以下 API 模块导入来访问这些接口: + +```py +import statsmodels.api as sm +import statsmodels.formula.api as smf +``` + +为了展示如何使用这些方法,我们从一些随机数据生成一个线性模型。在 Jupyter 中运行以下代码: + +```py +# To make the example reproducible +rng = np.random.default_rng(seed=12345) + +def dnorm(mean, variance, size=1): + if isinstance(size, int): + size = size, + return mean + np.sqrt(variance) * rng.standard_normal(*size) + +N = 100 +X = np.c_[dnorm(0, 0.4, size=N), + dnorm(0, 0.6, size=N), + dnorm(0, 0.2, size=N)] +eps = dnorm(0, 0.1, size=N) +beta = [0.1, 0.3, 0.5] + +y = np.dot(X, beta) + eps +``` + +在这里,我写下了具有已知参数`beta`的“真实”模型。在这种情况下,`dnorm`是一个用于生成具有特定均值和方差的正态分布数据的辅助函数。现在我们有: + +```py +In [66]: X[:5] +Out[66]: +array([[-0.9005, -0.1894, -1.0279], + [ 0.7993, -1.546 , -0.3274], + [-0.5507, -0.1203, 0.3294], + [-0.1639, 0.824 , 0.2083], + [-0.0477, -0.2131, -0.0482]]) + +In [67]: y[:5] +Out[67]: array([-0.5995, -0.5885, 0.1856, -0.0075, -0.0154]) +``` + +通常使用截距项拟合线性模型,就像我们之前在 Patsy 中看到的那样。`sm.add_constant`函数可以向现有矩阵添加一个截距列: + +```py +In [68]: X_model = sm.add_constant(X) + +In [69]: X_model[:5] +Out[69]: +array([[ 1. , -0.9005, -0.1894, -1.0279], + [ 1. , 0.7993, -1.546 , -0.3274], + [ 1. , -0.5507, -0.1203, 0.3294], + [ 1. , -0.1639, 0.824 , 0.2083], + [ 1. , -0.0477, -0.2131, -0.0482]]) +``` + +`sm.OLS`类可以拟合普通最小二乘线性回归: + +```py +In [70]: model = sm.OLS(y, X) +``` + +模型的`fit`方法返回一个包含估计模型参数和其他诊断信息的回归结果对象: + +```py +In [71]: results = model.fit() + +In [72]: results.params +Out[72]: array([0.0668, 0.268 , 0.4505]) +``` + +`results`上的`summary`方法可以打印出模型的诊断输出: + +```py +In [73]: print(results.summary()) +OLS Regression Results +================================================================================= +====== +Dep. Variable: y R-squared (uncentered): + 0.469 +Model: OLS Adj. R-squared (uncentered): + 0.452 +Method: Least Squares F-statistic: + 28.51 +Date: Wed, 12 Apr 2023 Prob (F-statistic): 2. +66e-13 +Time: 13:09:20 Log-Likelihood: - +25.611 +No. Observations: 100 AIC: + 57.22 +Df Residuals: 97 BIC: + 65.04 +Df Model: 3 + +Covariance Type: nonrobust + +============================================================================== + coef std err t P>|t| [0.025 0.975] +------------------------------------------------------------------------------ +x1 0.0668 0.054 1.243 0.217 -0.040 0.174 +x2 0.2680 0.042 6.313 0.000 0.184 0.352 +x3 0.4505 0.068 6.605 0.000 0.315 0.586 +============================================================================== +Omnibus: 0.435 Durbin-Watson: 1.869 +Prob(Omnibus): 0.805 Jarque-Bera (JB): 0.301 +Skew: 0.134 Prob(JB): 0.860 +Kurtosis: 2.995 Cond. No. 1.64 +============================================================================== +Notes: +[1] R² is computed without centering (uncentered) since the model does not contai +n a constant. +[2] Standard Errors assume that the covariance matrix of the errors is correctly +specified. +``` + +这里的参数名称已经被赋予了通用名称`x1, x2`等。假设所有模型参数都在一个 DataFrame 中: + +```py +In [74]: data = pd.DataFrame(X, columns=['col0', 'col1', 'col2']) + +In [75]: data['y'] = y + +In [76]: data[:5] +Out[76]: + col0 col1 col2 y +0 -0.900506 -0.189430 -1.027870 -0.599527 +1 0.799252 -1.545984 -0.327397 -0.588454 +2 -0.550655 -0.120254 0.329359 0.185634 +3 -0.163916 0.824040 0.208275 -0.007477 +4 -0.047651 -0.213147 -0.048244 -0.015374 +``` + +现在我们可以使用 statsmodels 的公式 API 和 Patsy 公式字符串: + +```py +In [77]: results = smf.ols('y ~ col0 + col1 + col2', data=data).fit() + +In [78]: results.params +Out[78]: +Intercept -0.020799 +col0 0.065813 +col1 0.268970 +col2 0.449419 +dtype: float64 + +In [79]: results.tvalues +Out[79]: +Intercept -0.652501 +col0 1.219768 +col1 6.312369 +col2 6.567428 +dtype: float64 +``` + +注意 statsmodels 如何将结果返回为带有 DataFrame 列名称附加的 Series。在使用公式和 pandas 对象时,我们也不需要使用`add_constant`。 + +给定新的样本外数据,可以根据估计的模型参数计算预测值: + +```py +In [80]: results.predict(data[:5]) +Out[80]: +0 -0.592959 +1 -0.531160 +2 0.058636 +3 0.283658 +4 -0.102947 +dtype: float64 +``` + +在 statsmodels 中有许多用于分析、诊断和可视化线性模型结果的附加工具,您可以探索。除了普通最小二乘法之外,还有其他类型的线性模型。 + +### 估计时间序列过程 + +statsmodels 中的另一类模型是用于时间序列分析的模型。其中包括自回归过程、卡尔曼滤波和其他状态空间模型以及多变量自回归模型。 + +让我们模拟一些具有自回归结构和噪声的时间序列数据。在 Jupyter 中运行以下代码: + +```py +init_x = 4 + +values = [init_x, init_x] +N = 1000 + +b0 = 0.8 +b1 = -0.4 +noise = dnorm(0, 0.1, N) +for i in range(N): + new_x = values[-1] * b0 + values[-2] * b1 + noise[i] + values.append(new_x) +``` + +这个数据具有 AR(2)结构(两个*滞后*),参数为`0.8`和`-0.4`。当拟合 AR 模型时,您可能不知道要包括的滞后项的数量,因此可以使用一些更大数量的滞后项来拟合模型: + +```py +In [82]: from statsmodels.tsa.ar_model import AutoReg + +In [83]: MAXLAGS = 5 + +In [84]: model = AutoReg(values, MAXLAGS) + +In [85]: results = model.fit() +``` + +结果中的估计参数首先是截距,接下来是前两个滞后的估计值: + +```py +In [86]: results.params +Out[86]: array([ 0.0235, 0.8097, -0.4287, -0.0334, 0.0427, -0.0567]) +``` + +这些模型的更深层细节以及如何解释它们的结果超出了我在本书中可以涵盖的范围,但在 statsmodels 文档中还有很多内容等待探索。 + +## 12.4 scikit-learn 简介 + +[scikit-learn](http://scikit-learn.org)是最广泛使用和信任的通用 Python 机器学习工具包之一。它包含广泛的标准监督和无监督机器学习方法,具有模型选择和评估工具,数据转换,数据加载和模型持久性。这些模型可用于分类,聚类,预测和其他常见任务。您可以像这样从 conda 安装 scikit-learn: + +```py +conda install scikit-learn +``` + +有很多在线和印刷资源可供学习机器学习以及如何应用类似 scikit-learn 的库来解决实际问题。在本节中,我将简要介绍 scikit-learn API 风格。 + +scikit-learn 中的 pandas 集成在近年来显著改善,当您阅读本文时,它可能已经进一步改进。我鼓励您查看最新的项目文档。 + +作为本章的示例,我使用了一份来自 Kaggle 竞赛的[经典数据集](https://www.kaggle.com/c/titanic),关于 1912 年*泰坦尼克号*上乘客生存率。我们使用 pandas 加载训练和测试数据集: + +```py +In [87]: train = pd.read_csv('datasets/titanic/train.csv') + +In [88]: test = pd.read_csv('datasets/titanic/test.csv') + +In [89]: train.head(4) +Out[89]: + PassengerId Survived Pclass +0 1 0 3 \ +1 2 1 1 +2 3 1 3 +3 4 1 1 + Name Sex Age SibSp +0 Braund, Mr. Owen Harris male 22.0 1 \ +1 Cumings, Mrs. John Bradley (Florence Briggs Thayer) female 38.0 1 +2 Heikkinen, Miss. Laina female 26.0 0 +3 Futrelle, Mrs. Jacques Heath (Lily May Peel) female 35.0 1 + Parch Ticket Fare Cabin Embarked +0 0 A/5 21171 7.2500 NaN S +1 0 PC 17599 71.2833 C85 C +2 0 STON/O2\. 3101282 7.9250 NaN S +3 0 113803 53.1000 C123 S +``` + +像 statsmodels 和 scikit-learn 这样的库通常无法处理缺失数据,因此我们查看列,看看是否有包含缺失数据的列: + +```py +In [90]: train.isna().sum() +Out[90]: +PassengerId 0 +Survived 0 +Pclass 0 +Name 0 +Sex 0 +Age 177 +SibSp 0 +Parch 0 +Ticket 0 +Fare 0 +Cabin 687 +Embarked 2 +dtype: int64 + +In [91]: test.isna().sum() +Out[91]: +PassengerId 0 +Pclass 0 +Name 0 +Sex 0 +Age 86 +SibSp 0 +Parch 0 +Ticket 0 +Fare 1 +Cabin 327 +Embarked 0 +dtype: int64 +``` + +在统计学和机器学习的示例中,一个典型的任务是根据数据中的特征预测乘客是否会生存。模型在*训练*数据集上拟合,然后在外样本*测试*数据集上进行评估。 + +我想使用`Age`作为预测变量,但它有缺失数据。有很多方法可以进行缺失数据插补,但我将使用训练数据集的中位数来填充两个表中的空值: + +```py +In [92]: impute_value = train['Age'].median() + +In [93]: train['Age'] = train['Age'].fillna(impute_value) + +In [94]: test['Age'] = test['Age'].fillna(impute_value) +``` + +现在我们需要指定我们的模型。我添加一个名为`IsFemale`的列,作为`'Sex'`列的编码版本: + +```py +In [95]: train['IsFemale'] = (train['Sex'] == 'female').astype(int) + +In [96]: test['IsFemale'] = (test['Sex'] == 'female').astype(int) +``` + +然后我们决定一些模型变量并创建 NumPy 数组: + +```py +In [97]: predictors = ['Pclass', 'IsFemale', 'Age'] + +In [98]: X_train = train[predictors].to_numpy() + +In [99]: X_test = test[predictors].to_numpy() + +In [100]: y_train = train['Survived'].to_numpy() + +In [101]: X_train[:5] +Out[101]: +array([[ 3., 0., 22.], + [ 1., 1., 38.], + [ 3., 1., 26.], + [ 1., 1., 35.], + [ 3., 0., 35.]]) + +In [102]: y_train[:5] +Out[102]: array([0, 1, 1, 1, 0]) +``` + +我不断言这是一个好模型或这些特征是否被正确设计。我们使用 scikit-learn 中的`LogisticRegression`模型并创建一个模型实例: + +```py +In [103]: from sklearn.linear_model import LogisticRegression + +In [104]: model = LogisticRegression() +``` + +我们可以使用模型的`fit`方法将此模型拟合到训练数据中: + +```py +In [105]: model.fit(X_train, y_train) +Out[105]: LogisticRegression() +``` + +现在,我们可以使用`model.predict`为测试数据集进行预测: + +```py +In [106]: y_predict = model.predict(X_test) + +In [107]: y_predict[:10] +Out[107]: array([0, 0, 0, 0, 1, 0, 1, 0, 1, 0]) +``` + +如果您有测试数据集的真实值,可以计算准确率百分比或其他错误度量: + +```py +(y_true == y_predict).mean() +``` + +在实践中,模型训练通常存在许多额外的复杂层。许多模型具有可以调整的参数,并且有一些技术,如*交叉验证*可用于参数调整,以避免过度拟合训练数据。这通常可以提供更好的预测性能或对新数据的鲁棒性。 + +交叉验证通过拆分训练数据来模拟外样本预测。根据像均方误差这样的模型准确度得分,您可以对模型参数执行网格搜索。一些模型,如逻辑回归,具有内置交叉验证的估计器类。例如,`LogisticRegressionCV`类可以与一个参数一起使用,该参数指示在模型正则化参数`C`上执行多精细的网格搜索: + +```py +In [108]: from sklearn.linear_model import LogisticRegressionCV + +In [109]: model_cv = LogisticRegressionCV(Cs=10) + +In [110]: model_cv.fit(X_train, y_train) +Out[110]: LogisticRegressionCV() +``` + +手动进行交叉验证,可以使用`cross_val_score`辅助函数,该函数处理数据拆分过程。例如,要对我们的模型进行四个不重叠的训练数据拆分进行交叉验证,我们可以这样做: + +```py +In [111]: from sklearn.model_selection import cross_val_score + +In [112]: model = LogisticRegression(C=10) + +In [113]: scores = cross_val_score(model, X_train, y_train, cv=4) + +In [114]: scores +Out[114]: array([0.7758, 0.7982, 0.7758, 0.7883]) +``` + +默认的评分指标取决于模型,但可以选择一个明确的评分函数。交叉验证模型训练时间较长,但通常可以获得更好的模型性能。 + +## 12.5 结论 + +虽然我只是浅尝了一些 Python 建模库的表面,但有越来越多的框架适用于各种统计和机器学习,要么是用 Python 实现的,要么有 Python 用户界面。 + +这本书专注于数据整理,但还有许多其他专门用于建模和数据科学工具的书籍。一些优秀的书籍包括: + ++ 《Python 机器学习入门》作者 Andreas Müller 和 Sarah Guido(O'Reilly) + ++ 《Python 数据科学手册》作者 Jake VanderPlas(O'Reilly) + ++ 《从零开始的数据科学:Python 基础》作者 Joel Grus(O'Reilly) + ++ 《Python 机器学习》作者 Sebastian Raschka 和 Vahid Mirjalili(Packt Publishing) + ++ 《使用 Scikit-Learn、Keras 和 TensorFlow 进行实践机器学习》作者 Aurélien Géron(O'Reilly) + +尽管书籍可以是学习的宝贵资源,但当底层的开源软件发生变化时,它们有时会变得过时。熟悉各种统计或机器学习框架的文档是一个好主意,以便了解最新功能和 API。 diff --git a/translations/cn/pyda3e_16.md b/translations/cn/pyda3e_16.md new file mode 100644 index 000000000..508e0e0be --- /dev/null +++ b/translations/cn/pyda3e_16.md @@ -0,0 +1,2158 @@ +# 十三、数据分析示例 + +> 原文:[`wesmckinney.com/book/data-analysis-examples`](https://wesmckinney.com/book/data-analysis-examples) +> +> 译者:[飞龙](https://github.com/wizardforcel) +> +> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) + + +> 此开放访问网络版本的《Python 数据分析第三版》现已作为[印刷版和数字版](https://amzn.to/3DyLaJc)的伴侣提供。如果您发现任何勘误,请[在此处报告](https://oreilly.com/catalog/0636920519829/errata)。请注意,由 Quarto 生成的本站点的某些方面与 O'Reilly 的印刷版和电子书版本的格式不同。 +> +> 如果您发现本书的在线版本有用,请考虑[订购纸质版](https://amzn.to/3DyLaJc)或[无 DRM 的电子书](https://www.ebooks.com/en-us/book/210644288/python-for-data-analysis/wes-mckinney/?affId=WES398681F)以支持作者。本网站的内容不得复制或再生产。代码示例采用 MIT 许可,可在 GitHub 或 Gitee 上找到。 + +现在我们已经到达本书的最后一章,我们将查看一些真实世界的数据集。对于每个数据集,我们将使用本书中介绍的技术从原始数据中提取含义。演示的技术可以应用于各种其他数据集。本章包含一系列杂例数据集,您可以使用这些数据集练习本书中的工具。 + +示例数据集可在本书附带的[GitHub 存储库](http://github.com/wesm/pydata-book)中找到。如果无法访问 GitHub,还可以从[Gitee 上的存储库镜像](https://gitee.com/wesmckinn/pydata-book)获取它们。 + +## 13.1 Bitly Data from 1.USA.gov + +2011 年,URL 缩短服务[Bitly](https://bitly.com)与美国政府网站[USA.gov](https://www.usa.gov)合作,提供从缩短链接以*.gov*或*.mil*结尾的用户收集的匿名数据的源。2011 年,可下载的文本文件提供了实时数据以及每小时的快照。本文撰写时(2022 年),该服务已关闭,但我们保留了一份数据文件用于本书的示例。 + +在每个文件的每一行中,每小时快照包含一种称为 JSON 的常见网络数据形式,JSON 代表 JavaScript 对象表示法。例如,如果我们只读取文件的第一行,可能会看到类似于这样的内容: + +```py +In [5]: path = "datasets/bitly_usagov/example.txt" + +In [6]: with open(path) as f: + ...: print(f.readline()) + ...: +{ "a": "Mozilla\\/5.0 (Windows NT 6.1; WOW64) AppleWebKit\\/535.11 +(KHTML, like Gecko) Chrome\\/17.0.963.78 Safari\\/535.11", "c": "US", "nk": 1, +"tz": "America\\/New_York", "gr": "MA", "g": "A6qOVH", "h": "wfLQtf", "l": +"orofrog", "al": "en-US,en;q=0.8", "hh": "1.usa.gov", "r": +"http:\\/\\/www.facebook.com\\/l\\/7AQEFzjSi\\/1.usa.gov\\/wfLQtf", "u": +"http:\\/\\/www.ncbi.nlm.nih.gov\\/pubmed\\/22415991", "t": 1331923247, "hc": +1331822918, "cy": "Danvers", "ll": [ 42.576698, -70.954903 ] } +``` + +Python 有内置和第三方库,用于将 JSON 字符串转换为 Python 字典。在这里,我们将使用`json`模块及其在我们下载的示例文件中的每一行上调用的`loads`函数: + +```py +import json +with open(path) as f: + records = [json.loads(line) for line in f] +``` + +结果对象`records`现在是一个 Python 字典列表: + +```py +In [18]: records[0] +Out[18]: +{'a': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) +Chrome/17.0.963.78 Safari/535.11', + 'al': 'en-US,en;q=0.8', + 'c': 'US', + 'cy': 'Danvers', + 'g': 'A6qOVH', + 'gr': 'MA', + 'h': 'wfLQtf', + 'hc': 1331822918, + 'hh': '1.usa.gov', + 'l': 'orofrog', + 'll': [42.576698, -70.954903], + 'nk': 1, + 'r': 'http://www.facebook.com/l/7AQEFzjSi/1.usa.gov/wfLQtf', + 't': 1331923247, + 'tz': 'America/New_York', + 'u': 'http://www.ncbi.nlm.nih.gov/pubmed/22415991'} +``` + +### 使用纯 Python 计算时区 + +假设我们有兴趣找出数据集中最常出现的时区(`tz`字段)。我们可以通过多种方式来实现这一点。首先,让我们再次使用列表推导式提取时区列表: + +```py +In [15]: time_zones = [rec["tz"] for rec in records] +--------------------------------------------------------------------------- +KeyError Traceback (most recent call last) + in +----> 1 time_zones = [rec["tz"] for rec in records] + in (.0) +----> 1 time_zones = [rec["tz"] for rec in records] +KeyError: 'tz' +``` + +糟糕!原来并非所有记录都有时区字段。我们可以通过在列表推导式末尾添加检查`if "tz" in rec`来处理这个问题: + +```py +In [16]: time_zones = [rec["tz"] for rec in records if "tz" in rec] + +In [17]: time_zones[:10] +Out[17]: +['America/New_York', + 'America/Denver', + 'America/New_York', + 'America/Sao_Paulo', + 'America/New_York', + 'America/New_York', + 'Europe/Warsaw', + '', + '', + ''] +``` + +仅查看前 10 个时区,我们会发现其中一些是未知的(空字符串)。您也可以将这些过滤掉,但我暂时保留它们。接下来,为了按时区生成计数,我将展示两种方法:一种更困难的方法(仅使用 Python 标准库)和一种更简单的方法(使用 pandas)。计数的一种方法是使用字典来存储计数,同时我们遍历时区: + +```py +def get_counts(sequence): + counts = {} + for x in sequence: + if x in counts: + counts[x] += 1 + else: + counts[x] = 1 + return counts +``` + +使用 Python 标准库中更高级的工具,您可以更简洁地编写相同的内容: + +```py +from collections import defaultdict + +def get_counts2(sequence): + counts = defaultdict(int) # values will initialize to 0 + for x in sequence: + counts[x] += 1 + return counts +``` + +我将这个逻辑放在一个函数中,以使其更具可重用性。要在时区上使用它,只需传递`time_zones`列表: + +```py +In [20]: counts = get_counts(time_zones) + +In [21]: counts["America/New_York"] +Out[21]: 1251 + +In [22]: len(time_zones) +Out[22]: 3440 +``` + +如果我们想要前 10 个时区及其计数,我们可以通过`(count, timezone)`创建一个元组列表,并对其进行排序: + +```py +def top_counts(count_dict, n=10): + value_key_pairs = [(count, tz) for tz, count in count_dict.items()] + value_key_pairs.sort() + return value_key_pairs[-n:] +``` + +我们有: + +```py +In [24]: top_counts(counts) +Out[24]: +[(33, 'America/Sao_Paulo'), + (35, 'Europe/Madrid'), + (36, 'Pacific/Honolulu'), + (37, 'Asia/Tokyo'), + (74, 'Europe/London'), + (191, 'America/Denver'), + (382, 'America/Los_Angeles'), + (400, 'America/Chicago'), + (521, ''), + (1251, 'America/New_York')] +``` + +如果您搜索 Python 标准库,可能会找到`collections.Counter`类,这将使这个任务变得更简单: + +```py +In [25]: from collections import Counter + +In [26]: counts = Counter(time_zones) + +In [27]: counts.most_common(10) +Out[27]: +[('America/New_York', 1251), + ('', 521), + ('America/Chicago', 400), + ('America/Los_Angeles', 382), + ('America/Denver', 191), + ('Europe/London', 74), + ('Asia/Tokyo', 37), + ('Pacific/Honolulu', 36), + ('Europe/Madrid', 35), + ('America/Sao_Paulo', 33)] +``` + +### 使用 pandas 计算时区 + +您可以通过将记录列表传递给`pandas.DataFrame`来从原始记录集创建一个 DataFrame: + +```py +In [28]: frame = pd.DataFrame(records) +``` + +我们可以查看有关这个新 DataFrame 的一些基本信息,比如列名、推断的列类型或缺失值的数量,使用`frame.info()`: + +```py +In [29]: frame.info() + +RangeIndex: 3560 entries, 0 to 3559 +Data columns (total 18 columns): + # Column Non-Null Count Dtype +--- ------ -------------- ----- + 0 a 3440 non-null object + 1 c 2919 non-null object + 2 nk 3440 non-null float64 + 3 tz 3440 non-null object + 4 gr 2919 non-null object + 5 g 3440 non-null object + 6 h 3440 non-null object + 7 l 3440 non-null object + 8 al 3094 non-null object + 9 hh 3440 non-null object + 10 r 3440 non-null object + 11 u 3440 non-null object + 12 t 3440 non-null float64 + 13 hc 3440 non-null float64 + 14 cy 2919 non-null object + 15 ll 2919 non-null object + 16 _heartbeat_ 120 non-null float64 + 17 kw 93 non-null object +dtypes: float64(4), object(14) +memory usage: 500.8+ KB + +In [30]: frame["tz"].head() +Out[30]: +0 America/New_York +1 America/Denver +2 America/New_York +3 America/Sao_Paulo +4 America/New_York +Name: tz, dtype: object +``` + +`frame`的输出显示为*摘要视图*,适用于大型 DataFrame 对象。然后我们可以使用 Series 的`value_counts`方法: + +```py +In [31]: tz_counts = frame["tz"].value_counts() + +In [32]: tz_counts.head() +Out[32]: +tz +America/New_York 1251 + 521 +America/Chicago 400 +America/Los_Angeles 382 +America/Denver 191 +Name: count, dtype: int64 +``` + +我们可以使用 matplotlib 可视化这些数据。我们可以通过为记录中的未知或缺失时区数据填充替代值来使图表更加美观。我们使用`fillna`方法替换缺失值,并使用布尔数组索引来处理空字符串: + +```py +In [33]: clean_tz = frame["tz"].fillna("Missing") + +In [34]: clean_tz[clean_tz == ""] = "Unknown" + +In [35]: tz_counts = clean_tz.value_counts() + +In [36]: tz_counts.head() +Out[36]: +tz +America/New_York 1251 +Unknown 521 +America/Chicago 400 +America/Los_Angeles 382 +America/Denver 191 +Name: count, dtype: int64 +``` + +此时,我们可以使用[seaborn 包](http://seaborn.pydata.org)制作一个水平条形图(参见 1.usa.gov 示例数据中的顶级时区以查看结果可视化): + +```py +In [38]: import seaborn as sns + +In [39]: subset = tz_counts.head() + +In [40]: sns.barplot(y=subset.index, x=subset.to_numpy()) +``` + +![](img/7dd52fd5fa321b6a9d79111326825f9b.png) + +图 13.1:1.usa.gov 示例数据中的顶级时区 + +`a`字段包含有关用于执行 URL 缩短的浏览器、设备或应用程序的信息: + +```py +In [41]: frame["a"][1] +Out[41]: 'GoogleMaps/RochesterNY' + +In [42]: frame["a"][50] +Out[42]: 'Mozilla/5.0 (Windows NT 5.1; rv:10.0.2) Gecko/20100101 Firefox/10.0.2' + +In [43]: frame["a"][51][:50] # long line +Out[43]: 'Mozilla/5.0 (Linux; U; Android 2.2.2; en-us; LG-P9' +``` + +解析这些“代理”字符串中的所有有趣信息可能看起来是一项艰巨的任务。一种可能的策略是将字符串中的第一个标记(大致对应于浏览器功能)拆分出来,并对用户行为进行另一个摘要: + +```py +In [44]: results = pd.Series([x.split()[0] for x in frame["a"].dropna()]) + +In [45]: results.head(5) +Out[45]: +0 Mozilla/5.0 +1 GoogleMaps/RochesterNY +2 Mozilla/4.0 +3 Mozilla/5.0 +4 Mozilla/5.0 +dtype: object + +In [46]: results.value_counts().head(8) +Out[46]: +Mozilla/5.0 2594 +Mozilla/4.0 601 +GoogleMaps/RochesterNY 121 +Opera/9.80 34 +TEST_INTERNET_AGENT 24 +GoogleProducer 21 +Mozilla/6.0 5 +BlackBerry8520/5.0.0.681 4 +Name: count, dtype: int64 +``` + +现在,假设您想将顶级时区分解为 Windows 和非 Windows 用户。为简化起见,假设如果代理字符串中包含`"Windows"`字符串,则用户使用的是 Windows。由于一些代理缺失,我们将排除这些数据: + +```py +In [47]: cframe = frame[frame["a"].notna()].copy() +``` + +然后,我们想计算每行是否为 Windows 的值: + +```py +In [48]: cframe["os"] = np.where(cframe["a"].str.contains("Windows"), + ....: "Windows", "Not Windows") + +In [49]: cframe["os"].head(5) +Out[49]: +0 Windows +1 Not Windows +2 Windows +3 Not Windows +4 Windows +Name: os, dtype: object +``` + +然后,您可以按其时区列和这个新的操作系统列表对数据进行分组: + +```py +In [50]: by_tz_os = cframe.groupby(["tz", "os"]) +``` + +类似于`value_counts`函数,可以使用`size`计算组计数。然后将结果重塑为表格,使用`unstack`: + +```py +In [51]: agg_counts = by_tz_os.size().unstack().fillna(0) + +In [52]: agg_counts.head() +Out[52]: +os Not Windows Windows +tz + 245.0 276.0 +Africa/Cairo 0.0 3.0 +Africa/Casablanca 0.0 1.0 +Africa/Ceuta 0.0 2.0 +Africa/Johannesburg 0.0 1.0 +``` + +最后,让我们选择顶级的整体时区。为此,我从`agg_counts`中的行计数构建一个间接索引数组。在使用`agg_counts.sum("columns")`计算行计数后,我可以调用`argsort()`来获得一个可以用于升序排序的索引数组: + +```py +In [53]: indexer = agg_counts.sum("columns").argsort() + +In [54]: indexer.values[:10] +Out[54]: array([24, 20, 21, 92, 87, 53, 54, 57, 26, 55]) +``` + +我使用`take`按顺序选择行,然后切掉最后 10 行(最大值): + +```py +In [55]: count_subset = agg_counts.take(indexer[-10:]) + +In [56]: count_subset +Out[56]: +os Not Windows Windows +tz +America/Sao_Paulo 13.0 20.0 +Europe/Madrid 16.0 19.0 +Pacific/Honolulu 0.0 36.0 +Asia/Tokyo 2.0 35.0 +Europe/London 43.0 31.0 +America/Denver 132.0 59.0 +America/Los_Angeles 130.0 252.0 +America/Chicago 115.0 285.0 + 245.0 276.0 +America/New_York 339.0 912.0 +``` + +pandas 有一个方便的方法叫做`nlargest`,可以做同样的事情: + +```py +In [57]: agg_counts.sum(axis="columns").nlargest(10) +Out[57]: +tz +America/New_York 1251.0 + 521.0 +America/Chicago 400.0 +America/Los_Angeles 382.0 +America/Denver 191.0 +Europe/London 74.0 +Asia/Tokyo 37.0 +Pacific/Honolulu 36.0 +Europe/Madrid 35.0 +America/Sao_Paulo 33.0 +dtype: float64 +``` + +然后,可以绘制一个分组条形图,比较 Windows 和非 Windows 用户的数量,使用 seaborn 的`barplot`函数(参见按 Windows 和非 Windows 用户的顶级时区)。我首先调用`count_subset.stack()`并重置索引以重新排列数据,以便更好地与 seaborn 兼容: + +```py +In [59]: count_subset = count_subset.stack() + +In [60]: count_subset.name = "total" + +In [61]: count_subset = count_subset.reset_index() + +In [62]: count_subset.head(10) +Out[62]: + tz os total +0 America/Sao_Paulo Not Windows 13.0 +1 America/Sao_Paulo Windows 20.0 +2 Europe/Madrid Not Windows 16.0 +3 Europe/Madrid Windows 19.0 +4 Pacific/Honolulu Not Windows 0.0 +5 Pacific/Honolulu Windows 36.0 +6 Asia/Tokyo Not Windows 2.0 +7 Asia/Tokyo Windows 35.0 +8 Europe/London Not Windows 43.0 +9 Europe/London Windows 31.0 + +In [63]: sns.barplot(x="total", y="tz", hue="os", data=count_subset) +``` + +![](img/f69b4d795dc2eedaf3e3e02e6e0a9d87.png) + +图 13.2:按 Windows 和非 Windows 用户的顶级时区 + +在较小的组中,很难看出 Windows 用户的相对百分比,因此让我们将组百分比归一化为 1: + +```py +def norm_total(group): + group["normed_total"] = group["total"] / group["total"].sum() + return group + +results = count_subset.groupby("tz").apply(norm_total) +``` + +然后在出现频率最高的时区中 Windows 和非 Windows 用户的百分比中绘制这个图: + +```py +In [66]: sns.barplot(x="normed_total", y="tz", hue="os", data=results) +``` + +![](img/a218b1a30914e6cbfa11d7eb83a4f1cb.png) + +图 13.3:出现频率最高的时区中 Windows 和非 Windows 用户的百分比 + +我们可以通过使用`transform`方法和`groupby`更有效地计算归一化和: + +```py +In [67]: g = count_subset.groupby("tz") + +In [68]: results2 = count_subset["total"] / g["total"].transform("sum") +``` + +## 13.2 MovieLens 1M 数据集 + +[GroupLens Research](https://grouplens.org/datasets/movielens)提供了从 1990 年代末到 2000 年代初从 MovieLens 用户收集的多个电影评分数据集。数据提供了电影评分、电影元数据(类型和年份)以及关于用户的人口统计数据(年龄、邮政编码、性别认同和职业)。这些数据通常在基于机器学习算法的推荐系统的开发中很有兴趣。虽然我们在本书中没有详细探讨机器学习技术,但我将向您展示如何将这些数据集切分成您需要的确切形式。 + +MovieLens 1M 数据集包含从六千名用户对四千部电影收集的一百万个评分。它分布在三个表中:评分、用户信息和电影信息。我们可以使用`pandas.read_table`将每个表加载到一个 pandas DataFrame 对象中。在 Jupyter 单元格中运行以下代码: + +```py +unames = ["user_id", "gender", "age", "occupation", "zip"] +users = pd.read_table("datasets/movielens/users.dat", sep="::", + header=None, names=unames, engine="python") + +rnames = ["user_id", "movie_id", "rating", "timestamp"] +ratings = pd.read_table("datasets/movielens/ratings.dat", sep="::", + header=None, names=rnames, engine="python") + +mnames = ["movie_id", "title", "genres"] +movies = pd.read_table("datasets/movielens/movies.dat", sep="::", + header=None, names=mnames, engine="python") +``` + +您可以通过查看每个 DataFrame 来验证一切是否成功: + +```py +In [70]: users.head(5) +Out[70]: + user_id gender age occupation zip +0 1 F 1 10 48067 +1 2 M 56 16 70072 +2 3 M 25 15 55117 +3 4 M 45 7 02460 +4 5 M 25 20 55455 + +In [71]: ratings.head(5) +Out[71]: + user_id movie_id rating timestamp +0 1 1193 5 978300760 +1 1 661 3 978302109 +2 1 914 3 978301968 +3 1 3408 4 978300275 +4 1 2355 5 978824291 + +In [72]: movies.head(5) +Out[72]: + movie_id title genres +0 1 Toy Story (1995) Animation|Children's|Comedy +1 2 Jumanji (1995) Adventure|Children's|Fantasy +2 3 Grumpier Old Men (1995) Comedy|Romance +3 4 Waiting to Exhale (1995) Comedy|Drama +4 5 Father of the Bride Part II (1995) Comedy + +In [73]: ratings +Out[73]: + user_id movie_id rating timestamp +0 1 1193 5 978300760 +1 1 661 3 978302109 +2 1 914 3 978301968 +3 1 3408 4 978300275 +4 1 2355 5 978824291 +... ... ... ... ... +1000204 6040 1091 1 956716541 +1000205 6040 1094 5 956704887 +1000206 6040 562 5 956704746 +1000207 6040 1096 4 956715648 +1000208 6040 1097 4 956715569 +[1000209 rows x 4 columns] +``` + +请注意,年龄和职业被编码为整数,表示数据集的*README*文件中描述的组。分析分布在三个表中的数据并不是一项简单的任务;例如,假设您想要按性别身份和年龄计算特定电影的平均评分。正如您将看到的,将所有数据合并到一个单一表中更方便。使用 pandas 的`merge`函数,我们首先将`ratings`与`users`合并,然后将该结果与`movies`数据合并。pandas 根据重叠的名称推断要用作合并(或*join*)键的列: + +```py +In [74]: data = pd.merge(pd.merge(ratings, users), movies) + +In [75]: data +Out[75]: + user_id movie_id rating timestamp gender age occupation zip +0 1 1193 5 978300760 F 1 10 48067 \ +1 2 1193 5 978298413 M 56 16 70072 +2 12 1193 4 978220179 M 25 12 32793 +3 15 1193 4 978199279 M 25 7 22903 +4 17 1193 5 978158471 M 50 1 95350 +... ... ... ... ... ... ... ... ... +1000204 5949 2198 5 958846401 M 18 17 47901 +1000205 5675 2703 3 976029116 M 35 14 30030 +1000206 5780 2845 1 958153068 M 18 17 92886 +1000207 5851 3607 5 957756608 F 18 20 55410 +1000208 5938 2909 4 957273353 M 25 1 35401 + title genres +0 One Flew Over the Cuckoo's Nest (1975) Drama +1 One Flew Over the Cuckoo's Nest (1975) Drama +2 One Flew Over the Cuckoo's Nest (1975) Drama +3 One Flew Over the Cuckoo's Nest (1975) Drama +4 One Flew Over the Cuckoo's Nest (1975) Drama +... ... ... +1000204 Modulations (1998) Documentary +1000205 Broken Vessels (1998) Drama +1000206 White Boys (1999) Drama +1000207 One Little Indian (1973) Comedy|Drama|Western +1000208 Five Wives, Three Secretaries and Me (1998) Documentary +[1000209 rows x 10 columns] + +In [76]: data.iloc[0] +Out[76]: +user_id 1 +movie_id 1193 +rating 5 +timestamp 978300760 +gender F +age 1 +occupation 10 +zip 48067 +title One Flew Over the Cuckoo's Nest (1975) +genres Drama +Name: 0, dtype: object +``` + +为了获得按性别分组的每部电影的平均评分,我们可以使用`pivot_table`方法: + +```py +In [77]: mean_ratings = data.pivot_table("rating", index="title", + ....: columns="gender", aggfunc="mean") + +In [78]: mean_ratings.head(5) +Out[78]: +gender F M +title +$1,000,000 Duck (1971) 3.375000 2.761905 +'Night Mother (1986) 3.388889 3.352941 +'Til There Was You (1997) 2.675676 2.733333 +'burbs, The (1989) 2.793478 2.962085 +...And Justice for All (1979) 3.828571 3.689024 +``` + +这产生了另一个包含平均评分的 DataFrame,其中电影标题作为行标签(“索引”),性别作为列标签。我首先筛选出至少收到 250 个评分的电影(一个任意的数字);为此,我按标题对数据进行分组,并使用`size()`来获取每个标题的组大小的 Series: + +```py +In [79]: ratings_by_title = data.groupby("title").size() + +In [80]: ratings_by_title.head() +Out[80]: +title +$1,000,000 Duck (1971) 37 +'Night Mother (1986) 70 +'Til There Was You (1997) 52 +'burbs, The (1989) 303 +...And Justice for All (1979) 199 +dtype: int64 + +In [81]: active_titles = ratings_by_title.index[ratings_by_title >= 250] + +In [82]: active_titles +Out[82]: +Index([''burbs, The (1989)', '10 Things I Hate About You (1999)', + '101 Dalmatians (1961)', '101 Dalmatians (1996)', '12 Angry Men (1957)', + '13th Warrior, The (1999)', '2 Days in the Valley (1996)', + '20,000 Leagues Under the Sea (1954)', '2001: A Space Odyssey (1968)', + '2010 (1984)', + ... + 'X-Men (2000)', 'Year of Living Dangerously (1982)', + 'Yellow Submarine (1968)', 'You've Got Mail (1998)', + 'Young Frankenstein (1974)', 'Young Guns (1988)', + 'Young Guns II (1990)', 'Young Sherlock Holmes (1985)', + 'Zero Effect (1998)', 'eXistenZ (1999)'], + dtype='object', name='title', length=1216) +``` + +然后,可以使用至少收到 250 个评分的标题的索引来从`mean_ratings`中选择行,使用`.loc`: + +```py +In [83]: mean_ratings = mean_ratings.loc[active_titles] + +In [84]: mean_ratings +Out[84]: +gender F M +title +'burbs, The (1989) 2.793478 2.962085 +10 Things I Hate About You (1999) 3.646552 3.311966 +101 Dalmatians (1961) 3.791444 3.500000 +101 Dalmatians (1996) 3.240000 2.911215 +12 Angry Men (1957) 4.184397 4.328421 +... ... ... +Young Guns (1988) 3.371795 3.425620 +Young Guns II (1990) 2.934783 2.904025 +Young Sherlock Holmes (1985) 3.514706 3.363344 +Zero Effect (1998) 3.864407 3.723140 +eXistenZ (1999) 3.098592 3.289086 +[1216 rows x 2 columns] +``` + +要查看女性观众最喜欢的电影,我们可以按降序排序`F`列: + +```py +In [86]: top_female_ratings = mean_ratings.sort_values("F", ascending=False) + +In [87]: top_female_ratings.head() +Out[87]: +gender F M +title +Close Shave, A (1995) 4.644444 4.473795 +Wrong Trousers, The (1993) 4.588235 4.478261 +Sunset Blvd. (a.k.a. Sunset Boulevard) (1950) 4.572650 4.464589 +Wallace & Gromit: The Best of Aardman Animation (1996) 4.563107 4.385075 +Schindler's List (1993) 4.562602 4.491415 +``` + +### 测量评分分歧 + +假设您想要找到在男性和女性观众之间最具分歧的电影。一种方法是向`mean_ratings`添加一个包含平均值差异的列,然后按照该列进行排序: + +```py +In [88]: mean_ratings["diff"] = mean_ratings["M"] - mean_ratings["F"] +``` + +按照`"diff"`排序,可以得到评分差异最大的电影,以便看到哪些电影更受女性喜欢: + +```py +In [89]: sorted_by_diff = mean_ratings.sort_values("diff") + +In [90]: sorted_by_diff.head() +Out[90]: +gender F M diff +title +Dirty Dancing (1987) 3.790378 2.959596 -0.830782 +Jumpin' Jack Flash (1986) 3.254717 2.578358 -0.676359 +Grease (1978) 3.975265 3.367041 -0.608224 +Little Women (1994) 3.870588 3.321739 -0.548849 +Steel Magnolias (1989) 3.901734 3.365957 -0.535777 +``` + +颠倒行的顺序并再次切片前 10 行,我们得到了男性喜欢但女性评分不高的电影: + +```py +In [91]: sorted_by_diff[::-1].head() +Out[91]: +gender F M diff +title +Good, The Bad and The Ugly, The (1966) 3.494949 4.221300 0.726351 +Kentucky Fried Movie, The (1977) 2.878788 3.555147 0.676359 +Dumb & Dumber (1994) 2.697987 3.336595 0.638608 +Longest Day, The (1962) 3.411765 4.031447 0.619682 +Cable Guy, The (1996) 2.250000 2.863787 0.613787 +``` + +假设您想要找到在观众中引起最大分歧的电影,而不考虑性别认同。分歧可以通过评分的方差或标准差来衡量。为了得到这个结果,我们首先按标题计算评分的标准差,然后筛选出活跃的标题: + +```py +In [92]: rating_std_by_title = data.groupby("title")["rating"].std() + +In [93]: rating_std_by_title = rating_std_by_title.loc[active_titles] + +In [94]: rating_std_by_title.head() +Out[94]: +title +'burbs, The (1989) 1.107760 +10 Things I Hate About You (1999) 0.989815 +101 Dalmatians (1961) 0.982103 +101 Dalmatians (1996) 1.098717 +12 Angry Men (1957) 0.812731 +Name: rating, dtype: float64 +``` + +然后,我们按降序排序并选择前 10 行,这大致是评分最具分歧的 10 部电影: + +```py +In [95]: rating_std_by_title.sort_values(ascending=False)[:10] +Out[95]: +title +Dumb & Dumber (1994) 1.321333 +Blair Witch Project, The (1999) 1.316368 +Natural Born Killers (1994) 1.307198 +Tank Girl (1995) 1.277695 +Rocky Horror Picture Show, The (1975) 1.260177 +Eyes Wide Shut (1999) 1.259624 +Evita (1996) 1.253631 +Billy Madison (1995) 1.249970 +Fear and Loathing in Las Vegas (1998) 1.246408 +Bicentennial Man (1999) 1.245533 +Name: rating, dtype: float64 +``` + +您可能已经注意到电影类型是以管道分隔(`|`)的字符串给出的,因为一部电影可以属于多种类型。为了帮助我们按类型对评分数据进行分组,我们可以在 DataFrame 上使用`explode`方法。让我们看看这是如何工作的。首先,我们可以使用 Series 上的`str.split`方法将类型字符串拆分为类型列表: + +```py +In [96]: movies["genres"].head() +Out[96]: +0 Animation|Children's|Comedy +1 Adventure|Children's|Fantasy +2 Comedy|Romance +3 Comedy|Drama +4 Comedy +Name: genres, dtype: object + +In [97]: movies["genres"].head().str.split("|") +Out[97]: +0 [Animation, Children's, Comedy] +1 [Adventure, Children's, Fantasy] +2 [Comedy, Romance] +3 [Comedy, Drama] +4 [Comedy] +Name: genres, dtype: object + +In [98]: movies["genre"] = movies.pop("genres").str.split("|") + +In [99]: movies.head() +Out[99]: + movie_id title +0 1 Toy Story (1995) \ +1 2 Jumanji (1995) +2 3 Grumpier Old Men (1995) +3 4 Waiting to Exhale (1995) +4 5 Father of the Bride Part II (1995) + genre +0 [Animation, Children's, Comedy] +1 [Adventure, Children's, Fantasy] +2 [Comedy, Romance] +3 [Comedy, Drama] +4 [Comedy] +``` + +现在,调用`movies.explode("genre")`会生成一个新的 DataFrame,其中每个电影类型列表中的“内部”元素都有一行。例如,如果一部电影被分类为喜剧和浪漫片,那么结果中将有两行,一行只有“喜剧”,另一行只有“浪漫片”: + +```py +In [100]: movies_exploded = movies.explode("genre") + +In [101]: movies_exploded[:10] +Out[101]: + movie_id title genre +0 1 Toy Story (1995) Animation +0 1 Toy Story (1995) Children's +0 1 Toy Story (1995) Comedy +1 2 Jumanji (1995) Adventure +1 2 Jumanji (1995) Children's +1 2 Jumanji (1995) Fantasy +2 3 Grumpier Old Men (1995) Comedy +2 3 Grumpier Old Men (1995) Romance +3 4 Waiting to Exhale (1995) Comedy +3 4 Waiting to Exhale (1995) Drama +``` + +现在,我们可以将所有三个表合并在一起,并按类型分组: + +```py +In [102]: ratings_with_genre = pd.merge(pd.merge(movies_exploded, ratings), users +) + +In [103]: ratings_with_genre.iloc[0] +Out[103]: +movie_id 1 +title Toy Story (1995) +genre Animation +user_id 1 +rating 5 +timestamp 978824268 +gender F +age 1 +occupation 10 +zip 48067 +Name: 0, dtype: object + +In [104]: genre_ratings = (ratings_with_genre.groupby(["genre", "age"]) + .....: ["rating"].mean() + .....: .unstack("age")) + +In [105]: genre_ratings[:10] +Out[105]: +age 1 18 25 35 45 50 +genre +Action 3.506385 3.447097 3.453358 3.538107 3.528543 3.611333 \ +Adventure 3.449975 3.408525 3.443163 3.515291 3.528963 3.628163 +Animation 3.476113 3.624014 3.701228 3.740545 3.734856 3.780020 +Children's 3.241642 3.294257 3.426873 3.518423 3.527593 3.556555 +Comedy 3.497491 3.460417 3.490385 3.561984 3.591789 3.646868 +Crime 3.710170 3.668054 3.680321 3.733736 3.750661 3.810688 +Documentary 3.730769 3.865865 3.946690 3.953747 3.966521 3.908108 +Drama 3.794735 3.721930 3.726428 3.782512 3.784356 3.878415 +Fantasy 3.317647 3.353778 3.452484 3.482301 3.532468 3.581570 +Film-Noir 4.145455 3.997368 4.058725 4.064910 4.105376 4.175401 +age 56 +genre +Action 3.610709 +Adventure 3.649064 +Animation 3.756233 +Children's 3.621822 +Comedy 3.650949 +Crime 3.832549 +Documentary 3.961538 +Drama 3.933465 +Fantasy 3.532700 +Film-Noir 4.125932 +``` + +## 13.3 美国婴儿姓名 1880-2010 + +美国社会保障管理局(SSA)提供了从 1880 年到现在的婴儿名字频率数据。Hadley Wickham,几个流行 R 包的作者,在 R 中说明数据操作时使用了这个数据集。 + +我们需要进行一些数据整理来加载这个数据集,但一旦我们这样做了,我们将得到一个看起来像这样的 DataFrame: + +```py +In [4]: names.head(10) +Out[4]: + name sex births year +0 Mary F 7065 1880 +1 Anna F 2604 1880 +2 Emma F 2003 1880 +3 Elizabeth F 1939 1880 +4 Minnie F 1746 1880 +5 Margaret F 1578 1880 +6 Ida F 1472 1880 +7 Alice F 1414 1880 +8 Bertha F 1320 1880 +9 Sarah F 1288 1880 +``` + +有许多事情你可能想要对数据集做: + ++ 可视化随着时间推移给定名字(您自己的名字或其他名字)的婴儿比例 + ++ 确定一个名字的相对排名 + ++ 确定每年最受欢迎的名字或受欢迎程度增长或下降最多的名字 + ++ 分析名字的趋势:元音、辅音、长度、整体多样性、拼写变化、首尾字母 + ++ 分析趋势的外部来源:圣经名字、名人、人口统计学 + +使用本书中的工具,许多这类分析都可以实现,所以我会带你走一些。 + +截至目前,美国社会保障管理局提供了数据文件,每年一个文件,其中包含每个性别/名字组合的总出生数。您可以下载这些文件的[原始存档](http://www.ssa.gov/oact/babynames/limits.html)。 + +如果您在阅读此页面时发现已移动,很可能可以通过互联网搜索再次找到。下载“国家数据”文件*names.zip*并解压缩后,您将获得一个包含一系列文件如*yob1880.txt*的目录。我使用 Unix 的`head`命令查看其中一个文件的前 10 行(在 Windows 上,您可以使用`more`命令或在文本编辑器中打开): + +```py +In [106]: !head -n 10 datasets/babynames/yob1880.txt +Mary,F,7065 +Anna,F,2604 +Emma,F,2003 +Elizabeth,F,1939 +Minnie,F,1746 +Margaret,F,1578 +Ida,F,1472 +Alice,F,1414 +Bertha,F,1320 +Sarah,F,1288 +``` + +由于这已经是逗号分隔形式,可以使用`pandas.read_csv`将其加载到 DataFrame 中: + +```py +In [107]: names1880 = pd.read_csv("datasets/babynames/yob1880.txt", + .....: names=["name", "sex", "births"]) + +In [108]: names1880 +Out[108]: + name sex births +0 Mary F 7065 +1 Anna F 2604 +2 Emma F 2003 +3 Elizabeth F 1939 +4 Minnie F 1746 +... ... .. ... +1995 Woodie M 5 +1996 Worthy M 5 +1997 Wright M 5 +1998 York M 5 +1999 Zachariah M 5 +[2000 rows x 3 columns] +``` + +这些文件只包含每年至少有五次出现的名字,所以为了简单起见,我们可以使用按性别的出生列的总和作为该年出生的总数: + +```py +In [109]: names1880.groupby("sex")["births"].sum() +Out[109]: +sex +F 90993 +M 110493 +Name: births, dtype: int64 +``` + +由于数据集按年份分成文件,首先要做的事情之一是将所有数据组装到一个单独的 DataFrame 中,并进一步添加一个`year`字段。您可以使用`pandas.concat`来做到这一点。在 Jupyter 单元格中运行以下内容: + +```py +pieces = [] +for year in range(1880, 2011): + path = f"datasets/babynames/yob{year}.txt" + frame = pd.read_csv(path, names=["name", "sex", "births"]) + + # Add a column for the year + frame["year"] = year + pieces.append(frame) + +# Concatenate everything into a single DataFrame +names = pd.concat(pieces, ignore_index=True) +``` + +这里有几件事情需要注意。首先,记住`concat`默认按行组合 DataFrame 对象。其次,您必须传递`ignore_index=True`,因为我们不关心从`pandas.read_csv`返回的原始行号。因此,现在我们有一个包含所有年份的所有名字数据的单个 DataFrame: + +```py +In [111]: names +Out[111]: + name sex births year +0 Mary F 7065 1880 +1 Anna F 2604 1880 +2 Emma F 2003 1880 +3 Elizabeth F 1939 1880 +4 Minnie F 1746 1880 +... ... .. ... ... +1690779 Zymaire M 5 2010 +1690780 Zyonne M 5 2010 +1690781 Zyquarius M 5 2010 +1690782 Zyran M 5 2010 +1690783 Zzyzx M 5 2010 +[1690784 rows x 4 columns] +``` + +有了这些数据,我们可以开始使用`groupby`或`pivot_table`在年份和性别水平上对数据进行聚合(参见按性别和年份统计的总出生数): + +```py +In [112]: total_births = names.pivot_table("births", index="year", + .....: columns="sex", aggfunc=sum) + +In [113]: total_births.tail() +Out[113]: +sex F M +year +2006 1896468 2050234 +2007 1916888 2069242 +2008 1883645 2032310 +2009 1827643 1973359 +2010 1759010 1898382 + +In [114]: total_births.plot(title="Total births by sex and year") +``` + +![](img/6716e4c3fc8eb1a44199a90351da2a0d.png) + +图 13.4:按性别和年份统计的总出生数 + +接下来,让我们插入一个名为`prop`的列,该列显示每个名字相对于总出生数的比例。`prop`值为`0.02`表示每 100 个婴儿中有 2 个被赋予特定的名字。因此,我们按年份和性别对数据进行分组,然后向每个组添加新列: + +```py +def add_prop(group): + group["prop"] = group["births"] / group["births"].sum() + return group +names = names.groupby(["year", "sex"], group_keys=False).apply(add_prop) +``` + +现在得到的完整数据集现在具有以下列: + +```py +In [116]: names +Out[116]: + name sex births year prop +0 Mary F 7065 1880 0.077643 +1 Anna F 2604 1880 0.028618 +2 Emma F 2003 1880 0.022013 +3 Elizabeth F 1939 1880 0.021309 +4 Minnie F 1746 1880 0.019188 +... ... .. ... ... ... +1690779 Zymaire M 5 2010 0.000003 +1690780 Zyonne M 5 2010 0.000003 +1690781 Zyquarius M 5 2010 0.000003 +1690782 Zyran M 5 2010 0.000003 +1690783 Zzyzx M 5 2010 0.000003 +[1690784 rows x 5 columns] +``` + +在执行这样的组操作时,通常很有价值进行一些合理性检查,比如验证所有组中`prop`列的总和是否为 1: + +```py +In [117]: names.groupby(["year", "sex"])["prop"].sum() +Out[117]: +year sex +1880 F 1.0 + M 1.0 +1881 F 1.0 + M 1.0 +1882 F 1.0 + ... +2008 M 1.0 +2009 F 1.0 + M 1.0 +2010 F 1.0 + M 1.0 +Name: prop, Length: 262, dtype: float64 +``` + +现在这样做了,我将提取数据的一个子集以便进一步分析:每个性别/年份组合的前 1000 个名字。这是另一个组操作: + +```py +In [118]: def get_top1000(group): + .....: return group.sort_values("births", ascending=False)[:1000] + +In [119]: grouped = names.groupby(["year", "sex"]) + +In [120]: top1000 = grouped.apply(get_top1000) + +In [121]: top1000.head() +Out[121]: + name sex births year prop +year sex +1880 F 0 Mary F 7065 1880 0.077643 + 1 Anna F 2604 1880 0.028618 + 2 Emma F 2003 1880 0.022013 + 3 Elizabeth F 1939 1880 0.021309 + 4 Minnie F 1746 1880 0.019188 +``` + +我们可以删除组索引,因为我们不需要它进行分析: + +```py +In [122]: top1000 = top1000.reset_index(drop=True) +``` + +现在得到的数据集要小得多: + +```py +In [123]: top1000.head() +Out[123]: + name sex births year prop +0 Mary F 7065 1880 0.077643 +1 Anna F 2604 1880 0.028618 +2 Emma F 2003 1880 0.022013 +3 Elizabeth F 1939 1880 0.021309 +4 Minnie F 1746 1880 0.019188 +``` + +我们将在接下来的数据调查中使用这个前一千个数据集。 + +### 分析命名趋势 + +有了完整的数据集和前一千个数据集,我们可以开始分析各种有趣的命名趋势。首先,我们可以将前一千个名字分为男孩和女孩部分: + +```py +In [124]: boys = top1000[top1000["sex"] == "M"] + +In [125]: girls = top1000[top1000["sex"] == "F"] +``` + +简单的时间序列,比如每年约翰或玛丽的数量,可以绘制,但需要一些操作才能更有用。让我们形成一个按年份和姓名总数的数据透视表: + +```py +In [126]: total_births = top1000.pivot_table("births", index="year", + .....: columns="name", + .....: aggfunc=sum) +``` + +现在,可以使用 DataFrame 的`plot`方法为一些名字绘制图表(一些男孩和女孩名字随时间变化显示了结果): + +```py +In [127]: total_births.info() + +Index: 131 entries, 1880 to 2010 +Columns: 6868 entries, Aaden to Zuri +dtypes: float64(6868) +memory usage: 6.9 MB + +In [128]: subset = total_births[["John", "Harry", "Mary", "Marilyn"]] + +In [129]: subset.plot(subplots=True, figsize=(12, 10), + .....: title="Number of births per year") +``` + +![](img/e1a3230ba5c578a21a784b09be9c0e91.png) + +图 13.5:一些男孩和女孩名字随时间变化 + +看到这个,你可能会得出结论,这些名字已经不再受到美国人口的青睐。但事实实际上比这更复杂,将在下一节中探讨。 + +#### 衡量命名多样性的增加 + +减少图表的原因之一是越来越少的父母选择常见的名字给他们的孩子。这个假设可以在数据中进行探索和确认。一个度量是由前 1000 个最受欢迎的名字代表的出生比例,我按年份和性别进行汇总和绘制(性别在前一千个名字中所代表的出生比例显示了结果图): + +```py +In [131]: table = top1000.pivot_table("prop", index="year", + .....: columns="sex", aggfunc=sum) + +In [132]: table.plot(title="Sum of table1000.prop by year and sex", + .....: yticks=np.linspace(0, 1.2, 13)) +``` + +![](img/24e093fb2856ccb1ffa83d884da1713d.png) + +图 13.6:性别在前一千个名字中所代表的出生比例 + +您可以看到,确实存在着越来越多的名字多样性(前一千名中总比例减少)。另一个有趣的指标是在出生的前 50%中按照从高到低的流行度顺序取的不同名字的数量。这个数字更难计算。让我们只考虑 2010 年的男孩名字: + +```py +In [133]: df = boys[boys["year"] == 2010] + +In [134]: df +Out[134]: + name sex births year prop +260877 Jacob M 21875 2010 0.011523 +260878 Ethan M 17866 2010 0.009411 +260879 Michael M 17133 2010 0.009025 +260880 Jayden M 17030 2010 0.008971 +260881 William M 16870 2010 0.008887 +... ... .. ... ... ... +261872 Camilo M 194 2010 0.000102 +261873 Destin M 194 2010 0.000102 +261874 Jaquan M 194 2010 0.000102 +261875 Jaydan M 194 2010 0.000102 +261876 Maxton M 193 2010 0.000102 +[1000 rows x 5 columns] +``` + +在对`prop`进行降序排序后,我们想知道最受欢迎的名字中有多少个名字达到了 50%。您可以编写一个`for`循环来执行此操作,但使用矢量化的 NumPy 方法更具计算效率。对`prop`进行累积求和`cumsum`,然后调用`searchsorted`方法返回`0.5`需要插入的累积和位置,以保持其按顺序排序: + +```py +In [135]: prop_cumsum = df["prop"].sort_values(ascending=False).cumsum() + +In [136]: prop_cumsum[:10] +Out[136]: +260877 0.011523 +260878 0.020934 +260879 0.029959 +260880 0.038930 +260881 0.047817 +260882 0.056579 +260883 0.065155 +260884 0.073414 +260885 0.081528 +260886 0.089621 +Name: prop, dtype: float64 + +In [137]: prop_cumsum.searchsorted(0.5) +Out[137]: 116 +``` + +由于数组是从零开始索引的,将此结果加 1 将得到 117 的结果。相比之下,在 1900 年,这个数字要小得多: + +```py +In [138]: df = boys[boys.year == 1900] + +In [139]: in1900 = df.sort_values("prop", ascending=False).prop.cumsum() + +In [140]: in1900.searchsorted(0.5) + 1 +Out[140]: 25 +``` + +现在,您可以将此操作应用于每个年份/性别组合,对这些字段进行`groupby`,并`apply`一个返回每个组计数的函数: + +```py +def get_quantile_count(group, q=0.5): + group = group.sort_values("prop", ascending=False) + return group.prop.cumsum().searchsorted(q) + 1 + +diversity = top1000.groupby(["year", "sex"]).apply(get_quantile_count) +diversity = diversity.unstack() +``` + +这个结果 DataFrame `diversity` 现在有两个时间序列,一个用于每个性别,按年份索引。这可以像以前一样进行检查和绘制(参见按年份绘制的多样性指标): + +```py +In [143]: diversity.head() +Out[143]: +sex F M +year +1880 38 14 +1881 38 14 +1882 38 15 +1883 39 15 +1884 39 16 + +In [144]: diversity.plot(title="Number of popular names in top 50%") +``` + +![](img/1e1bea5330bbf1ad2202b26ba95095b8.png) + +图 13.7:按年份绘制的多样性指标 + +正如你所看到的,女孩名字一直比男孩名字更多样化,而且随着时间的推移,它们变得更加多样化。关于到底是什么推动了这种多样性的进一步分析,比如替代拼写的增加,留给读者自行探讨。 + +#### “最后一个字母”革命 + +在 2007 年,婴儿姓名研究员劳拉·瓦滕伯格指出,过去 100 年来,以最后一个字母结尾的男孩名字的分布发生了显著变化。为了看到这一点,我们首先按年份、性别和最后一个字母聚合完整数据集中的所有出生情况: + +```py +def get_last_letter(x): + return x[-1] + +last_letters = names["name"].map(get_last_letter) +last_letters.name = "last_letter" + +table = names.pivot_table("births", index=last_letters, + columns=["sex", "year"], aggfunc=sum) +``` + +然后我们选择三个代表性年份跨越历史,并打印前几行: + +```py +In [146]: subtable = table.reindex(columns=[1910, 1960, 2010], level="year") + +In [147]: subtable.head() +Out[147]: +sex F M +year 1910 1960 2010 1910 1960 2010 +last_letter +a 108376.0 691247.0 670605.0 977.0 5204.0 28438.0 +b NaN 694.0 450.0 411.0 3912.0 38859.0 +c 5.0 49.0 946.0 482.0 15476.0 23125.0 +d 6750.0 3729.0 2607.0 22111.0 262112.0 44398.0 +e 133569.0 435013.0 313833.0 28655.0 178823.0 129012.0 +``` + +接下来,通过总出生数对表进行标准化,计算一个包含每个性别以每个字母结尾的总出生比例的新表: + +```py +In [148]: subtable.sum() +Out[148]: +sex year +F 1910 396416.0 + 1960 2022062.0 + 2010 1759010.0 +M 1910 194198.0 + 1960 2132588.0 + 2010 1898382.0 +dtype: float64 + +In [149]: letter_prop = subtable / subtable.sum() + +In [150]: letter_prop +Out[150]: +sex F M +year 1910 1960 2010 1910 1960 2010 +last_letter +a 0.273390 0.341853 0.381240 0.005031 0.002440 0.014980 +b NaN 0.000343 0.000256 0.002116 0.001834 0.020470 +c 0.000013 0.000024 0.000538 0.002482 0.007257 0.012181 +d 0.017028 0.001844 0.001482 0.113858 0.122908 0.023387 +e 0.336941 0.215133 0.178415 0.147556 0.083853 0.067959 +... ... ... ... ... ... ... +v NaN 0.000060 0.000117 0.000113 0.000037 0.001434 +w 0.000020 0.000031 0.001182 0.006329 0.007711 0.016148 +x 0.000015 0.000037 0.000727 0.003965 0.001851 0.008614 +y 0.110972 0.152569 0.116828 0.077349 0.160987 0.058168 +z 0.002439 0.000659 0.000704 0.000170 0.000184 0.001831 +[26 rows x 6 columns] +``` + +现在有了字母比例,我们可以按年份将每个性别分解为条形图(参见以每个字母结尾的男孩和女孩名字的比例): + +```py +import matplotlib.pyplot as plt + +fig, axes = plt.subplots(2, 1, figsize=(10, 8)) +letter_prop["M"].plot(kind="bar", rot=0, ax=axes[0], title="Male") +letter_prop["F"].plot(kind="bar", rot=0, ax=axes[1], title="Female", + legend=False) +``` + +![](img/5f557600f8cbeb90fd07375592d49c18.png) + +图 13.8:以每个字母结尾的男孩和女孩名字的比例 + +正如您所看到的,自 20 世纪 60 年代以来,以*n*结尾的男孩名字经历了显著增长。回到之前创建的完整表格,再次按年份和性别进行标准化,并选择男孩名字的一部分字母,最后转置使每一列成为一个时间序列: + +```py +In [153]: letter_prop = table / table.sum() + +In [154]: dny_ts = letter_prop.loc[["d", "n", "y"], "M"].T + +In [155]: dny_ts.head() +Out[155]: +last_letter d n y +year +1880 0.083055 0.153213 0.075760 +1881 0.083247 0.153214 0.077451 +1882 0.085340 0.149560 0.077537 +1883 0.084066 0.151646 0.079144 +1884 0.086120 0.149915 0.080405 +``` + +有了这个时间序列的 DataFrame,我可以再次使用其`plot`方法制作时间趋势图(请参见随时间变化以 d/n/y 结尾的男孩出生比例): + +```py +In [158]: dny_ts.plot() +``` + +![](img/e7ac4057ad5de3681057e0690540ab39.png) + +图 13.9:随时间变化以 d/n/y 结尾的男孩出生比例 + +#### 男孩名字变成女孩名字(反之亦然) + +另一个有趣的趋势是查看在样本早期更受一性别欢迎,但随着时间推移已成为另一性别的首选名字的名字。一个例子是 Lesley 或 Leslie 这个名字。回到`top1000` DataFrame,我计算出数据集中以"Lesl"开头的名字列表: + +```py +In [159]: all_names = pd.Series(top1000["name"].unique()) + +In [160]: lesley_like = all_names[all_names.str.contains("Lesl")] + +In [161]: lesley_like +Out[161]: +632 Leslie +2294 Lesley +4262 Leslee +4728 Lesli +6103 Lesly +dtype: object +``` + +然后,我们可以筛选出那些名字,按名字分组对出生进行求和,以查看相对频率: + +```py +In [162]: filtered = top1000[top1000["name"].isin(lesley_like)] + +In [163]: filtered.groupby("name")["births"].sum() +Out[163]: +name +Leslee 1082 +Lesley 35022 +Lesli 929 +Leslie 370429 +Lesly 10067 +Name: births, dtype: int64 +``` + +接下来,让我们按性别和年份进行聚合,并在年份内进行归一化: + +```py +In [164]: table = filtered.pivot_table("births", index="year", + .....: columns="sex", aggfunc="sum") + +In [165]: table = table.div(table.sum(axis="columns"), axis="index") + +In [166]: table.tail() +Out[166]: +sex F M +year +2006 1.0 NaN +2007 1.0 NaN +2008 1.0 NaN +2009 1.0 NaN +2010 1.0 NaN +``` + +最后,现在可以制作按性别随时间变化的分布图(请参见随时间变化男/女 Lesley 样式名字的比例): + +```py +In [168]: table.plot(style={"M": "k-", "F": "k--"}) +``` + +![](img/61d4a67e0c93651a04b70d68220275c0.png) + +图 13.10:随时间变化男/女 Lesley 样式名字的比例 + +## 13.4 USDA 食品数据库 + +美国农业部(USDA)提供了一个食品营养信息数据库。程序员 Ashley Williams 以 JSON 格式创建了这个数据库的一个版本。记录看起来像这样: + +```py +{ + "id": 21441, + "description": "KENTUCKY FRIED CHICKEN, Fried Chicken, EXTRA CRISPY, +Wing, meat and skin with breading", + "tags": ["KFC"], + "manufacturer": "Kentucky Fried Chicken", + "group": "Fast Foods", + "portions": [ + { + "amount": 1, + "unit": "wing, with skin", + "grams": 68.0 + }, + + ... + ], + "nutrients": [ + { + "value": 20.8, + "units": "g", + "description": "Protein", + "group": "Composition" + }, + + ... + ] +} +``` + +每种食物都有一些标识属性,还有两个营养素和分量大小的列表。这种形式的数据不太适合分析,因此我们需要做一些工作,将数据整理成更好的形式。 + +您可以使用您选择的任何 JSON 库将此文件加载到 Python 中。我将使用内置的 Python `json`模块: + +```py +In [169]: import json + +In [170]: db = json.load(open("datasets/usda_food/database.json")) + +In [171]: len(db) +Out[171]: 6636 +``` + +`db`中的每个条目都是一个包含单个食物所有数据的字典。`"nutrients"`字段是一个字典列表,每个营养素一个: + +```py +In [172]: db[0].keys() +Out[172]: dict_keys(['id', 'description', 'tags', 'manufacturer', 'group', 'porti +ons', 'nutrients']) + +In [173]: db[0]["nutrients"][0] +Out[173]: +{'value': 25.18, + 'units': 'g', + 'description': 'Protein', + 'group': 'Composition'} + +In [174]: nutrients = pd.DataFrame(db[0]["nutrients"]) + +In [175]: nutrients.head(7) +Out[175]: + value units description group +0 25.18 g Protein Composition +1 29.20 g Total lipid (fat) Composition +2 3.06 g Carbohydrate, by difference Composition +3 3.28 g Ash Other +4 376.00 kcal Energy Energy +5 39.28 g Water Composition +6 1573.00 kJ Energy Energy +``` + +将字典列表转换为 DataFrame 时,我们可以指定要提取的字段列表。我们将提取食物名称、组、ID 和制造商: + +```py +In [176]: info_keys = ["description", "group", "id", "manufacturer"] + +In [177]: info = pd.DataFrame(db, columns=info_keys) + +In [178]: info.head() +Out[178]: + description group id +0 Cheese, caraway Dairy and Egg Products 1008 \ +1 Cheese, cheddar Dairy and Egg Products 1009 +2 Cheese, edam Dairy and Egg Products 1018 +3 Cheese, feta Dairy and Egg Products 1019 +4 Cheese, mozzarella, part skim milk Dairy and Egg Products 1028 + manufacturer +0 +1 +2 +3 +4 + +In [179]: info.info() + +RangeIndex: 6636 entries, 0 to 6635 +Data columns (total 4 columns): + # Column Non-Null Count Dtype +--- ------ -------------- ----- + 0 description 6636 non-null object + 1 group 6636 non-null object + 2 id 6636 non-null int64 + 3 manufacturer 5195 non-null object +dtypes: int64(1), object(3) +memory usage: 207.5+ KB +``` + +从`info.info()`的输出中,我们可以看到`manufacturer`列中有缺失数据。 + +您可以使用`value_counts`查看食物组的分布: + +```py +In [180]: pd.value_counts(info["group"])[:10] +Out[180]: +group +Vegetables and Vegetable Products 812 +Beef Products 618 +Baked Products 496 +Breakfast Cereals 403 +Legumes and Legume Products 365 +Fast Foods 365 +Lamb, Veal, and Game Products 345 +Sweets 341 +Fruits and Fruit Juices 328 +Pork Products 328 +Name: count, dtype: int64 +``` + +现在,要对所有营养数据进行一些分析,最简单的方法是将每种食物的营养成分组装成一个单独的大表格。为此,我们需要采取几个步骤。首先,我将把每个食物营养列表转换为一个 DataFrame,添加一个食物`id`的列,并将 DataFrame 附加到列表中。然后,可以使用`concat`将它们连接起来。在 Jupyter 单元格中运行以下代码: + +```py +nutrients = [] + +for rec in db: + fnuts = pd.DataFrame(rec["nutrients"]) + fnuts["id"] = rec["id"] + nutrients.append(fnuts) + +nutrients = pd.concat(nutrients, ignore_index=True) +``` + +如果一切顺利,`nutrients`应该是这样的: + +```py +In [182]: nutrients +Out[182]: + value units description group id +0 25.180 g Protein Composition 1008 +1 29.200 g Total lipid (fat) Composition 1008 +2 3.060 g Carbohydrate, by difference Composition 1008 +3 3.280 g Ash Other 1008 +4 376.000 kcal Energy Energy 1008 +... ... ... ... ... ... +389350 0.000 mcg Vitamin B-12, added Vitamins 43546 +389351 0.000 mg Cholesterol Other 43546 +389352 0.072 g Fatty acids, total saturated Other 43546 +389353 0.028 g Fatty acids, total monounsaturated Other 43546 +389354 0.041 g Fatty acids, total polyunsaturated Other 43546 +[389355 rows x 5 columns] +``` + +我注意到这个 DataFrame 中有重复项,所以删除它们会更容易: + +```py +In [183]: nutrients.duplicated().sum() # number of duplicates +Out[183]: 14179 + +In [184]: nutrients = nutrients.drop_duplicates() +``` + +由于 DataFrame 对象中都有`"group"`和`"description"`,我们可以重命名以便更清晰: + +```py +In [185]: col_mapping = {"description" : "food", + .....: "group" : "fgroup"} + +In [186]: info = info.rename(columns=col_mapping, copy=False) + +In [187]: info.info() + +RangeIndex: 6636 entries, 0 to 6635 +Data columns (total 4 columns): + # Column Non-Null Count Dtype +--- ------ -------------- ----- + 0 food 6636 non-null object + 1 fgroup 6636 non-null object + 2 id 6636 non-null int64 + 3 manufacturer 5195 non-null object +dtypes: int64(1), object(3) +memory usage: 207.5+ KB + +In [188]: col_mapping = {"description" : "nutrient", + .....: "group" : "nutgroup"} + +In [189]: nutrients = nutrients.rename(columns=col_mapping, copy=False) + +In [190]: nutrients +Out[190]: + value units nutrient nutgroup id +0 25.180 g Protein Composition 1008 +1 29.200 g Total lipid (fat) Composition 1008 +2 3.060 g Carbohydrate, by difference Composition 1008 +3 3.280 g Ash Other 1008 +4 376.000 kcal Energy Energy 1008 +... ... ... ... ... ... +389350 0.000 mcg Vitamin B-12, added Vitamins 43546 +389351 0.000 mg Cholesterol Other 43546 +389352 0.072 g Fatty acids, total saturated Other 43546 +389353 0.028 g Fatty acids, total monounsaturated Other 43546 +389354 0.041 g Fatty acids, total polyunsaturated Other 43546 +[375176 rows x 5 columns] +``` + +完成所有这些后,我们准备将`info`与`nutrients`合并: + +```py +In [191]: ndata = pd.merge(nutrients, info, on="id") + +In [192]: ndata.info() + +RangeIndex: 375176 entries, 0 to 375175 +Data columns (total 8 columns): + # Column Non-Null Count Dtype +--- ------ -------------- ----- + 0 value 375176 non-null float64 + 1 units 375176 non-null object + 2 nutrient 375176 non-null object + 3 nutgroup 375176 non-null object + 4 id 375176 non-null int64 + 5 food 375176 non-null object + 6 fgroup 375176 non-null object + 7 manufacturer 293054 non-null object +dtypes: float64(1), int64(1), object(6) +memory usage: 22.9+ MB + +In [193]: ndata.iloc[30000] +Out[193]: +value 0.04 +units g +nutrient Glycine +nutgroup Amino Acids +id 6158 +food Soup, tomato bisque, canned, condensed +fgroup Soups, Sauces, and Gravies +manufacturer +Name: 30000, dtype: object +``` + +现在我们可以制作按食物组和营养类型中位数值的图表(请参见各食物组的锌中位数值): + +```py +In [195]: result = ndata.groupby(["nutrient", "fgroup"])["value"].quantile(0.5) + +In [196]: result["Zinc, Zn"].sort_values().plot(kind="barh") +``` + +![](img/c65abbdbb1b538d16ea4f45c46581a89.png) + +图 13.11:各食物组的锌中位数值 + +使用`idxmax`或`argmax` Series 方法,您可以找到每种营养素中最密集的食物。在 Jupyter 单元格中运行以下内容: + +```py +by_nutrient = ndata.groupby(["nutgroup", "nutrient"]) + +def get_maximum(x): + return x.loc[x.value.idxmax()] + +max_foods = by_nutrient.apply(get_maximum)[["value", "food"]] + +# make the food a little smaller +max_foods["food"] = max_foods["food"].str[:50] +``` + +生成的 DataFrame 太大,无法在书中显示;这里只有`"Amino Acids"`营养组: + +```py +In [198]: max_foods.loc["Amino Acids"]["food"] +Out[198]: +nutrient +Alanine Gelatins, dry powder, unsweetened +Arginine Seeds, sesame flour, low-fat +Aspartic acid Soy protein isolate +Cystine Seeds, cottonseed flour, low fat (glandless) +Glutamic acid Soy protein isolate +Glycine Gelatins, dry powder, unsweetened +Histidine Whale, beluga, meat, dried (Alaska Native) +Hydroxyproline KENTUCKY FRIED CHICKEN, Fried Chicken, ORIGINAL RE +Isoleucine Soy protein isolate, PROTEIN TECHNOLOGIES INTERNAT +Leucine Soy protein isolate, PROTEIN TECHNOLOGIES INTERNAT +Lysine Seal, bearded (Oogruk), meat, dried (Alaska Native +Methionine Fish, cod, Atlantic, dried and salted +Phenylalanine Soy protein isolate, PROTEIN TECHNOLOGIES INTERNAT +Proline Gelatins, dry powder, unsweetened +Serine Soy protein isolate, PROTEIN TECHNOLOGIES INTERNAT +Threonine Soy protein isolate, PROTEIN TECHNOLOGIES INTERNAT +Tryptophan Sea lion, Steller, meat with fat (Alaska Native) +Tyrosine Soy protein isolate, PROTEIN TECHNOLOGIES INTERNAT +Valine Soy protein isolate, PROTEIN TECHNOLOGIES INTERNAT +Name: food, dtype: object +``` + +## 13.5 2012 年联邦选举委员会数据库 + +美国联邦选举委员会(FEC)发布了有关政治竞选捐款的数据。这包括捐助者姓名、职业和雇主、地址以及捐款金额。2012 年美国总统选举的捐款数据作为一个 150 兆字节的 CSV 文件*P00000001-ALL.csv*可用(请参阅本书的数据存储库),可以使用`pandas.read_csv`加载: + +```py +In [199]: fec = pd.read_csv("datasets/fec/P00000001-ALL.csv", low_memory=False) + +In [200]: fec.info() + +RangeIndex: 1001731 entries, 0 to 1001730 +Data columns (total 16 columns): + # Column Non-Null Count Dtype +--- ------ -------------- ----- + 0 cmte_id 1001731 non-null object + 1 cand_id 1001731 non-null object + 2 cand_nm 1001731 non-null object + 3 contbr_nm 1001731 non-null object + 4 contbr_city 1001712 non-null object + 5 contbr_st 1001727 non-null object + 6 contbr_zip 1001620 non-null object + 7 contbr_employer 988002 non-null object + 8 contbr_occupation 993301 non-null object + 9 contb_receipt_amt 1001731 non-null float64 + 10 contb_receipt_dt 1001731 non-null object + 11 receipt_desc 14166 non-null object + 12 memo_cd 92482 non-null object + 13 memo_text 97770 non-null object + 14 form_tp 1001731 non-null object + 15 file_num 1001731 non-null int64 +dtypes: float64(1), int64(1), object(14) +memory usage: 122.3+ MB +``` + +注意: + +有几个人要求我将数据集从 2012 年选举更新到 2016 年或 2020 年选举。不幸的是,联邦选举委员会提供的最新数据集变得更大更复杂,我决定在这里使用它们会分散我想要说明的分析技术。 + +数据框中的一个示例记录如下: + +```py +In [201]: fec.iloc[123456] +Out[201]: +cmte_id C00431445 +cand_id P80003338 +cand_nm Obama, Barack +contbr_nm ELLMAN, IRA +contbr_city TEMPE +contbr_st AZ +contbr_zip 852816719 +contbr_employer ARIZONA STATE UNIVERSITY +contbr_occupation PROFESSOR +contb_receipt_amt 50.0 +contb_receipt_dt 01-DEC-11 +receipt_desc NaN +memo_cd NaN +memo_text NaN +form_tp SA17A +file_num 772372 +Name: 123456, dtype: object +``` + +您可能会想到一些方法来开始切片和切块这些数据,以提取有关捐赠者和竞选捐款模式的信息统计。我将展示一些应用本书中技术的不同分析方法。 + +您会发现数据中没有政党隶属关系,因此添加这些信息会很有用。您可以使用`unique`获取所有唯一的政治候选人列表: + +```py +In [202]: unique_cands = fec["cand_nm"].unique() + +In [203]: unique_cands +Out[203]: +array(['Bachmann, Michelle', 'Romney, Mitt', 'Obama, Barack', + "Roemer, Charles E. 'Buddy' III", 'Pawlenty, Timothy', + 'Johnson, Gary Earl', 'Paul, Ron', 'Santorum, Rick', + 'Cain, Herman', 'Gingrich, Newt', 'McCotter, Thaddeus G', + 'Huntsman, Jon', 'Perry, Rick'], dtype=object) + +In [204]: unique_cands[2] +Out[204]: 'Obama, Barack' +``` + +表示政党隶属关系的一种方法是使用字典:¹ + +```py +parties = {"Bachmann, Michelle": "Republican", + "Cain, Herman": "Republican", + "Gingrich, Newt": "Republican", + "Huntsman, Jon": "Republican", + "Johnson, Gary Earl": "Republican", + "McCotter, Thaddeus G": "Republican", + "Obama, Barack": "Democrat", + "Paul, Ron": "Republican", + "Pawlenty, Timothy": "Republican", + "Perry, Rick": "Republican", + "Roemer, Charles E. 'Buddy' III": "Republican", + "Romney, Mitt": "Republican", + "Santorum, Rick": "Republican"} +``` + +现在,使用这个映射和 Series 对象上的`map`方法,您可以从候选人姓名计算一个政党数组: + +```py +In [206]: fec["cand_nm"][123456:123461] +Out[206]: +123456 Obama, Barack +123457 Obama, Barack +123458 Obama, Barack +123459 Obama, Barack +123460 Obama, Barack +Name: cand_nm, dtype: object + +In [207]: fec["cand_nm"][123456:123461].map(parties) +Out[207]: +123456 Democrat +123457 Democrat +123458 Democrat +123459 Democrat +123460 Democrat +Name: cand_nm, dtype: object + +# Add it as a column +In [208]: fec["party"] = fec["cand_nm"].map(parties) + +In [209]: fec["party"].value_counts() +Out[209]: +party +Democrat 593746 +Republican 407985 +Name: count, dtype: int64 +``` + +一些数据准备要点。首先,这些数据包括捐款和退款(负捐款金额): + +```py +In [210]: (fec["contb_receipt_amt"] > 0).value_counts() +Out[210]: +contb_receipt_amt +True 991475 +False 10256 +Name: count, dtype: int64 +``` + +为简化分析,我将限制数据集为正捐款: + +```py +In [211]: fec = fec[fec["contb_receipt_amt"] > 0] +``` + +由于巴拉克·奥巴马和米特·罗姆尼是主要的两位候选人,我还将准备一个只包含对他们竞选活动的捐款的子集: + +```py +In [212]: fec_mrbo = fec[fec["cand_nm"].isin(["Obama, Barack", "Romney, Mitt"])] +``` + +### 按职业和雇主的捐款统计 + +按职业捐款是另一个经常研究的统计数据。例如,律师倾向于向民主党捐款更多,而商业高管倾向于向共和党捐款更多。您没有理由相信我;您可以在数据中自己看到。首先,可以使用`value_counts`计算每个职业的总捐款数: + +```py +In [213]: fec["contbr_occupation"].value_counts()[:10] +Out[213]: +contbr_occupation +RETIRED 233990 +INFORMATION REQUESTED 35107 +ATTORNEY 34286 +HOMEMAKER 29931 +PHYSICIAN 23432 +INFORMATION REQUESTED PER BEST EFFORTS 21138 +ENGINEER 14334 +TEACHER 13990 +CONSULTANT 13273 +PROFESSOR 12555 +Name: count, dtype: int64 +``` + +通过查看职业,您会注意到许多职业都指的是相同的基本工作类型,或者有几种相同事物的变体。以下代码片段演示了一种通过从一个职业映射到另一个职业来清理其中一些职业的技术;请注意使用`dict.get`的“技巧”,以允许没有映射的职业“通过”: + +```py +occ_mapping = { + "INFORMATION REQUESTED PER BEST EFFORTS" : "NOT PROVIDED", + "INFORMATION REQUESTED" : "NOT PROVIDED", + "INFORMATION REQUESTED (BEST EFFORTS)" : "NOT PROVIDED", + "C.E.O.": "CEO" +} + +def get_occ(x): + # If no mapping provided, return x + return occ_mapping.get(x, x) + +fec["contbr_occupation"] = fec["contbr_occupation"].map(get_occ) +``` + +我也会为雇主做同样的事情: + +```py +emp_mapping = { + "INFORMATION REQUESTED PER BEST EFFORTS" : "NOT PROVIDED", + "INFORMATION REQUESTED" : "NOT PROVIDED", + "SELF" : "SELF-EMPLOYED", + "SELF EMPLOYED" : "SELF-EMPLOYED", +} + +def get_emp(x): + # If no mapping provided, return x + return emp_mapping.get(x, x) + +fec["contbr_employer"] = fec["contbr_employer"].map(get_emp) +``` + +现在,您可以使用`pivot_table`按政党和职业对数据进行聚合,然后筛选出总捐款至少为 200 万美元的子集: + +```py +In [216]: by_occupation = fec.pivot_table("contb_receipt_amt", + .....: index="contbr_occupation", + .....: columns="party", aggfunc="sum") + +In [217]: over_2mm = by_occupation[by_occupation.sum(axis="columns") > 2000000] + +In [218]: over_2mm +Out[218]: +party Democrat Republican +contbr_occupation +ATTORNEY 11141982.97 7477194.43 +CEO 2074974.79 4211040.52 +CONSULTANT 2459912.71 2544725.45 +ENGINEER 951525.55 1818373.70 +EXECUTIVE 1355161.05 4138850.09 +HOMEMAKER 4248875.80 13634275.78 +INVESTOR 884133.00 2431768.92 +LAWYER 3160478.87 391224.32 +MANAGER 762883.22 1444532.37 +NOT PROVIDED 4866973.96 20565473.01 +OWNER 1001567.36 2408286.92 +PHYSICIAN 3735124.94 3594320.24 +PRESIDENT 1878509.95 4720923.76 +PROFESSOR 2165071.08 296702.73 +REAL ESTATE 528902.09 1625902.25 +RETIRED 25305116.38 23561244.49 +SELF-EMPLOYED 672393.40 1640252.54 +``` + +这些数据以条形图形式更容易查看(`"barh"`表示水平条形图;请参见按职业和政党分组的总捐款): + +```py +In [220]: over_2mm.plot(kind="barh") +``` + +![](img/f4e8c3c83f755658e00aaadb7414d336.png) + +图 13.12:按职业分组的政党总捐款 + +您可能对捐赠最多的职业或向奥巴马和罗姆尼捐款最多的公司感兴趣。为此,您可以按候选人姓名分组,并使用本章早期的`top`方法的变体: + +```py +def get_top_amounts(group, key, n=5): + totals = group.groupby(key)["contb_receipt_amt"].sum() + return totals.nlargest(n) +``` + +然后按职业和雇主进行汇总: + +```py +In [222]: grouped = fec_mrbo.groupby("cand_nm") + +In [223]: grouped.apply(get_top_amounts, "contbr_occupation", n=7) +Out[223]: +cand_nm contbr_occupation +Obama, Barack RETIRED 25305116.38 + ATTORNEY 11141982.97 + INFORMATION REQUESTED 4866973.96 + HOMEMAKER 4248875.80 + PHYSICIAN 3735124.94 + LAWYER 3160478.87 + CONSULTANT 2459912.71 +Romney, Mitt RETIRED 11508473.59 + INFORMATION REQUESTED PER BEST EFFORTS 11396894.84 + HOMEMAKER 8147446.22 + ATTORNEY 5364718.82 + PRESIDENT 2491244.89 + EXECUTIVE 2300947.03 + C.E.O. 1968386.11 +Name: contb_receipt_amt, dtype: float64 + +In [224]: grouped.apply(get_top_amounts, "contbr_employer", n=10) +Out[224]: +cand_nm contbr_employer +Obama, Barack RETIRED 22694358.85 + SELF-EMPLOYED 17080985.96 + NOT EMPLOYED 8586308.70 + INFORMATION REQUESTED 5053480.37 + HOMEMAKER 2605408.54 + SELF 1076531.20 + SELF EMPLOYED 469290.00 + STUDENT 318831.45 + VOLUNTEER 257104.00 + MICROSOFT 215585.36 +Romney, Mitt INFORMATION REQUESTED PER BEST EFFORTS 12059527.24 + RETIRED 11506225.71 + HOMEMAKER 8147196.22 + SELF-EMPLOYED 7409860.98 + STUDENT 496490.94 + CREDIT SUISSE 281150.00 + MORGAN STANLEY 267266.00 + GOLDMAN SACH & CO. 238250.00 + BARCLAYS CAPITAL 162750.00 + H.I.G. CAPITAL 139500.00 +Name: contb_receipt_amt, dtype: float64 +``` + +### 将捐款金额分桶 + +分析这些数据的一个有用方法是使用`cut`函数将捐助金额分成不同的桶: + +```py +In [225]: bins = np.array([0, 1, 10, 100, 1000, 10000, + .....: 100_000, 1_000_000, 10_000_000]) + +In [226]: labels = pd.cut(fec_mrbo["contb_receipt_amt"], bins) + +In [227]: labels +Out[227]: +411 (10, 100] +412 (100, 1000] +413 (100, 1000] +414 (10, 100] +415 (10, 100] + ... +701381 (10, 100] +701382 (100, 1000] +701383 (1, 10] +701384 (10, 100] +701385 (100, 1000] +Name: contb_receipt_amt, Length: 694282, dtype: category +Categories (8, interval[int64, right]): [(0, 1] < (1, 10] < (10, 100] < (100, 100 +0] < + (1000, 10000] < (10000, 100000] < (10000 +0, 1000000] < + (1000000, 10000000]] +``` + +然后,我们可以按姓名和 bin 标签对 Obama 和 Romney 的数据进行分组,以获得按捐款大小分组的直方图: + +```py +In [228]: grouped = fec_mrbo.groupby(["cand_nm", labels]) + +In [229]: grouped.size().unstack(level=0) +Out[229]: +cand_nm Obama, Barack Romney, Mitt +contb_receipt_amt +(0, 1] 493 77 +(1, 10] 40070 3681 +(10, 100] 372280 31853 +(100, 1000] 153991 43357 +(1000, 10000] 22284 26186 +(10000, 100000] 2 1 +(100000, 1000000] 3 0 +(1000000, 10000000] 4 0 +``` + +这些数据显示,奥巴马收到的小额捐款数量明显多于罗姆尼。您还可以对捐款金额进行求和,并在桶内进行归一化,以可视化每个候选人每个大小的总捐款的百分比(每个捐款大小收到的候选人总捐款的百分比显示了结果图): + +```py +In [231]: bucket_sums = grouped["contb_receipt_amt"].sum().unstack(level=0) + +In [232]: normed_sums = bucket_sums.div(bucket_sums.sum(axis="columns"), + .....: axis="index") + +In [233]: normed_sums +Out[233]: +cand_nm Obama, Barack Romney, Mitt +contb_receipt_amt +(0, 1] 0.805182 0.194818 +(1, 10] 0.918767 0.081233 +(10, 100] 0.910769 0.089231 +(100, 1000] 0.710176 0.289824 +(1000, 10000] 0.447326 0.552674 +(10000, 100000] 0.823120 0.176880 +(100000, 1000000] 1.000000 0.000000 +(1000000, 10000000] 1.000000 0.000000 + +In [234]: normed_sums[:-2].plot(kind="barh") +``` + +![](img/e84a484574c968c98c4258588ab07435.png) + +图 13.13:每个捐款大小收到的候选人总捐款的百分比 + +我排除了两个最大的桶,因为这些不是个人捐款。 + +这种分析可以以许多方式进行细化和改进。例如,您可以按捐赠人姓名和邮政编码对捐款进行汇总,以调整给出许多小额捐款与一笔或多笔大额捐款的捐赠者。我鼓励您自己探索数据集。 + +### 按州的捐款统计 + +我们可以通过候选人和州对数据进行汇总: + +```py +In [235]: grouped = fec_mrbo.groupby(["cand_nm", "contbr_st"]) + +In [236]: totals = grouped["contb_receipt_amt"].sum().unstack(level=0).fillna(0) + +In [237]: totals = totals[totals.sum(axis="columns") > 100000] + +In [238]: totals.head(10) +Out[238]: +cand_nm Obama, Barack Romney, Mitt +contbr_st +AK 281840.15 86204.24 +AL 543123.48 527303.51 +AR 359247.28 105556.00 +AZ 1506476.98 1888436.23 +CA 23824984.24 11237636.60 +CO 2132429.49 1506714.12 +CT 2068291.26 3499475.45 +DC 4373538.80 1025137.50 +DE 336669.14 82712.00 +FL 7318178.58 8338458.81 +``` + +如果您将每一行都除以总捐款金额,您将得到每位候选人每个州的总捐款相对百分比: + +* * * + +## 13.6 结论 + +在这本书第一版出版以来的 10 年里,Python 已经成为数据分析中流行和广泛使用的语言。您在这里所学习的编程技能将在未来很长一段时间内保持相关性。希望我们探讨过的编程工具和库能够为您提供帮助。 + +我们已经到达了这本书的结尾。我在附录中包含了一些您可能会发现有用的额外内容。 + +1. 这做出了一个简化的假设,即 Gary Johnson 是共和党人,尽管后来成为了自由党候选人。 diff --git a/translations/cn/pyda3e_17.md b/translations/cn/pyda3e_17.md new file mode 100644 index 000000000..83a54825a --- /dev/null +++ b/translations/cn/pyda3e_17.md @@ -0,0 +1 @@ +# 附录 diff --git a/translations/cn/pyda3e_18.md b/translations/cn/pyda3e_18.md new file mode 100644 index 000000000..c959f5658 --- /dev/null +++ b/translations/cn/pyda3e_18.md @@ -0,0 +1,1526 @@ +# 附录 A:高级 NumPy + +> 原文:[`wesmckinney.com/book/advanced-numpy`](https://wesmckinney.com/book/advanced-numpy) +> +> 译者:[飞龙](https://github.com/wizardforcel) +> +> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) + +> 此开放访问网络版本的《Python 数据分析第三版》现已作为[印刷版和数字版](https://amzn.to/3DyLaJc)的伴侣提供。如果您发现任何勘误,请[在此处报告](https://oreilly.com/catalog/0636920519829/errata)。请注意,由 Quarto 生成的本站点的某些方面与 O'Reilly 的印刷版和电子书版本的格式不同。 +> +> 如果您发现本书的在线版本有用,请考虑[订购纸质版](https://amzn.to/3DyLaJc)或[无 DRM 的电子书](https://www.ebooks.com/en-us/book/210644288/python-for-data-analysis/wes-mckinney/?affId=WES398681F)以支持作者。本网站的内容不得复制或再生产。代码示例采用 MIT 许可,可在 GitHub 或 Gitee 上找到。 +在这个附录中,我将深入探讨 NumPy 库的数组计算。这将包括有关 ndarray 类型的更多内部细节以及更高级的数组操作和算法。 + +这个附录包含各种主题,不一定需要按顺序阅读。在各章节中,我将为许多示例生成随机数据,这些示例将使用`numpy.random`模块中的默认随机数生成器: + +```py +In [11]: rng = np.random.default_rng(seed=12345) +``` + +## A.1 ndarray 对象内部 + +NumPy ndarray 提供了一种将块状同类型数据(连续或分步)解释为多维数组对象的方法。数据类型,或*dtype*,决定了数据被解释为浮点数、整数、布尔值或我们一直在查看的其他类型之一。 + +ndarray 灵活的部分之一是每个数组对象都是对数据块的*步进*视图。例如,您可能想知道,例如,数组视图`arr[::2, ::-1]`如何不复制任何数据。原因是 ndarray 不仅仅是一块内存和一个数据类型;它还具有*步进*信息,使数组能够以不同的步长在内存中移动。更准确地说,ndarray 内部包含以下内容: + ++ 一个*数据指针*—即 RAM 中的数据块或内存映射文件 + ++ 描述数组中固定大小值单元的*数据类型*或 dtype + ++ 一个指示数组*形状*的元组 + ++ 一个*步长*元组—表示在一个维度上前进一个元素所需的字节数 + +请参见图 A.1 以查看 ndarray 内部的简单模拟。 + +![](img/e27af70a8051b0749f47f917cb9bc365.png) + +图 A.1:NumPy ndarray 对象 + +例如,一个 10×5 的数组将具有形状`(10, 5)`: + +```py +In [12]: np.ones((10, 5)).shape +Out[12]: (10, 5) +``` + +一个典型的(C 顺序)3×4×5 的`float64`(8 字节)值数组具有步长`(160, 40, 8)`(了解步长可以是有用的,因为一般来说,特定轴上的步长越大,沿着该轴执行计算的成本就越高): + +```py +In [13]: np.ones((3, 4, 5), dtype=np.float64).strides +Out[13]: (160, 40, 8) +``` + +虽然典型的 NumPy 用户很少会对数组的步长感兴趣,但它们需要用来构建“零拷贝”数组视图。步长甚至可以是负数,这使得数组可以在内存中“向后”移动(例如,在像`obj[::-1]`或`obj[:, ::-1]`这样的切片中)。 + +### NumPy 数据类型层次结构 + +您可能偶尔需要检查代码是否包含整数、浮点数、字符串或 Python 对象的数组。由于有多种浮点数类型(`float16`到`float128`),检查数据类型是否在类型列表中会非常冗长。幸运的是,数据类型有超类,如`np.integer`和`np.floating`,可以与`np.issubdtype`函数一起使用: + +```py +In [14]: ints = np.ones(10, dtype=np.uint16) + +In [15]: floats = np.ones(10, dtype=np.float32) + +In [16]: np.issubdtype(ints.dtype, np.integer) +Out[16]: True + +In [17]: np.issubdtype(floats.dtype, np.floating) +Out[17]: True +``` + +您可以通过调用类型的`mro`方法查看特定数据类型的所有父类: + +```py +In [18]: np.float64.mro() +Out[18]: +[numpy.float64, + numpy.floating, + numpy.inexact, + numpy.number, + numpy.generic, + float, + object] +``` + +因此,我们还有: + +```py +In [19]: np.issubdtype(ints.dtype, np.number) +Out[19]: True +``` + +大多数 NumPy 用户永远不需要了解这一点,但有时会有用。请参见图 A.2 以查看数据类型层次结构和父-子类关系的图表。¹ + +![](img/d60ef95fddd13030d45f90939bef5ca1.png) + +图 A.2:NumPy 数据类型类层次结构 + +## A.2 高级数组操作 + +除了花式索引、切片和布尔子集之外,还有许多处理数组的方法。虽然大部分数据分析应用程序的繁重工作由 pandas 中的高级函数处理,但您可能在某个时候需要编写一个在现有库中找不到的数据算法。 + +### 重新塑形数组 + +在许多情况下,您可以将一个数组从一种形状转换为另一种形状而不复制任何数据。为此,将表示新形状的元组传递给 `reshape` 数组实例方法。例如,假设我们有一个希望重新排列成矩阵的值的一维数组(这在图 A.3 中有说明): + +```py +In [20]: arr = np.arange(8) + +In [21]: arr +Out[21]: array([0, 1, 2, 3, 4, 5, 6, 7]) + +In [22]: arr.reshape((4, 2)) +Out[22]: +array([[0, 1], + [2, 3], + [4, 5], + [6, 7]]) +``` + +![](img/cf72351c291cc639dc236d8072622fe1.png) + +图 A.3:按 C(行主要)或 FORTRAN(列主要)顺序重新塑形 + +多维数组也可以被重新塑形: + +```py +In [23]: arr.reshape((4, 2)).reshape((2, 4)) +Out[23]: +array([[0, 1, 2, 3], + [4, 5, 6, 7]]) +``` + +传递的形状维度中可以有一个为 -1,在这种情况下,该维度的值将从数据中推断出来: + +```py +In [24]: arr = np.arange(15) + +In [25]: arr.reshape((5, -1)) +Out[25]: +array([[ 0, 1, 2], + [ 3, 4, 5], + [ 6, 7, 8], + [ 9, 10, 11], + [12, 13, 14]]) +``` + +由于数组的 `shape` 属性是一个元组,它也可以传递给 `reshape`: + +```py +In [26]: other_arr = np.ones((3, 5)) + +In [27]: other_arr.shape +Out[27]: (3, 5) + +In [28]: arr.reshape(other_arr.shape) +Out[28]: +array([[ 0, 1, 2, 3, 4], + [ 5, 6, 7, 8, 9], + [10, 11, 12, 13, 14]]) +``` + +从一维到更高维的 `reshape` 的相反操作通常称为 *展平* 或 *raveling*: + +```py +In [29]: arr = np.arange(15).reshape((5, 3)) + +In [30]: arr +Out[30]: +array([[ 0, 1, 2], + [ 3, 4, 5], + [ 6, 7, 8], + [ 9, 10, 11], + [12, 13, 14]]) + +In [31]: arr.ravel() +Out[31]: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]) +``` + +如果结果中的值在原始数组中是连续的,`ravel` 不会生成基础值的副本。 + +`flatten` 方法的行为类似于 `ravel`,只是它总是返回数据的副本: + +```py +In [32]: arr.flatten() +Out[32]: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]) +``` + +数据可以以不同的顺序被重新塑形或展开。这对于新的 NumPy 用户来说是一个略微微妙的主题,因此是下一个子主题。 + +### C 与 FORTRAN 顺序 + +NumPy 能够适应内存中数据的许多不同布局。默认情况下,NumPy 数组是按 *行主要* 顺序创建的。从空间上讲,这意味着如果您有一个二维数据数组,数组中每行的项都存储在相邻的内存位置上。与行主要顺序相反的是 *列主要* 顺序,这意味着数据中每列的值都存储在相邻的内存位置上。 + +出于历史原因,行和列主要顺序也被称为 C 和 FORTRAN 顺序。在 FORTRAN 77 语言中,矩阵都是列主要的。 + +像 `reshape` 和 `ravel` 这样的函数接受一个 `order` 参数,指示数组中使用数据的顺序。在大多数情况下,这通常设置为 `'C'` 或 `'F'`(还有一些不常用的选项 `'A'` 和 `'K'`;请参阅 NumPy 文档,并参考图 A.3 以了解这些选项的说明): + +```py +In [33]: arr = np.arange(12).reshape((3, 4)) + +In [34]: arr +Out[34]: +array([[ 0, 1, 2, 3], + [ 4, 5, 6, 7], + [ 8, 9, 10, 11]]) + +In [35]: arr.ravel() +Out[35]: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) + +In [36]: arr.ravel('F') +Out[36]: array([ 0, 4, 8, 1, 5, 9, 2, 6, 10, 3, 7, 11]) +``` + +使用超过两个维度的数组进行重新塑形可能有点令人费解(参见图 A.3)。C 和 FORTRAN 顺序之间的关键区别在于维度的遍历方式: + +C/行主要顺序 + +在遍历更高维度时,*首先* 遍历(例如,先在轴 1 上再在轴 0 上前进)。 + +FORTRAN/列主要顺序 + +在遍历更高维度时,*最后* 遍历(例如,先在轴 0 上再在轴 1 上前进)。 + +### 连接和分割数组 + +`numpy.concatenate` 接受一个数组序列(元组,列表等),并按顺序沿着输入轴连接它们: + +```py +In [37]: arr1 = np.array([[1, 2, 3], [4, 5, 6]]) + +In [38]: arr2 = np.array([[7, 8, 9], [10, 11, 12]]) + +In [39]: np.concatenate([arr1, arr2], axis=0) +Out[39]: +array([[ 1, 2, 3], + [ 4, 5, 6], + [ 7, 8, 9], + [10, 11, 12]]) + +In [40]: np.concatenate([arr1, arr2], axis=1) +Out[40]: +array([[ 1, 2, 3, 7, 8, 9], + [ 4, 5, 6, 10, 11, 12]]) +``` + +有一些便利函数,如 `vstack` 和 `hstack`,用于常见类型的连接。前面的操作可以表示为: + +```py +In [41]: np.vstack((arr1, arr2)) +Out[41]: +array([[ 1, 2, 3], + [ 4, 5, 6], + [ 7, 8, 9], + [10, 11, 12]]) + +In [42]: np.hstack((arr1, arr2)) +Out[42]: +array([[ 1, 2, 3, 7, 8, 9], + [ 4, 5, 6, 10, 11, 12]]) +``` + +另一方面,`split` 将数组沿着一个轴分割成多个数组: + +```py +In [43]: arr = rng.standard_normal((5, 2)) + +In [44]: arr +Out[44]: +array([[-1.4238, 1.2637], + [-0.8707, -0.2592], + [-0.0753, -0.7409], + [-1.3678, 0.6489], + [ 0.3611, -1.9529]]) + +In [45]: first, second, third = np.split(arr, [1, 3]) + +In [46]: first +Out[46]: array([[-1.4238, 1.2637]]) + +In [47]: second +Out[47]: +array([[-0.8707, -0.2592], + [-0.0753, -0.7409]]) + +In [48]: third +Out[48]: +array([[-1.3678, 0.6489], + [ 0.3611, -1.9529]]) +``` + +传递给 `np.split` 的值 `[1, 3]` 指示在哪些索引处将数组分割成片段。 + +请参见表 A.1 以获取所有相关连接和分割函数的列表,其中一些仅作为非常通用的 `concatenate` 的便利。 + +表 A.1:数组连接函数 + +| 函数 | 描述 | +| --- | --- | +| `concatenate` | 最通用的函数,沿一个轴连接数组集合 | +| `vstack, row_stack` | 按行堆叠数组(沿轴 0) | +| `hstack` | 按列堆叠数组(沿轴 1) | +| `column_stack` | 类似于`hstack`,但首先将 1D 数组转换为 2D 列向量 | +| `dstack` | 按“深度”(沿轴 2)堆叠数组 | +| `split` | 沿特定轴在传递位置分割数组 | +| `hsplit`/`vsplit` | 在轴 0 和 1 上分割的便利函数 | + +#### 堆叠助手:r_ 和 c_ + +NumPy 命名空间中有两个特殊对象,`r_`和`c_`,使堆叠数组更简洁: + +```py +In [49]: arr = np.arange(6) + +In [50]: arr1 = arr.reshape((3, 2)) + +In [51]: arr2 = rng.standard_normal((3, 2)) + +In [52]: np.r_[arr1, arr2] +Out[52]: +array([[ 0. , 1. ], + [ 2. , 3. ], + [ 4. , 5. ], + [ 2.3474, 0.9685], + [-0.7594, 0.9022], + [-0.467 , -0.0607]]) + +In [53]: np.c_[np.r_[arr1, arr2], arr] +Out[53]: +array([[ 0. , 1. , 0. ], + [ 2. , 3. , 1. ], + [ 4. , 5. , 2. ], + [ 2.3474, 0.9685, 3. ], + [-0.7594, 0.9022, 4. ], + [-0.467 , -0.0607, 5. ]]) +``` + +这些还可以将切片转换为数组: + +```py +In [54]: np.c_[1:6, -10:-5] +Out[54]: +array([[ 1, -10], + [ 2, -9], + [ 3, -8], + [ 4, -7], + [ 5, -6]]) +``` + +查看文档字符串以了解您可以使用`c_`和`r_`做什么。 + +### 重复元素:tile 和 repeat + +用于重复或复制数组以生成更大数组的两个有用工具是`repeat`和`tile`函数。`repeat`将数组中的每个元素重复若干次,生成一个更大的数组: + +```py +In [55]: arr = np.arange(3) + +In [56]: arr +Out[56]: array([0, 1, 2]) + +In [57]: arr.repeat(3) +Out[57]: array([0, 0, 0, 1, 1, 1, 2, 2, 2]) +``` + +注意 + +需要复制或重复数组的情况在 NumPy 中可能不像其他数组编程框架(如 MATLAB)中那样常见。其中一个原因是*广播*通常更好地满足这种需求,这是下一节的主题。 + +默认情况下,如果传递一个整数,每个元素将重复该次数。如果传递一个整数数组,每个元素可以重复不同次数: + +```py +In [58]: arr.repeat([2, 3, 4]) +Out[58]: array([0, 0, 1, 1, 1, 2, 2, 2, 2]) +``` + +多维数组可以沿特定轴重复其元素: + +```py +In [59]: arr = rng.standard_normal((2, 2)) + +In [60]: arr +Out[60]: +array([[ 0.7888, -1.2567], + [ 0.5759, 1.399 ]]) + +In [61]: arr.repeat(2, axis=0) +Out[61]: +array([[ 0.7888, -1.2567], + [ 0.7888, -1.2567], + [ 0.5759, 1.399 ], + [ 0.5759, 1.399 ]]) +``` + +请注意,如果没有传递轴,数组将首先被展平,这可能不是您想要的。同样,当重复多维数组以不同次数重复给定切片时,可以传递整数数组: + +```py +In [62]: arr.repeat([2, 3], axis=0) +Out[62]: +array([[ 0.7888, -1.2567], + [ 0.7888, -1.2567], + [ 0.5759, 1.399 ], + [ 0.5759, 1.399 ], + [ 0.5759, 1.399 ]]) + +In [63]: arr.repeat([2, 3], axis=1) +Out[63]: +array([[ 0.7888, 0.7888, -1.2567, -1.2567, -1.2567], + [ 0.5759, 0.5759, 1.399 , 1.399 , 1.399 ]]) +``` + +另一方面,`tile`是一个沿轴堆叠数组副本的快捷方式。在视觉上,您可以将其视为类似于“铺设瓷砖”: + +```py +In [64]: arr +Out[64]: +array([[ 0.7888, -1.2567], + [ 0.5759, 1.399 ]]) + +In [65]: np.tile(arr, 2) +Out[65]: +array([[ 0.7888, -1.2567, 0.7888, -1.2567], + [ 0.5759, 1.399 , 0.5759, 1.399 ]]) +``` + +第二个参数是瓷砖的数量;对于标量,瓦片是按行而不是按列进行的。`tile`的第二个参数可以是一个元组,指示“瓦片”的布局: + +```py +In [66]: arr +Out[66]: +array([[ 0.7888, -1.2567], + [ 0.5759, 1.399 ]]) + +In [67]: np.tile(arr, (2, 1)) +Out[67]: +array([[ 0.7888, -1.2567], + [ 0.5759, 1.399 ], + [ 0.7888, -1.2567], + [ 0.5759, 1.399 ]]) + +In [68]: np.tile(arr, (3, 2)) +Out[68]: +array([[ 0.7888, -1.2567, 0.7888, -1.2567], + [ 0.5759, 1.399 , 0.5759, 1.399 ], + [ 0.7888, -1.2567, 0.7888, -1.2567], + [ 0.5759, 1.399 , 0.5759, 1.399 ], + [ 0.7888, -1.2567, 0.7888, -1.2567], + [ 0.5759, 1.399 , 0.5759, 1.399 ]]) +``` + +### 花式索引等效:take 和 put + +正如您可能从 Ch 4:NumPy 基础:数组和矢量化计算中记得的那样,通过使用整数数组进行*花式*索引来获取和设置数组的子集是一种方法: + +```py +In [69]: arr = np.arange(10) * 100 + +In [70]: inds = [7, 1, 2, 6] + +In [71]: arr[inds] +Out[71]: array([700, 100, 200, 600]) +``` + +在仅在单个轴上进行选择的特殊情况下,有一些替代的 ndarray 方法是有用的: + +```py +In [72]: arr.take(inds) +Out[72]: array([700, 100, 200, 600]) + +In [73]: arr.put(inds, 42) + +In [74]: arr +Out[74]: array([ 0, 42, 42, 300, 400, 500, 42, 42, 800, 900]) + +In [75]: arr.put(inds, [40, 41, 42, 43]) + +In [76]: arr +Out[76]: array([ 0, 41, 42, 300, 400, 500, 43, 40, 800, 900]) +``` + +要在其他轴上使用`take`,可以传递`axis`关键字: + +```py +In [77]: inds = [2, 0, 2, 1] + +In [78]: arr = rng.standard_normal((2, 4)) + +In [79]: arr +Out[79]: +array([[ 1.3223, -0.2997, 0.9029, -1.6216], + [-0.1582, 0.4495, -1.3436, -0.0817]]) + +In [80]: arr.take(inds, axis=1) +Out[80]: +array([[ 0.9029, 1.3223, 0.9029, -0.2997], + [-1.3436, -0.1582, -1.3436, 0.4495]]) +``` + +`put`不接受`axis`参数,而是索引到数组的展平(一维,C 顺序)版本。因此,当您需要使用索引数组在其他轴上设置元素时,最好使用基于`[]`的索引。 + +## A.3 广播 + +*广播*规定了不同形状数组之间的操作方式。它可以是一个强大的功能,但即使对于有经验的用户也可能会引起混淆。广播的最简单示例是将标量值与数组组合时发生: + +```py +In [81]: arr = np.arange(5) + +In [82]: arr +Out[82]: array([0, 1, 2, 3, 4]) + +In [83]: arr * 4 +Out[83]: array([ 0, 4, 8, 12, 16]) +``` + +在这里,我们说标量值 4 已经*广播*到乘法操作中的所有其他元素。 + +例如,我们可以通过减去列均值来对数组的每一列进行去均值处理。在这种情况下,只需要减去包含每列均值的数组即可: + +```py +In [84]: arr = rng.standard_normal((4, 3)) + +In [85]: arr.mean(0) +Out[85]: array([0.1206, 0.243 , 0.1444]) + +In [86]: demeaned = arr - arr.mean(0) + +In [87]: demeaned +Out[87]: +array([[ 1.6042, 2.3751, 0.633 ], + [ 0.7081, -1.202 , -1.3538], + [-1.5329, 0.2985, 0.6076], + [-0.7793, -1.4717, 0.1132]]) + +In [88]: demeaned.mean(0) +Out[88]: array([ 0., -0., 0.]) +``` + +请参见图 A.4 以了解此操作的示例。将行作为广播操作去均值需要更多的注意。幸运的是,跨任何数组维度广播潜在较低维值(例如从二维数组的每列中减去行均值)是可能的,只要遵循规则。 + +这将我们带到了广播规则。 + +两个数组在广播时兼容,如果对于每个*尾部维度*(即,从末尾开始),轴的长度匹配,或者长度中的任何一个为 1。然后在缺失或长度为 1 的维度上执行广播。 + +![](img/77180177aee670ca8681b6eabd6ebce8.png) + +图 A.4:在 1D 数组的轴 0 上进行广播 + +即使作为一个经验丰富的 NumPy 用户,我经常发现自己在思考广播规则时不得不停下来画图。考虑最后一个示例,假设我们希望减去每行的平均值。由于`arr.mean(0)`的长度为 3,它在轴 0 上是兼容的进行广播,因为`arr`中的尾部维度为 3,因此匹配。根据规则,要在轴 1 上进行减法(即,从每行减去行均值),较小的数组必须具有形状`(4, 1)`: + +```py +In [89]: arr +Out[89]: +array([[ 1.7247, 2.6182, 0.7774], + [ 0.8286, -0.959 , -1.2094], + [-1.4123, 0.5415, 0.7519], + [-0.6588, -1.2287, 0.2576]]) + +In [90]: row_means = arr.mean(1) + +In [91]: row_means.shape +Out[91]: (4,) + +In [92]: row_means.reshape((4, 1)) +Out[92]: +array([[ 1.7068], + [-0.4466], + [-0.0396], + [-0.5433]]) + +In [93]: demeaned = arr - row_means.reshape((4, 1)) + +In [94]: demeaned.mean(1) +Out[94]: array([-0., 0., 0., 0.]) +``` + +查看图 A.5 以了解此操作的示例。 + +![](img/95638725670f843291db058cdae27a0b.png) + +图 A.5:在 2D 数组的轴 1 上进行广播 + +查看图 A.6 以获得另一个示例,这次是在轴 0 上将二维数组添加到三维数组中。 + +![](img/fb752abddb93c77ae991903b8cf1f98f.png) + +图 A.6:在 3D 数组的轴 0 上进行广播 + +### 在其他轴上进行广播 + +使用更高维度数组进行广播可能看起来更加令人费解,但实际上只是遵循规则的问题。如果不遵循规则,就会出现如下错误: + +```py +In [95]: arr - arr.mean(1) +--------------------------------------------------------------------------- +ValueError Traceback (most recent call last) + in +----> 1 arr - arr.mean(1) +ValueError: operands could not be broadcast together with shapes (4,3) (4,) +``` + +通常希望使用低维数组在轴 0 以外的轴上执行算术运算是很常见的。根据广播规则,“广播维度”在较小数组中必须为 1。在这里显示的行减均值示例中,这意味着将行重塑为形状`(4, 1)`而不是`(4,)`: + +```py +In [96]: arr - arr.mean(1).reshape((4, 1)) +Out[96]: +array([[ 0.018 , 0.9114, -0.9294], + [ 1.2752, -0.5124, -0.7628], + [-1.3727, 0.5811, 0.7915], + [-0.1155, -0.6854, 0.8009]]) +``` + +在三维情况下,沿着任何三个维度进行广播只是将数据重塑为兼容形状的问题。图 A.7 很好地可视化了广播到三维数组的每个轴所需的形状。 + +![](img/2cdd2c64c3d5cf9a9a9300814e38d103.png) + +图 A.7:广播到 3D 数组上的兼容 2D 数组形状 + +因此,一个常见的问题是需要添加一个新的长度为 1 的新轴,专门用于广播目的。使用`reshape`是一种选择,但插入轴需要构造一个指示新形状的元组。这通常是一项繁琐的工作。因此,NumPy 数组提供了一种特殊的语法,通过索引插入新轴。我们使用特殊的`np.newaxis`属性以及“full”切片来插入新轴: + +```py +In [97]: arr = np.zeros((4, 4)) + +In [98]: arr_3d = arr[:, np.newaxis, :] + +In [99]: arr_3d.shape +Out[99]: (4, 1, 4) + +In [100]: arr_1d = rng.standard_normal(3) + +In [101]: arr_1d[:, np.newaxis] +Out[101]: +array([[ 0.3129], + [-0.1308], + [ 1.27 ]]) + +In [102]: arr_1d[np.newaxis, :] +Out[102]: array([[ 0.3129, -0.1308, 1.27 ]]) +``` + +因此,如果我们有一个三维数组并且希望减去轴 2 的均值,我们需要编写: + +```py +In [103]: arr = rng.standard_normal((3, 4, 5)) + +In [104]: depth_means = arr.mean(2) + +In [105]: depth_means +Out[105]: +array([[ 0.0431, 0.2747, -0.1885, -0.2014], + [-0.5732, -0.5467, 0.1183, -0.6301], + [ 0.0972, 0.5954, 0.0331, -0.6002]]) + +In [106]: depth_means.shape +Out[106]: (3, 4) + +In [107]: demeaned = arr - depth_means[:, :, np.newaxis] + +In [108]: demeaned.mean(2) +Out[108]: +array([[ 0., -0., 0., -0.], + [ 0., -0., -0., -0.], + [ 0., 0., 0., 0.]]) +``` + +您可能想知道是否有一种方法可以在不牺牲性能的情况下推广沿轴的减均值操作。有,但需要一些索引技巧: + +```py +def demean_axis(arr, axis=0): + means = arr.mean(axis) + + # This generalizes things like [:, :, np.newaxis] to N dimensions + indexer = [slice(None)] * arr.ndim + indexer[axis] = np.newaxis + return arr - means[indexer] +``` + +### 通过广播设置数组值 + +控制算术运算的相同广播规则也适用于通过数组索引设置值。在简单情况下,我们可以做如下操作: + +```py +In [109]: arr = np.zeros((4, 3)) + +In [110]: arr[:] = 5 + +In [111]: arr +Out[111]: +array([[5., 5., 5.], + [5., 5., 5.], + [5., 5., 5.], + [5., 5., 5.]]) +``` + +但是,如果我们有一个要设置到数组列中的值的一维数组,只要形状兼容,我们就可以这样做: + +```py +In [112]: col = np.array([1.28, -0.42, 0.44, 1.6]) + +In [113]: arr[:] = col[:, np.newaxis] + +In [114]: arr +Out[114]: +array([[ 1.28, 1.28, 1.28], + [-0.42, -0.42, -0.42], + [ 0.44, 0.44, 0.44], + [ 1.6 , 1.6 , 1.6 ]]) + +In [115]: arr[:2] = [[-1.37], [0.509]] + +In [116]: arr +Out[116]: +array([[-1.37 , -1.37 , -1.37 ], + [ 0.509, 0.509, 0.509], + [ 0.44 , 0.44 , 0.44 ], + [ 1.6 , 1.6 , 1.6 ]]) +``` + +## A.4 高级 ufunc 用法 + +虽然许多 NumPy 用户只会使用通用函数提供的快速逐元素操作,但偶尔一些附加功能可以帮助您编写更简洁的代码,而无需显式循环。 + +### ufunc 实例方法 + +NumPy 的每个二进制 ufunc 都有特殊的方法来执行某些特定类型的特殊向量化操作。这些方法在表 A.2 中进行了总结,但我将给出一些具体示例来说明它们的工作原理。 + +`reduce`接受一个数组并通过执行一系列二进制操作(可选地沿轴)来聚合其值。例如,在数组中求和元素的另一种方法是使用`np.add.reduce`: + +```py +In [117]: arr = np.arange(10) + +In [118]: np.add.reduce(arr) +Out[118]: 45 + +In [119]: arr.sum() +Out[119]: 45 +``` + +起始值(例如,`add`的 0)取决于 ufunc。如果传递了轴,将沿着该轴执行减少。这使您能够以简洁的方式回答某些类型的问题。作为一个不那么平凡的例子,我们可以使用`np.logical_and`来检查数组的每一行中的值是否已排序: + +```py +In [120]: my_rng = np.random.default_rng(12346) # for reproducibility + +In [121]: arr = my_rng.standard_normal((5, 5)) + +In [122]: arr +Out[122]: +array([[-0.9039, 0.1571, 0.8976, -0.7622, -0.1763], + [ 0.053 , -1.6284, -0.1775, 1.9636, 1.7813], + [-0.8797, -1.6985, -1.8189, 0.119 , -0.4441], + [ 0.7691, -0.0343, 0.3925, 0.7589, -0.0705], + [ 1.0498, 1.0297, -0.4201, 0.7863, 0.9612]]) + +In [123]: arr[::2].sort(1) # sort a few rows + +In [124]: arr[:, :-1] < arr[:, 1:] +Out[124]: +array([[ True, True, True, True], + [False, True, True, False], + [ True, True, True, True], + [False, True, True, False], + [ True, True, True, True]]) + +In [125]: np.logical_and.reduce(arr[:, :-1] < arr[:, 1:], axis=1) +Out[125]: array([ True, False, True, False, True]) +``` + +请注意,`logical_and.reduce`等同于`all`方法。 + +`accumulate` ufunc 方法与`reduce`相关,就像`cumsum`与`sum`相关一样。它生成一个与中间“累积”值大小相同的数组: + +```py +In [126]: arr = np.arange(15).reshape((3, 5)) + +In [127]: np.add.accumulate(arr, axis=1) +Out[127]: +array([[ 0, 1, 3, 6, 10], + [ 5, 11, 18, 26, 35], + [10, 21, 33, 46, 60]]) +``` + +`outer`在两个数组之间执行成对的叉积: + +```py +In [128]: arr = np.arange(3).repeat([1, 2, 2]) + +In [129]: arr +Out[129]: array([0, 1, 1, 2, 2]) + +In [130]: np.multiply.outer(arr, np.arange(5)) +Out[130]: +array([[0, 0, 0, 0, 0], + [0, 1, 2, 3, 4], + [0, 1, 2, 3, 4], + [0, 2, 4, 6, 8], + [0, 2, 4, 6, 8]]) +``` + +`outer`的输出将具有输入维度的连接: + +```py +In [131]: x, y = rng.standard_normal((3, 4)), rng.standard_normal(5) + +In [132]: result = np.subtract.outer(x, y) + +In [133]: result.shape +Out[133]: (3, 4, 5) +``` + +最后一个方法,`reduceat`,执行“本地减少”,本质上是一个数组“分组”操作,其中数组的切片被聚合在一起。它接受指示如何拆分和聚合值的“bin 边缘”序列: + +```py +In [134]: arr = np.arange(10) + +In [135]: np.add.reduceat(arr, [0, 5, 8]) +Out[135]: array([10, 18, 17]) +``` + +结果是在`arr[0:5]`,`arr[5:8]`和`arr[8:]`上执行的减少(这里是求和)。与其他方法一样,您可以传递一个`axis`参数: + +```py +In [136]: arr = np.multiply.outer(np.arange(4), np.arange(5)) + +In [137]: arr +Out[137]: +array([[ 0, 0, 0, 0, 0], + [ 0, 1, 2, 3, 4], + [ 0, 2, 4, 6, 8], + [ 0, 3, 6, 9, 12]]) + +In [138]: np.add.reduceat(arr, [0, 2, 4], axis=1) +Out[138]: +array([[ 0, 0, 0], + [ 1, 5, 4], + [ 2, 10, 8], + [ 3, 15, 12]]) +``` + +请参见表 A.2 以获取 ufunc 方法的部分列表。 + +表 A.2:ufunc 方法 + +| 方法 | 描述 | +| --- | --- | +| `accumulate(x)` | 聚合值,保留所有部分聚合。 | +| `at(x, indices, b=None)` | 在指定的索引处对`x`执行操作。参数`b`是需要两个数组输入的 ufunc 的第二个输入。 | +| `reduce(x)` | 通过连续应用操作来聚合值。 | +| `reduceat(x, bins)` | “本地”减少或“分组”;减少数据的连续切片以生成聚合数组。 | +| `outer(x, y)` | 将操作应用于`x`和`y`中所有元素对;生成的数组形状为`x.shape + y.shape`。 | + +### 用 Python 编写新的 ufuncs + +有许多创建自己的 NumPy ufuncs 的方法。最通用的方法是使用 NumPy C API,但这超出了本书的范围。在本节中,我们将看一下纯 Python ufuncs。 + +`numpy.frompyfunc`接受一个 Python 函数以及输入和输出数量的规范。例如,一个简单的逐元素相加的函数将被指定为: + +```py +In [139]: def add_elements(x, y): + .....: return x + y + +In [140]: add_them = np.frompyfunc(add_elements, 2, 1) + +In [141]: add_them(np.arange(8), np.arange(8)) +Out[141]: array([0, 2, 4, 6, 8, 10, 12, 14], dtype=object) +``` + +使用`frompyfunc`创建的函数始终返回 Python 对象的数组,这可能不方便。幸运的是,还有一种替代(但功能稍逊一筹)的函数`numpy.vectorize`,允许您指定输出类型: + +```py +In [142]: add_them = np.vectorize(add_elements, otypes=[np.float64]) + +In [143]: add_them(np.arange(8), np.arange(8)) +Out[143]: array([ 0., 2., 4., 6., 8., 10., 12., 14.]) +``` + +这些函数提供了一种创建类似 ufunc 函数的方法,但它们非常慢,因为它们需要调用 Python 函数来计算每个元素,这比 NumPy 的基于 C 的 ufunc 循环慢得多: + +```py +In [144]: arr = rng.standard_normal(10000) + +In [145]: %timeit add_them(arr, arr) +1.18 ms +- 14.8 us per loop (mean +- std. dev. of 7 runs, 1000 loops each) + +In [146]: %timeit np.add(arr, arr) +2.8 us +- 64.1 ns per loop (mean +- std. dev. of 7 runs, 100000 loops each) +``` + +在本附录的后面,我们将展示如何使用[Numba 库](http://numba.pydata.org)在 Python 中创建快速的 ufuncs。 + +## A.5 结构化和记录数组 + +到目前为止,您可能已经注意到 ndarray 是一个*同质*数据容器;也就是说,它表示一个内存块,其中每个元素占据相同数量的字节,由数据类型确定。表面上,这似乎不允许您表示异构或表格数据。*结构化*数组是一个 ndarray,其中每个元素可以被视为表示 C 中的*struct*(因此称为“结构化”名称)或 SQL 表中具有多个命名字段的行: + +```py +In [147]: dtype = [('x', np.float64), ('y', np.int32)] + +In [148]: sarr = np.array([(1.5, 6), (np.pi, -2)], dtype=dtype) + +In [149]: sarr +Out[149]: array([(1.5 , 6), (3.1416, -2)], dtype=[('x', ' 原文:[`wesmckinney.com/book/ipython`](https://wesmckinney.com/book/ipython) +> +> 译者:[飞龙](https://github.com/wizardforcel) +> +> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) + + +> 此开放访问网络版本的《Python 数据分析第三版》现已作为[印刷版和数字版](https://amzn.to/3DyLaJc)的伴侣提供。如果您发现任何勘误,请[在此处报告](https://oreilly.com/catalog/0636920519829/errata)。请注意,由 Quarto 生成的本站点的某些方面与 O'Reilly 的印刷版和电子书版本的格式不同。 +> +> 如果您发现本书的在线版本有用,请考虑[订购纸质版](https://amzn.to/3DyLaJc)或[无 DRM 的电子书](https://www.ebooks.com/en-us/book/210644288/python-for-data-analysis/wes-mckinney/?affId=WES398681F)以支持作者。本网站的内容不得复制或再生产。代码示例采用 MIT 许可,可在 GitHub 或 Gitee 上找到。 + +在第二章:Python 语言基础,IPython 和 Jupyter 笔记本中,我们讨论了使用 IPython shell 和 Jupyter 笔记本的基础知识。在本附录中,我们探索了 IPython 系统中的一些更深层次功能,可以从控制台或 Jupyter 中使用。 + +## B.1 终端键盘快捷键 + +IPython 有许多用于导航提示的键盘快捷键(这些快捷键对于 Emacs 文本编辑器或 Unix bash shell 的用户来说是熟悉的),并与 shell 的命令历史交互。表 B.1 总结了一些最常用的快捷键。请参阅图 B.1 以查看其中一些示例,如光标移动。 + +表 B.1:标准 IPython 键盘快捷键 + +| 键盘快捷键 | 描述 | +| --- | --- | +| Ctrl-P 或向上箭头 | 在命令历史中向后搜索以当前输入文本开头的命令 | +| Ctrl-N 或向下箭头 | 在命令历史中向前搜索以当前输入文本开头的命令 | +| Ctrl-R | Readline 风格的反向历史搜索(部分匹配) | +| Ctrl-Shift-V | 从剪贴板粘贴文本 | +| Ctrl-C | 中断当前正在执行的代码 | +| Ctrl-A | 将光标移动到行首 | +| Ctrl-E | 将光标移动到行尾 | +| Ctrl-K | 从光标处删除文本直到行尾 | +| Ctrl-U | 放弃当前行上的所有文本 | +| Ctrl-F | 将光标向前移动一个字符 | +| Ctrl-B | 将光标向后移动一个字符 | +| Ctrl-L | 清屏 | + +![](img/195715a4c5d3641a804058ec12414e68.png) + +图 B.1:IPython shell 中一些键盘快捷键的示例 + +请注意,Jupyter 笔记本有一个完全独立的键盘快捷键集用于导航和编辑。由于这些快捷键的发展速度比 IPython 中的快捷键更快,我鼓励您探索 Jupyter 笔记本菜单中的集成帮助系统。 + +## B.2 关于魔术命令 + +IPython 中的特殊命令(这些命令不是 Python 本身的一部分)被称为*魔术*命令。这些命令旨在简化常见任务,并使您能够轻松控制 IPython 系统的行为。魔术命令是以百分号 `%` 为前缀的任何命令。例如,您可以使用 `%timeit` 魔术函数检查任何 Python 语句(如矩阵乘法)的执行时间: + +```py +In [20]: a = np.random.standard_normal((100, 100)) + + In [20]: %timeit np.dot(a, a) + 92.5 µs ± 3.43 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each) +``` + +魔术命令可以被视为在 IPython 系统中运行的命令行程序。其中许多具有额外的“命令行”选项,所有这些选项都可以使用 `?` 查看(正如您可能期望的那样): + +```py +In [21]: %debug? + Docstring: + :: + + %debug [--breakpoint FILE:LINE] [statement [statement ...]] + + Activate the interactive debugger. + + This magic command support two ways of activating debugger. + One is to activate debugger before executing code. This way, you + can set a break point, to step through the code from the point. + You can use this mode by giving statements to execute and optionally + a breakpoint. + + The other one is to activate debugger in post-mortem mode. You can + activate this mode simply running %debug without any argument. + If an exception has just occurred, this lets you inspect its stack + frames interactively. Note that this will always work only on the last + traceback that occurred, so you must call this quickly after an + exception that you wish to inspect has fired, because if another one + occurs, it clobbers the previous one. + + If you want IPython to automatically do this on every exception, see + the %pdb magic for more details. + + .. versionchanged:: 7.3 + When running code, user variables are no longer expanded, + the magic line is always left unmodified. + + positional arguments: + statement Code to run in debugger. You can omit this in cell + magic mode. + + optional arguments: + --breakpoint , -b + Set break point at LINE in FILE. +``` + +魔术函数可以默认使用,无需百分号,只要没有定义与所讨论的魔术函数同名的变量。这个功能称为*自动魔术*,可以通过 `%automagic` 启用或禁用。 + +一些魔术函数的行为类似于 Python 函数,它们的输出可以分配给一个变量: + +```py +In [22]: %pwd + Out[22]: '/home/wesm/code/pydata-book' + + In [23]: foo = %pwd + + In [24]: foo + Out[24]: '/home/wesm/code/pydata-book' +``` + +由于 IPython 的文档可以从系统内部访问,我鼓励您使用`%quickref`或`%magic`来探索所有可用的特殊命令。此信息显示在控制台分页器中,因此您需要按`q`键退出分页器。表 B.2 突出显示了在 IPython 中进行交互式计算和 Python 开发时最关键的一些命令。 + +表 B.2:一些经常使用的 IPython 魔术命令 + +| 命令 | 描述 | +| --- | --- | +| `%quickref` | 显示 IPython 快速参考卡 | +| `%magic` | 显示所有可用魔术命令的详细文档 | +| `%debug` | 进入最后一个异常回溯底部的交互式调试器 | +| `%hist` | 打印命令输入(和可选输出)历史记录 | +| `%pdb` | 在任何异常后自动进入调试器 | +| `%paste` | 从剪贴板执行预格式化的 Python 代码 | +| `%cpaste` | 打开一个特殊提示,用于手动粘贴要执行的 Python 代码 | +| `%reset` | 删除交互式命名空间中定义的所有变量/名称 | +| `%page` | 对对象进行漂亮打印并通过分页器显示 | +| `%run` | 在 IPython 内部运行 Python 脚本 | +| `%prun` | 使用`cProfile`执行并报告分析器输出 | +| `%time` | 报告单个语句的执行时间 | +| `%timeit` | 多次运行语句以计算集合平均执行时间;用于计时执行时间非常短的代码 | +| `%who, %who_ls, %whos` | 显示交互式命名空间中定义的变量,具有不同级别的信息/详细程度 | +| `%xdel` | 删除变量并尝试清除 IPython 内部对该对象的任何引用 | + +### %run 命令 + +您可以使用`%run`命令在 IPython 会话的环境中运行任何文件作为 Python 程序。假设您在*script.py*中存储了以下简单脚本: + +```py +def f(x, y, z): + return (x + y) / z + +a = 5 +b = 6 +c = 7.5 + +result = f(a, b, c) +``` + +您可以通过将文件名传递给`%run`来执行此操作: + +```py +In [14]: %run script.py +``` + +脚本在*空命名空间*中运行(没有导入或其他变量定义),因此行为应与在命令行上使用`python script.py`运行程序相同。然后,文件中定义的所有变量(导入、函数和全局变量)(直到引发异常(如果有))将在 IPython shell 中可访问: + +```py +In [15]: c +Out [15]: 7.5 + +In [16]: result +Out[16]: 1.4666666666666666 +``` + +如果 Python 脚本需要命令行参数(可以在`sys.argv`中找到),这些参数可以在文件路径之后传递,就像在命令行上运行一样。 + +注意 + +如果要让脚本访问已在交互式 IPython 命名空间中定义的变量,请使用`%run -i`而不是普通的`%run`。 + +在 Jupyter 笔记本中,您还可以使用相关的`%load`魔术函数,它将脚本导入到代码单元格中: + +```py +In [16]: %load script.py + + def f(x, y, z): + return (x + y) / z + + a = 5 + b = 6 + c = 7.5 + + result = f(a, b, c) +``` + +#### 中断运行的代码 + +在运行任何代码时按下 Ctrl-C,无论是通过`%run`运行脚本还是长时间运行的命令,都会引发`KeyboardInterrupt`。这将导致几乎所有 Python 程序立即停止,除非在某些不寻常的情况下。 + +警告: + +当一段 Python 代码调用了一些编译的扩展模块时,按下 Ctrl-C 并不总是会立即停止程序执行。在这种情况下,您要么等待控制返回到 Python 解释器,要么在更严重的情况下,在您的操作系统中强制终止 Python 进程(例如在 Windows 上使用任务管理器或在 Linux 上使用`kill`命令)。 + +### 从剪贴板执行代码 + +如果您正在使用 Jupyter 笔记本,您可以将代码复制粘贴到任何代码单元格中并执行。还可以在 IPython shell 中从剪贴板运行代码。假设您在其他应用程序中有以下代码: + +```py +x = 5 +y = 7 +if x > 5: + x += 1 + y = 8 +``` + +最可靠的方法是`%paste`和`%cpaste`魔术函数(请注意,这些在 Jupyter 中不起作用,因为您可以将文本复制并粘贴到 Jupyter 代码单元格中)。`%paste`获取剪贴板中的文本并将其作为单个块在 shell 中执行: + +```py +In [17]: %paste +x = 5 +y = 7 +if x > 5: + x += 1 + + y = 8 +## -- End pasted text -- +``` + +`%cpaste`类似,只是它为您提供了一个特殊的提示符,用于粘贴代码: + +```py +In [18]: %cpaste +Pasting code; enter '--' alone on the line to stop or use Ctrl-D. +:x = 5 +:y = 7 +:if x > 5: +: x += 1 +: +: y = 8 +:-- +``` + +使用`%cpaste`块,您可以在执行代码之前粘贴尽可能多的代码。您可能决定使用`%cpaste`在执行代码之前查看粘贴的代码。如果您意外粘贴了错误的代码,可以通过按 Ctrl-C 键来退出`%cpaste`提示符。 + +## B.3 使用命令历史 + +IPython 维护一个小型的磁盘数据库,其中包含您执行的每个命令的文本。这有各种用途: + ++ 使用最少的键入搜索、完成和执行先前执行的命令 + ++ 在会话之间保留命令历史 + ++ 将输入/输出历史记录记录到文件中 + +这些功能在 shell 中比在笔记本中更有用,因为笔记本通过设计在每个代码单元格中保留输入和输出的日志。 + +### 搜索和重用命令历史记录 + +IPython shell 允许您搜索和执行以前的代码或其他命令。这很有用,因为您经常会发现自己重复执行相同的命令,例如`%run`命令或其他代码片段。假设您已运行: + +```py +In[7]: %run first/second/third/data_script.py +``` + +然后探索脚本的结果(假设它成功运行),只发现您进行了错误的计算。找出问题并修改*data_script.py*后,您可以开始键入`%run`命令的几个字母,然后按下 Ctrl-P 键组合或向上箭头键。这将搜索命令历史记录,找到与您键入的字母匹配的第一个先前命令。多次按下 Ctrl-P 或向上箭头键将继续搜索历史记录。如果您错过了要执行的命令,不要担心。您可以通过按下 Ctrl-N 或向下箭头键来*向前*浏览命令历史记录。几次这样做后,您可能会开始在不经思考的情况下按下这些键! + +使用 Ctrl-R 会为您提供与 Unix 风格 shell 中使用的`readline`相同的部分增量搜索功能,例如 bash shell。在 Windows 上,IPython 通过模拟`readline`功能来实现。要使用此功能,请按 Ctrl-R,然后键入要搜索的输入行中包含的几个字符: + +```py +In [1]: a_command = foo(x, y, z) + +(reverse-i-search)`com': a_command = foo(x, y, z) +``` + +按下 Ctrl-R 将循环遍历每行的历史记录,匹配您键入的字符。 + +### 输入和输出变量 + +忘记将函数调用的结果分配给变量可能非常恼人。IPython 会将*输入命令和输出 Python 对象*的引用存储在特殊变量中。前两个输出分别存储在`_`(一个下划线)和`__`(两个下划线)变量中: + +```py +In [18]: 'input1' +Out[18]: 'input1' + +In [19]: 'input2' +Out[19]: 'input2' + +In [20]: __ +Out[20]: 'input1' + +In [21]: 'input3' +Out[21]: 'input3' + +In [22]: _ +Out[22]: 'input3' +``` + +输入变量存储在名为`_iX`的变量中,其中`X`是输入行号。 + +对于每个输入变量,都有一个相应的输出变量`_X`。因此,在输入行 27 之后,将有两个新变量,`_27`(用于输出)和`_i27`(用于输入): + +```py +In [26]: foo = 'bar' + +In [27]: foo +Out[27]: 'bar' + +In [28]: _i27 +Out[28]: u'foo' + +In [29]: _27 +Out[29]: 'bar' +``` + +由于输入变量是字符串,因此可以使用 Python 的`eval`关键字再次执行它们: + +```py +In [30]: eval(_i27) +Out[30]: 'bar' +``` + +在这里,`_i27`指的是`In [27]`中输入的代码。 + +几个魔术函数允许您使用输入和输出历史记录。`%hist`打印全部或部分输入历史记录,带或不带行号。`%reset`清除交互式命名空间,可选地清除输入和输出缓存。`%xdel`魔术函数从 IPython 机制中删除对*特定*对象的所有引用。有关这些魔术的更多详细信息,请参阅文档。 + +警告: + +在处理非常大的数据集时,请记住 IPython 的输入和输出历史可能导致其中引用的对象不会被垃圾回收(释放内存),即使您使用`del`关键字从交互式命名空间中删除变量。在这种情况下,谨慎使用`%xdel`和`%reset`可以帮助您避免遇到内存问题。 + +## B.4 与操作系统交互 + +IPython 的另一个特性是它允许您访问文件系统和操作系统 shell。这意味着,您可以像在 Windows 或 Unix(Linux,macOS)shell 中一样执行大多数标准命令行操作,而无需退出 IPython。这包括 shell 命令、更改目录以及将命令的结果存储在 Python 对象(列表或字符串)中。还有命令别名和目录标记功能。 + +查看表 B.3 以获取调用 shell 命令的魔术函数和语法摘要。我将在接下来的几节中简要介绍这些功能。 + +表 B.3:IPython 与系统相关的命令 + +| 命令 | 描述 | +| --- | --- | +| `!cmd` | 在系统 shell 中执行`cmd` | +| `output = !cmd args` | 运行`cmd`并将 stdout 存储在`output`中 | +| `%alias alias_name cmd` | 为系统(shell)命令定义别名 | +| `%bookmark` | 使用 IPython 的目录标记系统 | +| `%cd` | 将系统工作目录更改为传递的目录 | +| `%pwd` | 返回当前系统工作目录 | +| `%pushd` | 将当前目录放入堆栈并切换到目标目录 | +| `%popd` | 切换到堆栈顶部弹出的目录 | +| `%dirs` | 返回包含当前目录堆栈的列表 | +| `%dhist` | 打印访问过的目录的历史记录 | +| `%env` | 将系统环境变量作为字典返回 | +| `%matplotlib` | 配置 matplotlib 集成选项 | + +### Shell 命令和别名 + +在 IPython 中以感叹号`!`开头的行告诉 IPython 在感叹号后执行系统 shell 中的所有内容。这意味着您可以删除文件(使用`rm`或`del`,取决于您的操作系统)、更改目录或执行任何其他进程。 + +您可以通过将用`!`转义的表达式分配给变量来存储 shell 命令的控制台输出。例如,在我连接到以太网上网的基于 Linux 的机器上,我可以将我的 IP 地址作为 Python 变量获取: + +```py +In [1]: ip_info = !ifconfig wlan0 | grep "inet " + +In [2]: ip_info[0].strip() +Out[2]: 'inet addr:10.0.0.11 Bcast:10.0.0.255 Mask:255.255.255.0' +``` + +返回的 Python 对象`ip_info`实际上是一个包含各种控制台输出版本的自定义列表类型。 + +在使用`!`时,IPython 还可以在当前环境中定义的 Python 值进行替换。要做到这一点,请在变量名前加上美元符号`$`: + +```py +In [3]: foo = 'test*' + +In [4]: !ls $foo +test4.py test.py test.xml +``` + +`%alias`魔术函数可以为 shell 命令定义自定义快捷方式。例如: + +```py +In [1]: %alias ll ls -l + +In [2]: ll /usr +total 332 +drwxr-xr-x 2 root root 69632 2012-01-29 20:36 bin/ +drwxr-xr-x 2 root root 4096 2010-08-23 12:05 games/ +drwxr-xr-x 123 root root 20480 2011-12-26 18:08 include/ +drwxr-xr-x 265 root root 126976 2012-01-29 20:36 lib/ +drwxr-xr-x 44 root root 69632 2011-12-26 18:08 lib32/ +lrwxrwxrwx 1 root root 3 2010-08-23 16:02 lib64 -> lib/ +drwxr-xr-x 15 root root 4096 2011-10-13 19:03 local/ +drwxr-xr-x 2 root root 12288 2012-01-12 09:32 sbin/ +drwxr-xr-x 387 root root 12288 2011-11-04 22:53 share/ +drwxrwsr-x 24 root src 4096 2011-07-17 18:38 src/ +``` + +您可以通过使用分号将它们分隔来像在命令行上一样执行多个命令: + +```py +In [558]: %alias test_alias (cd examples; ls; cd ..) + +In [559]: test_alias +macrodata.csv spx.csv tips.csv +``` + +您会注意到,IPython 在会话关闭后会“忘记”您交互定义的任何别名。要创建永久别名,您需要使用配置系统。 + +### 目录标记系统 + +IPython 具有目录标记系统,使您可以保存常见目录的别名,以便您可以轻松跳转。例如,假设您想要创建一个指向本书补充材料的书签: + +```py +In [6]: %bookmark py4da /home/wesm/code/pydata-book +``` + +完成此操作后,当您使用`%cd`魔术时,您可以使用您定义的任何书签: + +```py +In [7]: cd py4da +(bookmark:py4da) -> /home/wesm/code/pydata-book +/home/wesm/code/pydata-book +``` + +如果书签名称与当前工作目录中的目录名称冲突,您可以使用`-b`标志来覆盖并使用书签位置。使用`%bookmark`的`-l`选项列出所有书签: + +```py +In [8]: %bookmark -l +Current bookmarks: +py4da -> /home/wesm/code/pydata-book-source +``` + +与别名不同,书签在 IPython 会话之间自动保留。 + +## B.5 软件开发工具 + +除了作为交互式计算和数据探索的舒适环境外,IPython 还可以成为一般 Python 软件开发的有用伴侣。在数据分析应用中,首先重要的是拥有*正确*的代码。幸运的是,IPython 已经紧密集成并增强了内置的 Python `pdb`调试器。其次,您希望您的代码*快速*。为此,IPython 具有方便的集成代码计时和性能分析工具。我将在这里详细介绍这些工具。 + +### 交互式调试器 + +IPython 的调试器通过制表符补全、语法高亮显示和异常跟踪中每行的上下文增强了`pdb`。调试代码的最佳时机之一是在发生错误后立即进行调试。在异常发生后立即输入`%debug`命令会调用“事后”调试器,并将您放入引发异常的堆栈帧中: + +```py +In [2]: run examples/ipython_bug.py +--------------------------------------------------------------------------- +AssertionError Traceback (most recent call last) +/home/wesm/code/pydata-book/examples/ipython_bug.py in () + 13 throws_an_exception() + 14 +---> 15 calling_things() + +/home/wesm/code/pydata-book/examples/ipython_bug.py in calling_things() + 11 def calling_things(): + 12 works_fine() +---> 13 throws_an_exception() + 14 + 15 calling_things() + +/home/wesm/code/pydata-book/examples/ipython_bug.py in throws_an_exception() + 7 a = 5 + 8 b = 6 +----> 9 assert(a + b == 10) + 10 + 11 def calling_things(): + +AssertionError: + +In [3]: %debug +> /home/wesm/code/pydata-book/examples/ipython_bug.py(9)throws_an_exception() + 8 b = 6 +----> 9 assert(a + b == 10) + 10 + +ipdb> +``` + +进入调试器后,您可以执行任意 Python 代码并探索每个堆栈帧中的所有对象和数据(这些对象和数据由解释器“保持活动”)。默认情况下,您从发生错误的最低级别开始。通过输入`u`(向上)和`d`(向下),您可以在堆栈跟踪的级别之间切换: + +```py +ipdb> u +> /home/wesm/code/pydata-book/examples/ipython_bug.py(13)calling_things() + 12 works_fine() +---> 13 throws_an_exception() + 14 +``` + +执行`%pdb`命令会使 IPython 在任何异常发生后自动调用调试器,这是许多用户会发现有用的模式。 + +在开发代码时使用调试器也很有帮助,特别是当您需要设置断点或逐步执行函数或脚本以检查每个步骤的行为时。有几种方法可以实现这一点。第一种方法是使用带有`-d`标志的`%run`,在执行传递的脚本中的任何代码之前调用调试器。您必须立即输入`s`(步进)以进入脚本: + +```py +In [5]: run -d examples/ipython_bug.py +Breakpoint 1 at /home/wesm/code/pydata-book/examples/ipython_bug.py:1 +NOTE: Enter 'c' at the ipdb> prompt to start your script. +> (1)() + +ipdb> s +--Call-- +> /home/wesm/code/pydata-book/examples/ipython_bug.py(1)() +1---> 1 def works_fine(): + 2 a = 5 + 3 b = 6 +``` + +在此之后,您可以自行决定如何处理文件。例如,在前面的异常中,我们可以在调用`works_fine`函数之前设置断点,并通过输入`c`(继续)运行脚本直到达到断点: + +```py +ipdb> b 12 +ipdb> c +> /home/wesm/code/pydata-book/examples/ipython_bug.py(12)calling_things() + 11 def calling_things(): +2--> 12 works_fine() + 13 throws_an_exception() +``` + +此时,您可以通过输入`n`(下一步)进入`works_fine()`或执行`works_fine()`以前进到下一行: + +```py +ipdb> n +> /home/wesm/code/pydata-book/examples/ipython_bug.py(13)calling_things() +2 12 works_fine() +---> 13 throws_an_exception() + 14 +``` + +然后,我们可以步入`throws_an_exception`并前进到发生错误的行,并查看作用域中的变量。请注意,调试器命令优先于变量名称;在这种情况下,使用`!`作为前缀来检查它们的内容: + +```py +ipdb> s +--Call-- +> /home/wesm/code/pydata-book/examples/ipython_bug.py(6)throws_an_exception() + 5 +----> 6 def throws_an_exception(): + 7 a = 5 + +ipdb> n +> /home/wesm/code/pydata-book/examples/ipython_bug.py(7)throws_an_exception() + 6 def throws_an_exception(): +----> 7 a = 5 + 8 b = 6 + +ipdb> n +> /home/wesm/code/pydata-book/examples/ipython_bug.py(8)throws_an_exception() + 7 a = 5 +----> 8 b = 6 + 9 assert(a + b == 10) + +ipdb> n +> /home/wesm/code/pydata-book/examples/ipython_bug.py(9)throws_an_exception() + 8 b = 6 +----> 9 assert(a + b == 10) + 10 + +ipdb> !a +5 +ipdb> !b +6 +``` + +根据我的经验,熟练掌握交互式调试器需要时间和实践。请参阅表 B.4 以获取调试器命令的完整目录。如果您习惯使用 IDE,您可能会发现基于终端的调试器一开始有点严格,但随着时间的推移会有所改善。一些 Python IDE 具有出色的 GUI 调试器,因此大多数用户都可以找到适合自己的工具。 + +表 B.4:Python 调试器命令 + +| 命令 | 动作 | +| --- | --- | +| `h(elp)` | 显示命令列表 | +| `help` | 显示的文档 | +| `c(ontinue)` | 恢复程序执行 | +| `q(uit)` | 在不执行任何其他代码的情况下退出调试器 | +| `b(reak)` | 在当前文件的行设置断点 | +| `b` | 在指定文件中的第行设置断点 | +| `s(tep)` | 步入函数调用 | +| `n(ext)` | 执行当前行并前进到当前级别的下一行 | +| `u(p)`/`d(own)` | 在函数调用堆栈中向上/向下移动 | +| `a(rgs)` | 显示当前函数的参数 | +| `debug` | 在新的(递归)调试器中调用语句 | +| `l(ist)` | 显示当前位置和堆栈当前级别的上下文 | +| `w(here)` | 打印当前位置的完整堆栈跟踪上下文 | + +#### 调试器的其他用法 + +还有几种有用的调用调试器的方法。第一种是使用特殊的`set_trace`函数(以`pdb.set_trace`命名),基本上是一个“穷人的断点”。以下是两个您可能希望将其放在某处以供您一般使用的小技巧(可能将它们添加到您的 IPython 配置文件中,就像我做的那样): + +```py +from IPython.core.debugger import Pdb + +def set_trace(): + Pdb(.set_trace(sys._getframe().f_back) + +def debug(f, *args, **kwargs): + pdb = Pdb() + return pdb.runcall(f, *args, **kwargs) +``` + +第一个函数`set_trace`提供了一个方便的方法,在代码的某个地方设置断点。您可以在代码的任何部分使用`set_trace`,以便在需要临时停止以更仔细地检查它时使用(例如,在异常发生之前): + +```py +In [7]: run examples/ipython_bug.py +> /home/wesm/code/pydata-book/examples/ipython_bug.py(16)calling_things() + 15 set_trace() +---> 16 throws_an_exception() + 17 +``` + +键入`c`(继续)将使代码正常恢复,不会造成任何伤害。 + +我们刚刚看过的`debug`函数使您可以轻松地在任意函数调用上调用交互式调试器。假设我们编写了一个类似以下内容的函数,并且希望逐步执行其逻辑: + +```py +def f(x, y, z=1): + tmp = x + y + return tmp / z +``` + +通常使用`f`看起来像`f(1, 2, z=3)`。要代替进入`f`,请将`f`作为`debug`的第一个参数传递,然后是要传递给`f`的位置参数和关键字参数: + +```py +In [6]: debug(f, 1, 2, z=3) +> (2)f() + 1 def f(x, y, z): +----> 2 tmp = x + y + 3 return tmp / z + +ipdb> +``` + +这两个技巧多年来为我节省了很多时间。 + +最后,调试器可以与`%run`一起使用。通过使用`%run -d`运行脚本,您将直接进入调试器,准备设置任何断点并启动脚本: + +```py +In [1]: %run -d examples/ipython_bug.py +Breakpoint 1 at /home/wesm/code/pydata-book/examples/ipython_bug.py:1 +NOTE: Enter 'c' at the ipdb> prompt to start your script. +> (1)() + +ipdb> +``` + +添加带有行号的`-b`会启动已经设置了断点的调试器: + +```py +In [2]: %run -d -b2 examples/ipython_bug.py +Breakpoint 1 at /home/wesm/code/pydata-book/examples/ipython_bug.py:2 +NOTE: Enter 'c' at the ipdb> prompt to start your script. +> (1)() + +ipdb> c +> /home/wesm/code/pydata-book/examples/ipython_bug.py(2)works_fine() + 1 def works_fine(): +1---> 2 a = 5 + 3 b = 6 + +ipdb> +``` + +### 计时代码:%time 和%timeit + +对于规模较大或运行时间较长的数据分析应用程序,您可能希望测量各个组件或单个语句或函数调用的执行时间。您可能希望获得一个报告,其中列出了在复杂过程中占用最多时间的函数。幸运的是,IPython 使您能够在开发和测试代码时方便地获取这些信息。 + +手动使用内置的`time`模块及其函数`time.clock`和`time.time`来计时代码通常是乏味和重复的,因为您必须编写相同的无聊样板代码: + +```py +import time +start = time.time() +for i in range(iterations): + # some code to run here +elapsed_per = (time.time() - start) / iterations +``` + +由于这是一个常见操作,IPython 有两个魔术函数`%time`和`%timeit`,可以为您自动化这个过程。 + +`%time`运行一次语句,报告总执行时间。假设我们有一个大型字符串列表,并且我们想比较不同方法选择所有以特定前缀开头的字符串。这里是一个包含 600,000 个字符串和两种相同方法的列表,只选择以`'foo'`开头的字符串: + +```py +# a very large list of strings +In [11]: strings = ['foo', 'foobar', 'baz', 'qux', + ....: 'python', 'Guido Van Rossum'] * 100000 + +In [12]: method1 = [x for x in strings if x.startswith('foo')] + +In [13]: method2 = [x for x in strings if x[:3] == 'foo'] +``` + +看起来它们在性能上应该是一样的,对吧?我们可以使用`%time`来确保: + +```py +In [14]: %time method1 = [x for x in strings if x.startswith('foo')] +CPU times: user 49.6 ms, sys: 676 us, total: 50.3 ms +Wall time: 50.1 ms + +In [15]: %time method2 = [x for x in strings if x[:3] == 'foo'] +CPU times: user 40.3 ms, sys: 603 us, total: 40.9 ms +Wall time: 40.6 ms +``` + +`Wall time`(“墙钟时间”的缩写)是主要关注的数字。从这些时间中,我们可以推断出存在一些性能差异,但这不是一个非常精确的测量。如果您尝试自己多次`%time`这些语句,您会发现结果有些变化。要获得更精确的测量结果,请使用`%timeit`魔术函数。给定一个任意语句,它有一个启发式方法多次运行语句以产生更准确的平均运行时间(这些结果在您的系统上可能有所不同): + +```py +In [563]: %timeit [x for x in strings if x.startswith('foo')] +10 loops, best of 3: 159 ms per loop + +In [564]: %timeit [x for x in strings if x[:3] == 'foo'] +10 loops, best of 3: 59.3 ms per loop +``` + +这个看似无害的例子说明了值得了解 Python 标准库、NumPy、pandas 和本书中使用的其他库的性能特征。在规模较大的数据分析应用程序中,这些毫秒将开始累积! + +`%timeit`特别适用于分析具有非常短执行时间的语句和函数,甚至可以到微秒(百万分之一秒)或纳秒(十亿分之一秒)的级别。这些可能看起来是微不足道的时间,但当然,一个耗时 20 微秒的函数被调用 100 万次比一个耗时 5 微秒的函数多花费 15 秒。在前面的例子中,我们可以直接比较这两个字符串操作以了解它们的性能特征: + +```py +In [565]: x = 'foobar' + +In [566]: y = 'foo' + +In [567]: %timeit x.startswith(y) +1000000 loops, best of 3: 267 ns per loop + +In [568]: %timeit x[:3] == y +10000000 loops, best of 3: 147 ns per loop +``` + +### 基本分析:%prun 和%run -p + +代码剖析与计时代码密切相关,只是它关注于确定时间花费在哪里。主要的 Python 剖析工具是`cProfile`模块,它与 IPython 没有特定关联。`cProfile`执行程序或任意代码块,同时跟踪每个函数中花费的时间。 + +在命令行上常用的一种使用`cProfile`的方式是运行整个程序并输出每个函数的聚合时间。假设我们有一个脚本,在循环中执行一些线性代数运算(计算一系列 100×100 矩阵的最大绝对特征值): + +```py +import numpy as np +from numpy.linalg import eigvals + +def run_experiment(niter=100): + K = 100 + results = [] + for _ in range(niter): + mat = np.random.standard_normal((K, K)) + max_eigenvalue = np.abs(eigvals(mat)).max() + results.append(max_eigenvalue) + return results +some_results = run_experiment() +print('Largest one we saw: {0}'.format(np.max(some_results))) +``` + +您可以通过命令行运行以下脚本来使用`cProfile`: + +```py +python -m cProfile cprof_example.py +``` + +如果尝试这样做,您会发现输出按函数名称排序。这使得很难了解大部分时间花费在哪里,因此使用`-s`标志指定*排序顺序*很有用: + +```py +$ python -m cProfile -s cumulative cprof_example.py +Largest one we saw: 11.923204422 + 15116 function calls (14927 primitive calls) in 0.720 seconds + +Ordered by: cumulative time + +ncalls tottime percall cumtime percall filename:lineno(function) + 1 0.001 0.001 0.721 0.721 cprof_example.py:1() + 100 0.003 0.000 0.586 0.006 linalg.py:702(eigvals) + 200 0.572 0.003 0.572 0.003 {numpy.linalg.lapack_lite.dgeev} + 1 0.002 0.002 0.075 0.075 __init__.py:106() + 100 0.059 0.001 0.059 0.001 {method 'randn') + 1 0.000 0.000 0.044 0.044 add_newdocs.py:9() + 2 0.001 0.001 0.037 0.019 __init__.py:1() + 2 0.003 0.002 0.030 0.015 __init__.py:2() + 1 0.000 0.000 0.030 0.030 type_check.py:3() + 1 0.001 0.001 0.021 0.021 __init__.py:15() + 1 0.013 0.013 0.013 0.013 numeric.py:1() + 1 0.000 0.000 0.009 0.009 __init__.py:6() + 1 0.001 0.001 0.008 0.008 __init__.py:45() + 262 0.005 0.000 0.007 0.000 function_base.py:3178(add_newdoc) + 100 0.003 0.000 0.005 0.000 linalg.py:162(_assertFinite) + ... +``` + +仅显示输出的前 15 行。通过扫描`cumtime`列向下阅读,可以最轻松地看出每个函数内部花费了多少总时间。请注意,如果一个函数调用另一个函数,*时钟不会停止*。`cProfile`记录每个函数调用的开始和结束时间,并使用这些时间来生成时间。 + +除了命令行用法外,`cProfile`还可以以编程方式用于剖析任意代码块,而无需运行新进程。IPython 具有方便的接口,可以使用`%prun`命令和`-p`选项来`%run`。`%prun`接受与`cProfile`相同的“命令行选项”,但会剖析一个任意的 Python 语句,而不是整个*.py*文件: + +```py +In [4]: %prun -l 7 -s cumulative run_experiment() + 4203 function calls in 0.643 seconds + +Ordered by: cumulative time +List reduced from 32 to 7 due to restriction <7> + +ncalls tottime percall cumtime percall filename:lineno(function) + 1 0.000 0.000 0.643 0.643 :1() + 1 0.001 0.001 0.643 0.643 cprof_example.py:4(run_experiment) + 100 0.003 0.000 0.583 0.006 linalg.py:702(eigvals) + 200 0.569 0.003 0.569 0.003 {numpy.linalg.lapack_lite.dgeev} + 100 0.058 0.001 0.058 0.001 {method 'randn'} + 100 0.003 0.000 0.005 0.000 linalg.py:162(_assertFinite) + 200 0.002 0.000 0.002 0.000 {method 'all' of 'numpy.ndarray'} +``` + +类似地,调用`%run -p -s cumulative cprof_example.py`具有与命令行方法相同的效果,只是您无需离开 IPython。 + +在 Jupyter 笔记本中,您可以使用`%%prun`魔术(两个`%`符号)来剖析整个代码块。这会弹出一个单独的窗口,显示剖析输出。这在获取可能快速答案的情况下很有用,比如“为什么那个代码块运行时间如此之长?” + +在使用 IPython 或 Jupyter 时,还有其他可用的工具可帮助使剖析更易于理解。其中之一是[SnakeViz](https://github.com/jiffyclub/snakeviz/),它使用 D3.js 生成剖析结果的交互式可视化。 + +### 逐行剖析函数 + +在某些情况下,您从`%prun`(或其他基于`cProfile`的剖析方法)获得的信息可能无法完全说明函数的执行时间,或者可能非常复杂,以至于按函数名称汇总的结果难以解释。对于这种情况,有一个名为`line_profiler`的小型库(可通过 PyPI 或其中一个软件包管理工具获取)。它包含一个 IPython 扩展,可以启用一个新的魔术函数`%lprun`,用于计算一个或多个函数的逐行剖析。您可以通过修改 IPython 配置(请参阅 IPython 文档或附录后面的配置部分)来启用此扩展,包括以下行: + +```py +# A list of dotted module names of IPython extensions to load. +c.InteractiveShellApp.extensions = ['line_profiler'] +``` + +您还可以运行以下命令: + +```py +%load_ext line_profiler +``` + +`line_profiler`可以以编程方式使用(请参阅完整文档),但在 IPython 中交互使用时可能效果最好。假设您有一个名为`prof_mod`的模块,其中包含执行一些 NumPy 数组操作的以下代码(如果要重现此示例,请将此代码放入一个新文件*prof_mod.py*中): + +```py +from numpy.random import randn + +def add_and_sum(x, y): + added = x + y + summed = added.sum(axis=1) + return summed + +def call_function(): + x = randn(1000, 1000) + y = randn(1000, 1000) + return add_and_sum(x, y) +``` + +如果我们想了解`add_and_sum`函数的性能,`%prun`给出以下结果: + +```py +In [569]: %run prof_mod + +In [570]: x = randn(3000, 3000) + +In [571]: y = randn(3000, 3000) + +In [572]: %prun add_and_sum(x, y) + 4 function calls in 0.049 seconds + Ordered by: internal time + ncalls tottime percall cumtime percall filename:lineno(function) + 1 0.036 0.036 0.046 0.046 prof_mod.py:3(add_and_sum) + 1 0.009 0.009 0.009 0.009 {method 'sum' of 'numpy.ndarray'} + 1 0.003 0.003 0.049 0.049 :1() +``` + +这并不特别启发人。启用`line_profiler` IPython 扩展后,将可用一个新命令`%lprun`。使用方式的唯一区别是我们必须指示`%lprun`要剖析哪个函数或函数。一般语法是: + +```py +%lprun -f func1 -f func2 statement_to_profile +``` + +在这种情况下,我们想要剖析`add_and_sum`,所以我们运行: + +```py +In [573]: %lprun -f add_and_sum add_and_sum(x, y) +Timer unit: 1e-06 s +File: prof_mod.py +Function: add_and_sum at line 3 +Total time: 0.045936 s +Line # Hits Time Per Hit % Time Line Contents +============================================================== + 3 def add_and_sum(x, y): + 4 1 36510 36510.0 79.5 added = x + y + 5 1 9425 9425.0 20.5 summed = added.sum(axis=1) + 6 1 1 1.0 0.0 return summed +``` + +这可能更容易解释。在这种情况下,我们对我们在语句中使用的相同函数进行了分析。查看前面的模块代码,我们可以调用`call_function`并对其进行分析,以及`add_and_sum`,从而获得代码性能的完整图片: + +```py +In [574]: %lprun -f add_and_sum -f call_function call_function() +Timer unit: 1e-06 s +File: prof_mod.py +Function: add_and_sum at line 3 +Total time: 0.005526 s +Line # Hits Time Per Hit % Time Line Contents +============================================================== + 3 def add_and_sum(x, y): + 4 1 4375 4375.0 79.2 added = x + y + 5 1 1149 1149.0 20.8 summed = added.sum(axis=1) + 6 1 2 2.0 0.0 return summed +File: prof_mod.py +Function: call_function at line 8 +Total time: 0.121016 s +Line # Hits Time Per Hit % Time Line Contents +============================================================== + 8 def call_function(): + 9 1 57169 57169.0 47.2 x = randn(1000, 1000) + 10 1 58304 58304.0 48.2 y = randn(1000, 1000) + 11 1 5543 5543.0 4.6 return add_and_sum(x, y) +``` + +作为一个一般准则,我倾向于使用`%prun`(`cProfile`)进行“宏”分析,以及`%lprun`(`line_profiler`)进行“微”分析。了解这两个工具是值得的。 + +注意 + +您必须明确指定要使用`%lprun`对其进行分析的函数名称的原因是“跟踪”每行的执行时间的开销很大。跟踪不感兴趣的函数可能会显著改变分析结果。 + +## B.6 使用 IPython 进行高效代码开发的提示 + +以一种方便开发、调试和最终*交互使用*的方式编写代码可能对许多用户来说是一种范式转变。有一些程序性细节,比如代码重新加载可能需要一些调整,以及编码风格方面的考虑。 + +因此,实现本节中描述的大多数策略更多地是一种艺术而不是科学,需要您进行一些实验来确定一种对您有效的编写 Python 代码的方式。最终,您希望以一种方便的方式构建代码,以便进行迭代使用,并能够尽可能轻松地探索运行程序或函数的结果。我发现,专为 IPython 设计的软件比仅用作独立命令行应用程序运行的代码更容易使用。当出现问题并且您必须诊断您或其他人可能在几个月或几年前编写的代码中的错误时,这变得尤为重要。 + +### 重新加载模块依赖项 + +在 Python 中,当您键入`import some_lib`时,将执行`some_lib`中的代码,并将在新创建的`some_lib`模块命名空间中存储定义的所有变量、函数和导入。下次使用`import some_lib`时,您将获得对现有模块命名空间的引用。在交互式 IPython 代码开发中可能出现困难的潜在问题是,当您运行依赖于其他模块的脚本时,您可能已经进行了更改。假设我在*test_script.py*中有以下代码: + +```py +import some_lib + +x = 5 +y = [1, 2, 3, 4] +result = some_lib.get_answer(x, y) +``` + +如果您执行`%run test_script.py`然后修改*some_lib.py*,下次执行`%run test_script.py`时,您仍将获得*some_lib.py*的*旧版本*,因为 Python 的“一次加载”模块系统。这种行为与其他一些数据分析环境(如 MATLAB)不同,后者会自动传播代码更改。¹ 为了应对这种情况,您有几种选择。第一种方法是使用标准库中的`importlib`模块中的`reload`函数: + +```py +import some_lib +import importlib + +importlib.reload(some_lib) +``` + +这尝试在每次运行*test_script.py*时为您提供*some_lib.py*的新副本(但也有一些情况下不会)。显然,如果依赖关系更深入,可能会在各个地方插入`reload`的用法有点棘手。对于这个问题,IPython 有一个特殊的`dreload`函数(*不是*一个魔术函数)用于对模块进行“深”(递归)重新加载。如果我运行*some_lib.py*然后使用`dreload(some_lib)`,它将尝试重新加载`some_lib`以及其所有依赖项。不幸的是,这并不适用于所有情况,但当适用时,它比不得不重新启动 IPython 要好。 + +### 代码设计提示 + +这没有简单的配方,但以下是我在自己的工作中发现有效的一些高级原则。 + +#### 保持相关对象和数据活动 + +看到一个为命令行编写的程序的结构有点像下面这样并不罕见: + +```py +from my_functions import g + +def f(x, y): + return g(x + y) + +def main(): + x = 6 + y = 7.5 + result = x + y + +if __name__ == '__main__': + main() +``` + +如果我们在 IPython 中运行这个程序,你能看出可能出现什么问题吗?完成后,`main`函数中定义的结果或对象将无法在 IPython shell 中访问。更好的方法是让`main`中的任何代码直接在模块的全局命名空间中执行(或者在`if __name__ == '__main__':`块中执行,如果你希望该模块也可以被导入)。这样,当你`%run`代码时,你将能够查看`main`中定义的所有变量。这相当于在 Jupyter 笔记本中的单元格中定义顶级变量。 + +#### 扁平比嵌套更好 + +深度嵌套的代码让我想到洋葱的许多层。在测试或调试一个函数时,你必须剥开多少层洋葱才能到达感兴趣的代码?“扁平比嵌套更好”的想法是 Python 之禅的一部分,它也适用于为交互式使用开发代码。尽可能使函数和类解耦和模块化使它们更容易进行测试(如果你正在编写单元测试)、调试和交互使用。 + +#### 克服对更长文件的恐惧 + +如果你来自 Java(或其他类似语言)背景,可能会被告知保持文件短小。在许多语言中,这是一个明智的建议;长长度通常是一个不好的“代码异味”,表明可能需要重构或重新组织。然而,在使用 IPython 开发代码时,处理 10 个小但相互关联的文件(每个文件不超过 100 行)通常会给你带来更多的头痛,而不是 2 或 3 个较长的文件。较少的文件意味着较少的模块需要重新加载,编辑时也减少了文件之间的跳转。我发现维护较大的模块,每个模块具有高度的*内部*内聚性(代码都涉及解决相同类型的问题),更加有用和符合 Python 风格。当朝着一个解决方案迭代时,当然有时将较大的文件重构为较小的文件是有意义的。 + +显然,我不支持将这个论点推向极端,即将所有代码放在一个庞大的文件中。为大型代码库找到一个明智和直观的模块和包结构通常需要一些工作,但在团队中正确地完成这一点尤为重要。每个模块应该在内部具有内聚性,并且应该尽可能明显地找到负责每个功能区域的函数和类。 + +## B.7 高级 IPython 功能 + +充分利用 IPython 系统可能会导致你以稍微不同的方式编写代码,或者深入了解配置。 + +### 配置文件和配置 + +IPython 和 Jupyter 环境的外观(颜色、提示、行之间的间距等)和行为的大部分方面都可以通过一个广泛的配置系统进行配置。以下是一些可以通过配置完成的事项: + ++ 更改颜色方案 + ++ 更改输入和输出提示的外观,或者在`Out`之后和下一个`In`提示之前删除空行 + ++ 执行一系列 Python 语句(例如,你经常使用的导入或任何其他你希望每次启动 IPython 时发生的事情) + ++ 启用始终开启的 IPython 扩展,比如`line_profiler`中的`%lprun`魔术 + ++ 启用 Jupyter 扩展 + ++ 定义你自己的魔术或系统别名 + +IPython shell 的配置在特殊的*ipython_config.py*文件中指定,这些文件通常位于用户主目录中的*.ipython/*目录中。配置是基于特定的*profile*执行的。当您正常启动 IPython 时,默认情况下会加载*default profile*,存储在*profile_default*目录中。因此,在我的 Linux 操作系统上,我的默认 IPython 配置文件的完整路径是: + +```py +/home/wesm/.ipython/profile_default/ipython_config.py +``` + +要在您的系统上初始化此文件,请在终端中运行: + +```py +ipython profile create default +``` + +我将不会详细介绍此文件的内容。幸运的是,它有注释描述每个配置选项的用途,因此我将让读者自行调整和自定义。另一个有用的功能是可以拥有*多个配置文件*。假设您想要为特定应用程序或项目定制一个备用 IPython 配置。创建新配置涉及键入以下内容: + +```py +ipython profile create secret_project +``` + +完成后,编辑新创建的*profile_secret_project*目录中的配置文件,然后启动 IPython,如下所示: + +```py +$ ipython --profile=secret_project +Python 3.8.0 | packaged by conda-forge | (default, Nov 22 2019, 19:11:19) +Type 'copyright', 'credits' or 'license' for more information +IPython 7.22.0 -- An enhanced Interactive Python. Type '?' for help. + +IPython profile: secret_project +``` + +与以往一样,在线 IPython 文档是了解更多有关配置文件和配置的绝佳资源。 + +Jupyter 的配置略有不同,因为您可以将其笔记本与 Python 以外的语言一起使用。要创建类似的 Jupyter 配置文件,请运行: + +```py +jupyter notebook --generate-config +``` + +这将在您的主目录中的*.jupyter/jupyter_notebook_config.py*目录中写入一个默认配置文件。编辑后,您可以将其重命名为不同的文件,例如: + +```py +$ mv ~/.jupyter/jupyter_notebook_config.py ~/.jupyter/my_custom_config.py +``` + +在启动 Jupyter 时,您可以添加`--config`参数: + +```py +jupyter notebook --config=~/.jupyter/my_custom_config.py +``` + +## B.8 结论 + +当您在本书中逐步学习代码示例并提高自己作为 Python 程序员的技能时,我鼓励您继续了解 IPython 和 Jupyter 生态系统。由于这些项目旨在帮助用户提高生产力,您可能会发现一些工具,使您比仅使用 Python 语言及其计算库更轻松地完成工作。 + +您还可以在[nbviewer 网站](https://nbviewer.jupyter.org)上找到大量有趣的 Jupyter 笔记本。 + +* * * + +1. 由于模块或包可能在特定程序的许多不同位置导入,Python 在第一次导入模块时缓存模块的代码,而不是每次执行模块中的代码。否则,模块化和良好的代码组织可能会导致应用程序效率低下。