USF-MSDS501-计算数据科学中文讲义-全-
USF MSDS501 计算数据科学中文讲义(全)
译者:飞龙
1.1 一些动机(音频处理)
原文:A bit of motivation (Audio processing)
译者:飞龙
学习编程涉及学习很多细节。 为了简单起见,教师倾向于从简单的代码示例开始,但这些最终变得非常无趣。 我想用一个有趣的计算应用开始本课程,来激励你学习如何编写代码。 我想表明,即使是一点点代码,回报也可能是巨大的。 我不希望你最初了解所有的细节,只是广泛的笔画。 在第一个讲义/实验中,我们将利用现有的代码库,来了解计算机如何表示音乐和其他音频文件。
随着我们进行下去,您将遇到许多全新的任务,例如从命令行在您的计算机上安装软件。 我们将研究一些实际应用,需要跨主题技能和知识,而不是针对特定主题提供一些讲座。 最好看看所有部分是如何组合在一起的,而不是孤立地看待主题。 随着您获得更多经验,您将回顾这些早期的例子,当你了解了一切,你会觉得“啊哈!” 。
播放声音文件
我们都在电脑上播放音乐文件。 例如,这里有两个有趣的:Kiss by Prince,Kiss.aiff 的初始序列和ahhh sound,ahhh.mp3。 您可以下载这些并使用音乐播放器播放它们。 但是,如果我们正在构建游戏,或进行语音识别,并且我们需要 Python 来加载声音文件并播放它们呢?通过利用类似烹饪书的代码库,我们可以使用几行 Python 代码来播放音频文件。您将有机会在本课程的声音实验中,尝试所有这些 Python 代码,但现在只是尝试获取代码的要点,和数字音频背后的原理。
要在 Python 中播放音频文件,我们首先必须将该音频文件加载到内存中。 我们很快就会看到,音频文件只不过是一系列数字。 这里有一些 Python 示例,加载了 Prince's Kiss 的一首歌:
import soundfile as sf
from IPython.display import Audio
kiss, samplerate = sf.read('sound/Kiss.aiff')
Audio(kiss, rate=samplerate)
代码首先从一些有用的 Python 包中导入一些必要的代码。 sf.read(...)是将文件加载到内存中的关键元素。 在该语句之后,变量kiss持有音频数据。 Audio(kiss,...)在技术上是 Python 代码,但它是特定于 Jupyter 笔记本的东西,让我可以使用浏览器播放声音。 这纯粹是为了演示目的。 在你的实验里,你会做一些像sd.play(kiss, ...)之类的东西。
这是另一个音频文件:
import sounddevice as sd
ahhh, samplerate = sf.read('sound/ahhh.wav')
Audio(ahhh[:,0], rate=samplerate)
要查看 Kiss 音频中的内容,我们可以打印变量kiss中值的一个子集:
import numpy as np
np.set_printoptions(suppress=True) # weird numpy thing to avoid scientific notation
print(f"n = {len(kiss)}, rate ={samplerate}hz")
print(kiss[5000:5020]) # kiss is a numpy ndarray that you will become intimately familiar with
'''
n = 123269, rate =44100hz
[ 0.00003052 0. -0.00009155 0.00018311 -0.00024414 0.00030518
-0.00033569 0.00030518 -0.00027466 0.00027466 -0.00021362 0.00006104
0.00003052 -0.00003052 0.00006104 -0.00003052 -0.00009155 0.00015259
-0.00015259 0.00015259]
'''
我们可以为ahhh做同样的事情。
print(ahhh[3000:3010]) # why is each sample actually 2 numbers?
'''
[[-0.02444458 -0.02212524]
[-0.02230835 -0.01843262]
[-0.01998901 -0.01403809]
[-0.01727295 -0.00921631]
[-0.0140686 -0.00402832]
[-0.01025391 0.00143433]
[-0.00570679 0.00714111]
[-0.00042725 0.01318359]
[ 0.0055542 0.01953125]
[ 0.01208496 0.02587891]]
'''
您可能想知道,采样率是多少以及数字如何表示音频。 它的工作方式与电影非常频繁地抓取快照(图片)的方式相同。 以相同的速度播放它们会产生运动的错觉。 电影拍照的频率称为帧速率,可能是每秒 32 帧。 音频文件也会拍摄快照,但不是图像,而是在特定时刻获取音量(声压)。 音频的一个非常常见的采样率是每秒 44,100 次(44,100 赫兹)。 在音频回放期间,每个值用于使扬声器的隔膜偏离其中间位置。 信不信由你,这会以一种再现原始声音的方式震动房间内的空气分子。 在 Big Bang 理论的一个令人敬畏的场景中,看看这个演讲者的动作:
from IPython.display import YouTubeVideo
YouTubeVideo("2CJJ6FrfuGU")
https://www.youtube.com/embed/2CJJ6FrfuGU
麦克风与扬声器相对,并且具有非常灵敏的振膜,在声波的存在下巧妙地振动。 如果我们以非常快速和规则的速率测量麦克风远离中线的偏离,我们将信号(例如音频信号)数字化。 在图形上,它看起来像这个时间-振幅图(麦克风偏离的幅度)
麦克风以连续的方式摆动,对采样率一无所知。这是一种所谓的模拟信号。要将其放入计算机,我们必须将其转换为数字。 您在上面看到的 Kiss 歌曲的数字是数字化的结果。
现在让我们遵循另一种方式,通过生成和数字化我们自己的简单信号,然后看看它听起来的样子。 接下来的 Python 代码中的关键位是sin(2*numpy.pi*440*t),它创建一个 440 赫兹的正弦波(每秒 440 个完整正弦波,每秒通过 0 到 2pi 440个周期)。 plt.scatter(...)绘制信号与时间(X 轴)。
import numpy
import matplotlib.pyplot as plt
%matplotlib inline
fs = 44100 # sampling frequency
T = 1.5 # seconds
t = numpy.linspace(0, T, int(T*fs), endpoint=False) # time variable
y = numpy.sin(2*numpy.pi*440*t) # pure sine wave at 440 Hz
print(len(y), "samples in", T, "seconds")
plt.figure(figsize=(8, 2.5)) # Prepare a plot 8x2.5 inches
plt.scatter(t[0:1000],y[0:1000],s=1)
plt.show()
# 66150 samples in 1.5 seconds

运动:如果我们通过扬声器运行它,你觉得它是什么?
这是 440Hz 的纯音。 想象一下,一个扬声器移出移入,然后每次重复相同的距离。现在,如果你像扬声器一样上下移动你的手,你会得到一种 Boing Boing Boing 动作。 现在开始走路并以相同的速度上下移动你的手。 对观察者来说,这个动作看起来像一个正弦波! 那么,这就是扬声器正在做的事情。上下持续偏离为人耳提供了纯音。
from IPython.display import Audio
Audio(y, rate=fs)
让我们制作一个更高频率(700 Hz)的另一个信号y2。
练习:您认为与之前的信号相比,它听起来像什么?
y2 = numpy.sin(2*numpy.pi*700*t) # pure sine wave at 440 Hz
plt.figure(figsize=(8, 2.5))
plt.scatter(t[0:1000],y2[0:1000],s=1)
plt.show()

from IPython.display import Audio
y2 = numpy.sin(2*numpy.pi*700*t) # pure sine wave at 700 Hz
Audio(y2, rate=fs)
练习:如果我将这些信号加在一起并播放结果,你觉得它是什么?
plt.figure(figsize=(8, 2.5))
plt.scatter(t[0:1000],y[0:1000]+y2[0:1000],s=1) # zoom in on y+y2 for a plot
plt.show()

是的,我们听到声音合并为一个和弦。在数学上,我们正在做的只是将信号振幅加在一起,我们可以用y + y2来做,其中y和y2是我们的数字列表。向量加法将第 i 个元素添加到一起来获得新信号,我们可以绘制和播放:
Audio(y+y2, rate=fs) # Play both sounds together
如果您想知道为什么这听起来就像手机上的按键音,那是因为手机按键会播放两个纯音,作为声音来识别您按下的按钮。
现在让我们看看两个音频文件的信号图:
plt.figure(figsize=(10, 2.5))
plt.plot(kiss);
plt.show()

plt.figure(figsize=(10, 2.5))
plt.plot(ahhh); # notice this one has two plots because it is a stereo signal

那些复杂的信号都可以被分解成一系列纯音正弦波的加法。 正弦波的频率表示音频信号中存在的声音(音调)的频率。 我认为人类可以听到大约 150Hz 到 17,000Hz 的声音。
一个非常酷的图是所谓的频谱图,它显示了特定时刻存在的频率:
fs = 44100 # sampling frequency
# Plot the spectrogram
plt.figure(figsize=(10, 5))
S, freqs, bins, im = plt.specgram(kiss, NFFT=1024, Fs=fs, noverlap=512)
plt.xlabel('Time')
plt.ylabel('Frequency')
plt.show()

练习
好了,现在我们已经知道了计算机如何表示音乐,让我们做一个实验,让你尝试通过 Python 播放声音。 但首先,我们真的需要做一个简单的“欢迎”程序,来介绍我们将在这个类中使用的 Python 工具。
1.2 Python 工具的初次尝试
原文:A first taste of Python tools
译者:飞龙
加载命令行应用
启动Terminal.app(Mac)或任何bash 终端,shell,它是 UNIX 风格的命令行提示符程序。你应该看到一个闪烁的光标和一个$提示符:
$
上面的$符号只是提示,终端正在等待您输入内容。执行命令后,您将再次看到$提示符。
命令行是一个非常低级的接口,用于与计算机的操作系统进行通信。您可以将终端视为诊断计算机,机械师可以将其插入汽车中,来获取控制权。仪表板类似于我们大多数时候使用的窗口图形界面。成为一名程序员就像成为一名机械师;有时你需要更强大但更复杂的工具来操作机器。
你应该或多或少总是运行一个命令行 shell,以防你需要做一些低级别的事情。机械师在他或她开始工作时所做的第一件事,就是连接诊断计算机。你也应该这样。
命令行实际上是一个完整的编程语言,包含循环和所有内容,但大多数时候我们只是执行命令。命令具有参数,就像编程语言中的函数调用具有参数一样。以下是如何在命令行打招呼:
$ echo "hello"
hello
$
通过按返回键终止命令。
echo命令类似于 Python 代码中的print命令。
执行该命令后,提示符将返回,指示您可以键入另一个命令。
我们可以做很多事情; 这是另一个:
$ date
Wed Jul 12 14:18:51 PDT 2017
我们可以将一个参数传递给命令(类似 Python 中的函数调用):
$ date "+%Y-%m-%d"
2018-07-08
这是一个很酷的例子,来自命令行的循环检查所有.md文件(你不需要在实验中运行这个文件;我只是在这里展示):
$ for f in *.md; do echo $f; done
aws.md
bash-intro.md
combinations.md
complexity.md
computation.md
data-in-memory.md
data.md
files.md
git.md
operations.md
planning.md
programming.md
reading-code.md
sqrt.md
环境健全性检查
在命令行中,键入以下命令以验证您是否可以访问python3:
$ which python3
/Users/parrt/anaconda3/bin/python3
或者,更好的是,确保默认的 Python 是版本 3:
$ which python
/Users/parrt/anaconda3/bin/python
Python 程序应该在 Anacondabin(二进制)目录中,该目录包含所有二进制可执行文件。如果你没有在which命令的输出中看到 anaconda,你需要查看 Anaconda 的文档,并找出你没有运行适当的 Python 的原因。(很可能你的PATH环境变量有问题。)现在可以使用任何 3.X 版本的 Python 运行了。
接下来,运行该程序并确保您有最新版本如 3.6.5:
$ python
Python 3.6.5 | packaged by conda-forge | (default, Mar 30 2018, 00:00:55)
[GCC 4.2.1 Compatible Apple LLVM 6.1.0 (clang-602.0.53)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>
接下来尝试导入一个库:
$ python
Python 3.6.5 | packaged by conda-forge | (default, Mar 30 2018, 00:00:55)
[GCC 4.2.1 Compatible Apple LLVM 6.1.0 (clang-602.0.53)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import pandas as pd
>>>
你不应该得到错误。
另请参阅我的书机器学习机制中的计算机环境健全性检查。
交互式 Python
现在让我们跳转到交互式 Python shell。简单来说,它与控制计算机的 bash 命令行 shell 相同。两者都是编程语言;他们只是有不同的专长。当我们从 bash shell 跳到 Python 的 shell 时,它就像是从法国跳到德国边境。我们不得不停止说法语并开始讲德语。(或者,如果你是美国人,请继续说英语,因为我们很懒,不懂任何外语。哈哈)当你退出 Python 解释器时,你会回到 bash 世界,就像你从德国到法国。
要进入 Python 世界,请从 bash $提示符输入python,就像我们在上一节中所做的那样。您应该看到它正在使用Anaconda 版本。 如果没有,这意味着您正在使用系统上的默认 Python。
现在,从 Python 提示符>>>(我们不再使用bash),键入500 + 1后跟换行符。你应该看到这样的东西:
$ python
Python 3.6.5 |Anaconda, Inc.| (default, Apr 26 2018, 08:42:37)
[GCC 4.2.1 Compatible Clang 4.0.1 (tags/RELEASE_401/final)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> 500+1
501
>>>
Python 已经求值了表达式并将结果打印回屏幕。好像我们使用了print函数调用,这给了我们相同的结果:
>>> print(500+1)
501
Python 交互式 shell 立即打印表达式值,但在将程序作为脚本运行时(即不是交互式)则不是这种情况。
要退出交互式 shell 并返回命令行(终端程序),请键入quit()(或ctrl-D)并按下回车:
>>> quit()
$
$提示符表示您回到了 bash 命令行。 您不能再键入 python 代码。
脚本化 Python
访问磁盘上合适的目录(文件夹),或创建一个,例如/Users/YOURID/msan501/inclass。(不要在任何目录或文件名中使用空格......永远不要!)现在在该目录中创建一个名为hello.py的**文本文件,其中只包含一行:
500+1
使用您选择的编辑器,虽然nano是一个很好的编辑器,因为当我们进行云计算时,您将能够在远程服务器上使用它。 Sublime 和 TextEdit.app 也有效。
这个“代码”正是您在交互式 Python shell 中首先输入的内容。 将文件保存在inclass目录或任何调用目录中。
一旦使用编辑器将 Python 文件写入磁盘,您应该能够使用命令行中的cd(更改目录)跳转到该目录:
$ cd /Users/YOURID/msan501/inclass
使用ls获取目录列表:
$ ls
hello.py
通过在命令行键入以下内容,验证是否已正确创建 Python 脚本:
$ cat hello.py
500+1
$
以下是最常见错误的解决方案:
- 不要将
.txt放在文件名的末尾;它必须是.py - 不要使用 MS Word 或任何其他文字处理器;你认为它是文本,但事实并非如此。有很多文本编辑器,包括 Mac 的
TextEdit.app。只需确保保存为纯文本而不是“富文本”。还有很多文本编辑器,如 Sublime 和 TextMate。(如果你真的很硬核,你将学习vi或emacs,你会看到我在课堂上使用它。)你也可以在命令行中使用nano,直接在命令行窗口中进行编辑。
现在,我们将运行该程序/脚本:
$ python hello.py
$
我们没有任何输出。这是一个至关重要的区别。交互式 Python shell 会立即打印表达式值,因为它是交互式的。当您从命令行运行文件时,它假定您希望像脚本一样,以批处理模式执行代码。这就是为什么我们没有print语句就得不到任何输出。
现在编辑该文件并将其更改为:
print(500+1)
保存文件并重新运行。 现在您应该看到:
$ python hello.py
501
$
Jupyter 笔记本(通过 Jupyter Lab)
现在,除了使用基于 Jupyter Lab 浏览器的环境之外,我们将做同样的事情。 (请参阅正在编写的 ML 书中的您的机器学习开发环境来了解更多信息。)
从命令行启动 Jupyter:
$ jupyter lab
I 11:27:00.606 LabApp] [jupyter_nbextensions_configurator] enabled 0.2.8
[I 11:27:00.613 LabApp] JupyterLab beta preview extension loaded from /Users/parrt/anaconda3/lib/python3.6/site-packages/jupyterlab
[I 11:27:00.613 LabApp] JupyterLab application directory is /Users/parrt/anaconda3/share/jupyter/lab
[W 11:27:00.616 LabApp] JupyterLab server extension not enabled, manually loading...
...
这将启动一个程序,在您的浏览器中启动一个选项卡:
单击Notebook类别下的Python 3图标,可为您创建一个新的笔记本窗口:
在In []旁边的第一个单元格中键入500 + 1。您应该看到在它下面的Out部分生成的输出 501。按control-enter,或工具栏中的右三角形来执行该单元格。
这是一个交互式环境,所以你可以返回去编辑500 + 1,比如说print(500 + 1)。 这样做,然后再次点击control-enter来运行。 你应该得到相同的输出。
您应该能够非常快速地测试 Python 小程序或代码段。重复这些过程,直到它们成为第二本能。
1.3 播放声音
译者:飞龙
本实验的目标是让您感到惊讶,您可以使用 Python 处理一些音频文件。您将无法获得所有详细信息,但您可以剪切并粘贴此实验来启动 Python 会话。作为次要目标,您将习惯于安装 Python 包和命令行工具。
首先,下载以下两个音频文件,以便我们拥有一些要处理的原材料。
我建议你将它们存储在如下目录中:
/Users/YOURID/msan501/labs/sound
其中YOURID是您的计算机登录名。 例如,我的登录名是parrt。在 Linux 上它会像:
/home/YOURID/msan501/labs/sound
安装命令行工具
事实证明,我们将在 Python 中使用的声音库,不知道如何处理 mp3 文件。 我们需要将ahhh.mp3文件转换为ahhh.wav,这些库知道如何处理它。 为此,我们将使用名为 mpg123 的工具。
要在 Mac 上安装大多数免费软件,我们使用一个名为 homebrew(家酿啤酒)的程序(从命令行访问只是brew)。它可能已预先安装在您的计算机上,但如果没有,请阅读说明并进行安装。这是一个切割和粘贴以/usr/bin/ruby...开头的单行的问题
一旦安装完毕,我们就可以用它来安装mpg123。 从终端运行以下brew命令,该命令应该给出指示的输出:
$ brew install mpg123
...
==> Downloading https://homebrew.bintray.com/bottles/mpg123-1.25.0.el_capitan.bottle.tar.gz
######################################################################## 100.0%
==> Pouring mpg123-1.25.0.el_capitan.bottle.tar.gz
==> Using the sandbox
/usr/local/Cellar/mpg123/1.25.0: 26 files, 765.2KB
在 Linux 上,mpg123 的安装是:
sudo apt-get install mpg123
或者:
$ sudo yum install mpg123
将mp3转换为wav文件
现在,我们安装了 mpg123,我们可以使用它将.mp3转换为.wav文件。 使用cd(更改目录)命令进入保存ahhh.mp3文件的目录,并使用ls列出文件:
$ cd /Users/YOURID/msan501/labs/sound
$ ls
Kiss.aiff ahhh.mp3
使用以下命令转换文件:
$ mpg123 -w ahhh.wav ahhh.mp3
High Performance MPEG 1.0/2.0/2.5 Audio Player for Layers 1, 2 and 3
version 1.25.0; written and copyright by Michael Hipp and others
free software (LGPL) without any warranty but with best wishes
Terminal control enabled, press 'h' for listing of keys and functions.
Playing MPEG stream 1 of 1: ahhh.mp3 ...
MPEG 1.0 L III cbr192 44100 j-s
Comment: 00000000 00000210 000009F5 000000000004FF7B 00000000 0002AB44 00000000 00000000 00000000 00000000 00000000 00000000
[0:07] Decoding of ahhh.mp3 finished.
在那个输出中有很多乱码,我们并不关心,但要确保它表示“完成”,并且看起来没有任何错误。 看一下当前目录中的文件,看看是否出现了ahhh.wav:
$ ls
Kiss.aiff ahhh.mp3 ahhh.wav
如果您无法弄清楚如何转换为ahhh.wav,请不要担心。 你可以[从我的笔记下载它](../ notes / sound / ahhh.wav)。
此时,我们已经学会了使用cd跳转到文件系统中的不同位置,并使用ls列出目录中的文件。 我们使用brew作为非 Python 包的install命令。
安装 Python 包
为了从 Python 读取和播放声音文件,我们需要利用一些现有的 Python 代码。 代码通常以包含库或包的形式提供;这些术语或多或少在实践中同义使用。
- 对于读取声音文件,我们需要 [pysoundfile]( http://pysoundfile.readthedocs.io/en/0.9.0/ 。
- 对于播放声音文件,我们需要 [sounddevice]( http://python-sounddevice.readthedocs.io/en/0.3.7/ 。
如果我们在安装包之前尝试使用包,那么 Python 执行器会向我们显示如下错误。
---------------------------------------------------------------------------
ImportError Traceback (most recent call last)
<ipython-input-61-b5992dda2a80> in <module>()
----> 1 import soundfile as sf
ImportError: No module named soundfile
关键是在import语句中,我们得到错误No module named soundfile。这不应该搞砸你的一天 - 这只是意味着我们必须安装该软件包。
要安装 Python 包,我们使用pip(或稍后,conda)程序。通常这个程序可以快速轻松地完成我们想要的操作,但是很多事情都可能出错。最常见的是你的pip程序与python程序不匹配。首先使用which验证它们是否来自同一个bin目录:
$ which pip
/Users/parrt/anaconda3/bin/pip
$ which python
/Users/parrt/anaconda3/bin/python
$ which python3
/Users/parrt/anaconda3/bin/python3
现在,让我们安装pysoundfile包:
$ pip install pysoundfile
Collecting pysoundfile
Downloading PySoundFile-0.9.0.post1-py2.py3.cp26.cp27.cp32.cp33.cp34.cp35.cp36.pp27.pp32.pp33-none-macosx_10_5_x86_64.macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.whl (573kB)
100% | | 573kB 1.4MB/s
Requirement already satisfied: cffi>=0.6 in /Users/parrt/anaconda2/lib/python2.7/site-packages (from pysoundfile)
Requirement already satisfied: pycparser in /Users/parrt/anaconda2/lib/python2.7/site-packages (from cffi>=0.6->pysoundfile)
Installing collected packages: pysoundfile
Successfully installed pysoundfile-0.9.0.post1
LINUX用户:你需要执行conda install libgcc来使这个声音组件生效。
同样安装其他包:
$ pip install sounddevice
Collecting sounddevice
...
为了确保我们已正确安装软件包,我们可以尝试使用交互式 Python shell python或ipython导入它们:
$ python
Python 3.6.5 | packaged by conda-forge | (default, Mar 30 2018, 00:00:55)
[GCC 4.2.1 Compatible Apple LLVM 6.1.0 (clang-602.0.53)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import soundfile as sf
>>> import sounddevice as sd
这一次,我们的import语句没有出错。
用 Python 播放声音
此时,您已经转换了一个文件,以便我们的 Python 库可以使用它,并且您已经安装了这些 Python 库。 在这样做时,你使用了一些你应该非常熟悉的命令行工具:brew,pip,which,cd,ls,python等等...
现在是时候把所有这些放在一个简单的 Python 程序中,你可以剪切和粘贴来播放声音。 让我们从代码开始播放Kiss.aiff文件。 在音频文件的同一目录中创建一个名为play.py的文件。 您可以使用nano或其他文本编辑器。 这是代码:
import soundfile as sf # Use this package
import sounddevice as sd # and this one
kiss, samplerate = sf.read('Kiss.aiff') # load Kiss.aiff into kiss variable
sd.play(kiss, samplerate) # start playing the music
sd.wait() # wait until music finishes before exiting
使用以下命令从命令行运行此程序:
$ python play.py
现在编辑程序并将'Kiss.aiff'字符串更改为'ahhh.wav':
...
kiss, samplerate = sf.read('ahhh.wav')
...
再次执行程序,您应该听到播放其他声音文件。
最常见的错误是声音文件没有与代码位于同一目录中。
生成我们自己的声音
在声音章节中,我们看到了如何产生正弦波,当我们将它扔给扬声器时,正弦波会产生纯音。 创建一个puretone.py文件并在其中放入以下代码。
import numpy
import sounddevice as sd
fs = 44100 # sampling frequency
T = 1.5 # seconds
t = numpy.linspace(0, T, int(T*fs), endpoint=False) # time variable
y = numpy.sin(2*numpy.pi*440*t) # pure sine wave at 440 Hz
sd.play(y, fs)
sd.wait()
从命令行运行它。通过归纳其他程序的执行,您应该知道如何运行这个新程序,它是:
$ python puretone.py
将440更改为更高的值,如1000:
...
y = numpy.sin(2*numpy.pi*1000*t) # pure sine wave at 1000 Hz
...
再次运行时,您应该听到更高的音调。
正如我们在演讲中所做的那样,让我们一起添加两个音调来产生更酷的声音。 带有两个正弦波结构的正弦波代码如下所示:
...
y = numpy.sin(2*numpy.pi*440*t) # pure sine wave at 440 Hz
y2 = numpy.sin(2*numpy.pi*1000*t) # pure sine wave at 1000 Hz
...
信号在'y和y2`变量中。 现在,改变播放声音的语句,以便在播放之前将信号添加到一起:
...
sd.play(y+y2, fs)
...
保存程序并运行它。 您应该听到两个声音一起播放为一个声音。
现在,让我们看看这个信号在视觉上是什么样的。 创建一个名为twotonesplot.py的文件并输入以下代码:
import numpy
import sounddevice as sd
import matplotlib.pyplot as plt
fs = 44100 # sampling frequency
T = 1.5 # seconds
t = numpy.linspace(0, T, int(T*fs), endpoint=False) # time variable
y = numpy.sin(2*numpy.pi*440*t) # pure sine wave at 440 Hz
y2 = numpy.sin(2*numpy.pi*1000*t) # pure sine wave at 1000 Hz
plt.scatter(t[0:1000],(y+y2)[0:1000],s=1)
plt.show()
看看您是否可以看到此代码与之前代码之间的关系。 我们有另一个import语句,它引入了一些绘图代码,我们用它来生成一个散点图,用最后两行替换play/wait序列。 除此之外,代码是相同的。 运行该代码,你应该看到一个漂亮的摇摆正弦波,这是两个不同频率的纯正弦波:
声音处理
减小音量
为了让你相信声波只是数字,让我们从音频文件加载的数字更小。当我们演奏它时会产生什么声音?是的,它应该变得更安静。这是一个名为softer.py的play.py程序的版本,它有一个额外的行来减少振幅:
import soundfile as sf # Use this package
import sounddevice as sd # and this one
kiss, samplerate = sf.read('Kiss.aiff') # load Kiss.aiff into kiss variable
kiss = kiss * .1 # Reduce amplitude to make quieter
sd.play(kiss, samplerate) # start playing the music
sd.wait() # wait until music finishes before exiting
该行将数字信号中的每个值除以 10:
kiss = kiss * .1 # Reduce amplitude to make quieter
事实证明,当我们将信号强度除以 10 时,我们的耳朵听到的声音减小一半。(术语分贝(decibel)应该是个双关语,Decimate意味着除以 10。)
减慢音频(并改变音高)
让我们说我们想加深 Prince 的声音。 我们所要做的就是减慢音乐速度。 要做到这一点,我们所要做的就是玩一玩采样率。 将play.py文件复制到slow.py并将play行更改为:
sd.play(kiss, samplerate*.8) # play the music at 80% speed
现在运行slow.py并且 Prince 的声音在最后听起来很怪异。 整个东西听起来很慢。 直观地说,降低播放时的采样率会降低速度,因为播放器每秒消耗更少的样本。 这会使声音延长,就像用手指放慢唱片转盘一样。
可以减慢音乐速度并且不更改音高,这在尝试将音乐从音频转换为乐谱时非常有用。构建我所使用的转录软件的人,很好的描述了不改变音高并减慢音乐所涉及的技巧。
答案
您可以在 msan501 类库中找到本实验的所有代码。 这是声音文件。
总结
声音处理是一个非常有趣的迷人话题,你刚刚在这里做了一些尝试。我希望这个实验可以激励您深入了解编程的细节,以便您可以学习为自己构建这些类型的程序。 在第一个项目中,您将了解图像处理,您猜对了,一切都是数字。
您还了解了如何使用命令行安装其他命令行程序和 Python 包。 当您编写越来越复杂的程序时,这将是您将一次又一次地使用的东西。
一、起步
2.1 编程导论
原文:Introduction to programming
译者:飞龙
计算机科学不仅关于计算机,就跟天文学不仅关于望远镜一样。
-- Edsger Dijkstra
我记得面对我的第一个编程任务(1980 年的 BASIC!),我完全没做出来。我甚至不知道如何开始解决这个问题。我很难过,尽管编码对我来说很快就会变得非常自然。我最初的困难的原因现在显而易见:教师完全没有提供将问题转换为正在运行的程序的技术或策略。我必须自己解决这个问题。
在介绍性课程中关注编程语言语法的方法是可以理解的。 解决问题不是一种精确,定义好的技能。 它更像是一种通过练习磨练的整体能力。 因此,教学和评分是具有挑战性的。 立即跳转到一些简单的编程语言语句的语法,要容易得多。 这种方法具体,原则上易于理解,但完全忽略了我们什么时候以及为什么需要这些语句。 可以在这种环境中作为学生而生存的教授,在教其他程序员时通常会继续使用孤注一掷的方法。
在本课程中,我想通过专注于解决问题和学习编写复杂的 Python 代码来纠正这一问题。要做到这一点,我们将遵循一个整体的问题解决策略,包括设计“工作计划”或“算法”,无论是纸上还是头脑中(当你获得更多经验时)。在我们进入编码阶段很久之前,该计划就帮助我们思考问题。计划的一部分是确定一个解决我们问题的合适操作顺序。这是一个棘手的问题,因此我们将通过以下方式缩小解决方案空间的范围:(1)将自己限制在一组通用的操作和数据结构中,(2)应用成熟的方法,我们称之为“向后工作”和“简化为一个已知的解决方案“,最后,(3)利用这个入门课程的主题特性,采用一个适用于大多数数据科学问题的程序大纲。当我们最终进入 Python 编程时,我们将自己局限于该语言的有用子集。目标是教你编程,而不是教你完整的 Python 语言。
请允许我首先区分编程(问题解决)和编码(用特定编程语言表达我们的解决方案)。
编程是什么?
当我们考虑编程时,我们会立即考虑编程语言,因为我们使用特定的语言语法表达自己。 但是,这就像向物理学家询问他们讨论物理学的语言。 编程主要是将“单词问题”(项目描述)转换为执行计划。 当然,编码(输入代码)的最终行为是必需的,但学习在精神上解决编程问题是最困难的过程,也是最重要的过程。
自然语言也是如此。 学习证明数学定理比学习用某种自然语言编写证明更难。 实际上,大多数数学语法在自然语言中都是相同的,就像编程语言一样。 像在数据科学计划中一样,用 Python 或 R 表达您的想法是编程过程中最简单的部分。 也就是说,编写正确的代码通常是该过程中最令人沮丧和耗时的部分,即使对于有经验的程序员也是如此。
编程更多是要表达什么而不是如何表达。 用计算机解决问题意味着识别一系列操作,每个操作都解决了整个问题的一部分。 每个操作本身可能是一系列子操作。 用 Python 或 R 表达这些操作并不困难。 确定哪些操作及其相对顺序是困难的部分。
让我们从解决编程问题的整体策略入手。
解决问题的策略
无论我们尝试编写什么软件,我们都可以遵循解决问题的整体策略。
在任何问题解决的情况下,第一步是充分理解问题并清楚地确定目标。 这可能听起来很明显,但是我们对这个问题的理解中的任何模糊性都可能使我们走错方向。 在数据科学环境中,目标通常是我们试图回答的问题,例如“哪个销售区域的同比增长最快?”(摘要统计量),“哪些交易是欺诈性的?”(分类器)或“未来某个日期股票价格是多少?”(预测器)。 我们应该能够使用英语单词精确地表达目标和预期输出。 如果我们不能这样做,那么 Python 或 R 中没有任何编码的专业知识可以解决问题。 我们很快就会看到一些例子。
问题解决过程的第二步(或可能是第一步的一部分)是手动写出一些输入 - 输出对。 这样做有助于我们了解程序需要做什么以及如何执行。 我们将要看到,这种技术不仅适用于整体输入和输出,而且适用于设计函数(可重用的代码段)。 如果我们无法手动识别和执行操作,我们无法使用代码自动执行操作。此外,列出一堆案例通常会突出特殊情况,例如“当输入为负时,输出应为空”。 换句话说,程序不应该以负数作为输入而崩溃。 程序员称之为测试驱动设计。
在求职面试设置中,此步骤意味着立即尝试绘制问题的几个实例。 例如,如果要求以某种方式处理数字列表,首先将三个或四个数字放在板上或纸上。 这自然会带来一些面试官期待你提出的重要问题,比如数据的来源以及它是否适合内存等......
第三步是弄清楚我们实现目标所需的数据或输入,即我们的原材料。 没有正确的数据,我们无法解决问题。 例如,我曾指导过一个学生实习团队,其目标是确定某个网站的哪些客户会升级到专业帐户。 学生只有已升级的用户数据,没有拒绝升级的用户数据。哎呀! 如果您只有苹果的数据,则无法构建苹果与橙子分类器。 如果您没有所需的所有数据,那么将此要求确定为问题解决过程的一部分非常重要。 数据采集通常需要编程,我们将回顾下面的主题,作为我们通用计划大纲的一部分。
在这一点上,我们实际上已经设定了解决问题所需的阶段,我们根本没有考虑过代码。 我们从最终结果开始,然后确定了我们需要的数据。 输入 - 输出对巧妙地包含了我们需要执行的计算。 一开始,我们有已知的数据,最后,我们有预期的输出或作品。 好的,进入编程步骤。
第四步是确定计算预期结果的操作顺序。 有时这被称为算法并且涉及规划输入数据上的特定操作和子操作,逐渐将其转换为预期输出。
前四个步骤是所谓的费曼技巧的关键部分,其中包括写下已分配任务或问题的完整说明,就像你对非专家解释它那样。直到你可以简单地写下来,而不会混淆语言或术语,你自己不明白这个问题。在你完成这个阶段之前,没有必要继续下去。(教师经常开玩笑,学习新主题的最佳方法是教授关于该主题的课程!)
在第五步中,我们将计划中的操作转换为实际的可执行代码。 这一步需要整本书,但这里总结了我的建议。 从最简单的子操作开始,确保它们先工作。 然后编写使用这些子操作的较大操作。 如果出现问题,您就会知道新代码中的子操作可能没有经过测试。 在这个阶段,我们通常会发现第四步中的设计问题,因此我们通常会重复四五次。 测试功能和修复错误称为调试。
最后,第六步是检查我们的整体结果的正确性。 最明显的检查是比较程序的输出与步骤 3 中的已知输入-输出对。 然后,最重要的是,在第 3 步到第 5 步中使用未考虑的输入来测试程序。 这是对程序通用性的重要测试。 如果程序输出错误,则返回第 4 步来查看错误。
而现在,出于现实因素。世界是一个非常混乱的地方,因为我们开始知道最少的问题,所以我们通常需要通过一些或所有这些步骤重复或反弹。 例如,假设我们正在构建一个苹果与橙子分类器,上面的过程使程序不能很好地区分这两个水果。 也许我们只有大小和形状的数据。 我们可能会认为分类器需要颜色数据,所以它回到第二步(可能是第三步),然后是第六步再次检查结果。
制定计划和程序
程序是一系列操作,用于转换数据或执行计算,它最终产生预期输出。编程是设计程序的行为:识别操作及其适当的顺序。 换句话说,编程就是为计算机提出一个工作计划,我们经常用半精确的英文描述,叫做伪代码。这是上一节中的第四步。
另一方面,编码是将这种高级伪代码转换为编程语言语法的行为。 随着您获得更多经验,在没有伪代码步骤的情况下,直接从工作计划变成代码变得更容易。
在第一次学习编程时,将已建立的模式,模板,策略和常见的数据转换操作用作拐杖,是有帮助的。 例如,在下一节中将查看数据科学程序的模板,它们在整个程序中的大多数情况下都可以使用! 在 Python 中的编程模式中,我们将看到许多模式,您可以拼凑起来创建程序。
如上所述,您还可以使用两种策略或一般准则来处理程序设计过程:
- 从最终结果开始,向后工作,询问每个步骤的先决条件。 换句话说,步骤 i 之前的一个或多个处理步骤,计算步骤 i 所需的数据或值。 例如,在计算平均值之前,我们无法打印某些数字的平均值。 在我们对这些数字求和之前,我们无法计算平均值。 我们不能综合,直到我们将这些数字加载到内存等...
- 使用已知解决方案将新问题归约或简化为现有问题的变体。要应用这种新方法,请询问您尝试解决的问题与您有解决方案的其他问题之间的区别。
这两种技术在建筑,工程和数学方面都是众所周知的。 例如,想象一下你想在离地面 10 英尺的地方竖立一尊沉重的雕像。 结构工程师可能会认为沉重的雕像需要一个直的金属底座。 然后,为了支撑所有这些重量,四个 10 英尺的钢梁应该支撑金属底座。 钢梁应在地面上有深层混凝土基座,等等。这是从最终结果倒退的。
作为重用的一个例子,建造一座新吊桥的工程师不会像以前从未建造过这样的东西那样。 他们可能会采用并调整现有的设计来适应新的情况。
另外,计划重用通常用于开其他学科的玩笑。 例如,来自物理学家笑话的集合,这里有一个变体:
一位物理学家和一位数学家正坐在教职员休息室里。 突然间,咖啡机着火了。 物理学家抓起一个水桶,跳向水槽,把水桶装满水,灭火。 第二天,同样的两个人坐在同一个休息室。 咖啡机再次着火。 这一次,数学家站起来,拿起一个桶,把桶交给物理学家,从而将问题归约到先前解决的问题。
练习:给定一个包含十进制数字的字符串,例如s = "501",打印出各个数字的总和。 在这种情况下,输出应为6 = 5 + 0 + 1。 提示:int('9')产生值 9。从期望的结果,求和,向后工作,找出你需要的东西。 例如,结果是数字的总和。 这意味着我们需要数字。 要获取数字,我们可以遍历字符串的字符,或者我们可以将字符串转换为字符列表并迭代它。 在我们迭代时,我们可以求和数字值。 总而言之,我们需要初始化一个临时结果变量,可能称为n。
练习:在A中给出数字列表,原地反转数字(意思是没有单独的A的副本,以及不创建新的列表来返回)。 首先在白板上写一个例子。 此练习对于在图像项目中的翻转图像非常有用。
如果您遇到困难,或只是检查您的答案,您可以查看我的答案。
现在我们已经有了解决问题的整体策略,让我们来看一个程序大纲,它将帮助我们上手您需要构建的任何数据科学程序。
数据科学程序模板
经验丰富的程序员从一组通用心智模板中抽取起点。 有桌面 GUI 应用程序,机器学习分类器,Web 服务器等模板....模板提供程序的整体结构,程序员只需根据特定问题进行定制。
依靠心智模板甚至物理模板非常常见,而不仅仅是编程。 律师拥有合同的通用模板,编剧有各种电影类型的通用脚本。 例如,大多数动作电影都是这样的:遇见坏人;遇见英雄;追逐场景;英雄克服了很大的困难来打败坏人和他的仆从。由于所需的精度,编程与编写法律文档最相似。 丢失的单词或标点可能导致程序崩溃或合同签字人破产。(例如,参见毁坏 NASA 火箭的错别字)。
获得作为程序员的经验,意味着识别代码中的模式,并在脑海中创建通用模板来供将来使用。 在开始使用时,您可以通过重用现有的代码库和使用相关模板,来依赖其他程序员的经验。 这将我们引领到以下通用数据科学程序模板,该模板适用于您可能遇到的大多数问题:
1.获取数据,这意味着找到合适的文件或从 Web 收集数据并存储在文件或数据库中
2.从磁盘或数据库加载数据,并放入组织成数据结构的内存
2.规范化,过滤,清理或以其他方式准备数据
3.处理数据,这可能意味着训练机器学习模型,转换数据,计算摘要统计量或优化成本函数
4.输出结果,可以是任何东西,从简单地将答案打印,到保存数据到磁盘以及生成奇特的可视化
为特定问题编写程序意味着要弄清楚每个步骤是什么,尽管并非所有程序都会使用每一步。
致谢。与 Kathi Fisler 的对话,为这里总结的有原则的,有计划的编程方法提供了很多灵感。 有关设计秘籍的更多信息,请参阅[通过自举从计算到代数的解决单词问题的转移技巧](https://cs.brown.edu/~sk/Publications/Papers/Published/sfkf-trans-word-prob-comp-alg-BS/ paper.pdf)。
2.2 在内存中表示数据
原文:Representing data in memory
译者:飞龙
典型的程序要求我们从磁盘加载数据并放入组织成数据结构的内存中。我们在内存中表示数据的方式对于构建程序至关重要。数据科学大纲尤其如此,因为处理数据是我们的焦点。
首先,让我们直接了解数据。数据元素具有值和类型,例如32和整数 或"hi"和字符串。我们通过组合和组织这些数据元素(例如整数列表)来构建数据结构。
我们还有一个名为指针或引用的特殊元素,它引用另一个元素。这就像一个电话号码“指向”手机,但不是手机本身。使用指针我们可以找到手机。指针列表就像指向人们的电话簿,但电话簿实际上并不是人的列表。(我们稍后会看到,即使我们做了像x = 3之类的简单操作,变量x也是秘密地指向值为 3 的整数对象的指针。)
接下来,让我们稍微了解一下计算机体系结构,来了解将内容加载到内存中的含义。
计算机体系结构
计算机由三个主要组件组成:一个用于存放数据的磁盘,一个存储器(在断电时擦除),以及一个处理该数据的处理器(CPU)。 这是一张实际 CPU 和一些内存芯片的图片:
计算机内存(RAM == 随机存取内存)要快得多,但通常比磁盘小很多,并且当计算机关机时所有内存都会丢失。 将内存视为工作空间或临时空间,将磁盘视为永久存储。内存芯片有点像人类的短期以及,相对于一张读写速度较慢而且持久的纸张而言,它很容易消失。
存储器被分解成固定大小的离散单元。单元的大小是一个字节,由 8 位(二进制开/关数字)组成。 存放 0 到 255 之间的数字就足够了。每个单元都用整数地址标识,就像邮箱上的数字一样(见下图和右图)。 处理器可以请求特定地址处的数据,并且还可以在特定存储器位置存储一条数据。例如,这是字节可寻址的计算机内存的抽象表示:
在这种情况下,存储器在地址 0 处具有值 100。在地址 1 处,存储器具有值 0。地址 4 具有我们可以存储在单个字节中的最大值:255。
从实际数字到音乐到视频的所有内容,使用一个或多个称为字节的原子存储单元来存储。
所有内容都存储为计算机中的数字或数字序列,甚至是文本。
数据存在于内存中,磁盘上,或者可以从网络中检索。作为编程计划的一部分,您需要知道数据所在的位置,以便将这些数据作为计划的一部分加载到内存中。
计算机体系结构的指标
以下是我们在计算机体系结构中使用的关键单位:
- Kilo. \(10^3 = 1,000\) or often \(2^{10} = 1024\)
- Mega. \(10^6 = 1,000,000\)
- Giga. \(10^9 = 1,000,000,000\)
- Tera. \(10^12 = 1,000,000,000,000\)
您需要知道这些单位,因为您需要知道数据集是否适合内存,或者它是否适合磁盘,甚至需要多长时间才能通过网络传输。
例如,当我刚开始时,我的第一台微型计算机有 16k 的 RAM,但我的桌面机现在有 32G 的 RAM。内存大小增加的比例是多少?
CPU 按照时钟的心跳执行指令,在这里我们得到术语时钟速率。 Mhz(百万赫兹==周期/秒),Ghz(十亿)是每秒时钟周期中的典型单位。 我的桌面具有 4Ghz 时钟速率,这意味着您每秒可以执行大约 4 千兆或十亿条指令。 好多啊。
如果您的网络是 100Mbits 每秒,那么您可以在 8 秒内传输 800Mbit(100M 字节)文件。
旧金山电话簿(未压缩)有多大? 你能以多快的速度在 8Mbit 每秒的网络上传输该电话簿?
内存的编程语言视角
编程语言以两种方式向我们提供更高级别的内存视图:我们可以使用名称来引用内存中的位置,每个内存单元可以保存任意大小的整数和实数值(它们确实有一个限制,但让我们现在保持简单)。 例如,以下是存储在内存中的两个命名值:
units = 923
price = 8.02
When referring to the kind of thing a value represents, we use the word type. The type of the "units" cell is integer and the type of "price" is real number (or floating-point number).
type(units)
type(price)
另一个非常常见的值类型是字符串,它实际上是一个字符列表。 我们使用字符串来保存地名,书名和任何其他基于文本的值。 我们可以将字符串视为单个值,因为编程语言隐藏了细节。 字符串可以任意长,编程语言将字符串存储为内存中的字节序列。 每个字符占用一个或两个字节。 换句话说,我们认为它是:
name = "Mary"
type(name)
但是它更像是:
使用 lolviz 包,我们可以可视化简单的类型,例如字符串:
from lolviz import *
strviz(name)
objviz(name) # render as list of char
这些基本数据类型:
- 整数
- 浮点数
- 字符串
是我们的基石。 如果我们将其中一些块安排在一起,我们可以创建更复杂的结构。
数据结构
列表
最常见的数据结构是列表,它只是一系列存储单元。 因为我们都熟悉电子表格,所以让我们使用电子表格可视化这些数据结构。 电子表格中的列实际上是列表,例如以下整数,浮点数和字符串列表/列:
Quantity = [6, 49, 27, 30, 19, 21, 12, 22, 21]
type(Quantity)
# list
len(Quantity)
# 9
objviz(Quantity)
我们也可以将电子表格的行视为列表。 例如,电子表格的标题行实际上是一个字符串列表:
headers = ['Date', 'Quantity', 'Unit Price', 'Shipping']
type(headers)
# list
objviz(headers)
所有这些列表都有一个共同点:元素的类型是相同的。它们是同构的。但是,我们也可以使用异构元素列表,这通常是我们在电子表格行中看到的:
arow = ['10/13/10', 6, 38.94, 35, 'Muhammed MacIntyre']
或者:
from datetime import date
arow = [date(2010, 10, 13), 6, 38.94, 35, 'Muhammed MacIntyre']
arow
# [datetime.date(2010, 10, 13), 6, 38.94, 35, 'Muhammed MacIntyre']
type(arow)
# list
listviz(arow)
异构列表通常用于对特定实体的信息位进行分组。 在机器学习中,我们称之为特征向量,实例或观测。 例如,苹果与橙子分类器可能具有包含重量(数值),体积(数值)和颜色(字符串)的特征向量。 这里重要的一点是,列表也可以用作聚合特定实体的特征的方法。 元素的顺序不如它们在同一列表中包含(聚合)的事实重要。
元组
元组是一个不可变列表,通常用于从函数返回多个值。 它也是一种简单的方法来组合相关元素的数值,例如:
me = ('parrt',607)
me
# ('parrt', 607)
我们像使用列表一样索引元素:
print(me[0])
print(me[1])
'''
parrt
607
'''
但是,没有办法像列表那样改变元素。 如果我们这样做:
me[0] = 'tombu'
结果是个错误:
TypeError: 'tuple' object does not support item assignment
这是使用多重赋值语句拆分元组的示例:
userid,office = me
print(userid)
print(office)
'''
parrt
607
'''
元组是分组相关项的好方法,无需创建正式的 Python 类定义。
集合
如果我们强制确保一个规则,列表中的所有元素都是唯一的,那么我们得到一个集合。集合是无序的。
ids = {100, 103, 121, 102, 113, 113, 113, 113}
ids
# {100, 102, 103, 113, 121}
type(ids)
# set
objviz(ids)
我们可以做很多有趣的集合运算:
{100,102}.union({109})
# {100, 102, 109}
{100,102}.intersection({100,119})
# {100}
表(列表的列表)
电子表格一个接一个地排列行,程序员将其解释为列表的列表。在分析或数据库世界中,我们称之为表:
在此示例中,每行代表一个销售交易。
机器学习算法的输入通常是一个表,其中每一行聚合与特定实例或观测相关联的数据。 这些表称为数据帧,将成为您的 BFF。
from pandas import DataFrame
df = DataFrame(data=[[99,'parrt'],[101,'sri'],[42,'kayla']],
columns=['ID','user'])
df
| ID | user | |
|---|---|---|
| 0 | 99 | parrt |
| 1 | 101 | sri |
| 2 | 42 | kayla |
df.values
'''
array([[99, 'parrt'],
[101, 'sri'],
[42, 'kayla']], dtype=object)
'''
df.columns
# Index(['ID', 'user'], dtype='object')
df.user
'''
0 parrt
1 sri
2 kayla
Name: user, dtype: object
'''
objviz(df.values)
矩阵
如果表元素都是数字,我们称之为矩阵。 这是一个包含 5 行和 2 列的矩阵:
让我向您介绍另一个新的 BFF,numpy:
import numpy as np
A = np.array([[19,11],
[21,15],
[103,18],
[99,13],
[8,2]])
print(A)
'''
[[ 19 11]
[ 21 15]
[103 18]
[ 99 13]
[ 8 2]]
'''
这是一个形状为 5 行,2 列的矩阵:
A.shape
# (5, 2)
lolviz(A.tolist())
有许多方法可以表示或布局事物和内存。在这种情况下,我们可以使用lolviz将矩阵视为列表列表:
或者作为一个矩阵:
objviz(A)
我们可以用numpy做很多矩阵数学:
objviz(A+A)
objviz(A*99)
objviz(A.T) #transpose
这里是个方程组:\(A x = b\), \(x = A^{-1} b\):
使用 numpy,我们可以使用 $ A $ 的逆来解决这个问题。
from numpy.linalg import inv
A = np.array([[38, 22], [42, 30]])
b = np.array([3, 5])
x = inv(A).dot(b)
objviz(x)
这里有一些东西,关于各种 numpy n 维数组形状:
x = np.array([3, 5]) # vertical vector with 2 rows
y = np.array([[3, 5]]) # matrix with 1 row and 2 columns
z = np.array([[3],[5]]) # matrix with 2 rows, 1 column
print(x.shape)
print(y.shape)
print(z.shape)
'''
(2,)
(1, 2)
(2, 1)
'''
元组(2,)表示具有 2 个元素的一维向量。 我们不能使用符号(2)因为这只是一个表达式而不是一个元组。 这是一个怪癖但是必要的。
遍历列表
电子表格模型对于理解数据结构是一个很好的模型,但重要的是要记住计算机一次处理一个元素(数字或字符串)。
作为人类,我们可以完整地查看上面的电子表格或数据结构,但程序必须遍历数据结构的元素。这有点像在列表元素上滑动放大镜:
这种遍历的概念抽象为元素的任何序列(或流),而不仅仅是列表。 例如,我们最终将遍历文本文件的行或从操作系统获得的文件名序列。 序列非常强大,因为它允许我们处理比计算机内存大得多的数据。 我们可以零散地处理数据,而列表则要求所有元素同时存在于内存中。
通常我们使用for语句遍历列表的元素:
译者注:Python 没有普通的
for语句,它的for就是foreach。
for q in Quantity:
print(q)
'''
6
49
27
30
19
21
12
22
21
'''
这里,q指向的对象的类型是int。 我们还可以使用索引的循环来遍历该列表:
for i in range(len(Quantity)):
print(Quantity[i])
'''
6
49
27
30
19
21
12
22
21
'''
对于完全适合内存的列表和其他结构,我们经常会发现反向遍历有用,它会从后到前检查元素:
for q in reversed(Quantity):
print(q)
'''
21
22
12
21
19
30
27
49
6
'''
字典
如果我们并排排列两个列表并将它们粘合在一起,我们会得到字典。 字典将一个值映射到另一个值,就像现实世界中的字典将单词映射到定义一样。 这是一个示例字典,将电影标题映射到获得奥斯卡奖提名的年份:
{'a':1,'b':3,'hi':45}
# {'a': 1, 'b': 3, 'hi': 45}
movies = {'Amadeus':1984, 'Witness':1985}
print(movies)
objviz(movies)
# {'Amadeus': 1984, 'Witness': 1985}
print(movies.keys())
print(movies.values())
'''
dict_keys(['Amadeus', 'Witness'])
dict_values([1984, 1985])
'''
遍历字典也很容易,但我们必须决定是否要遍历键或值:
movies = {'Amadeus':1984, 'Witness':1985}
for m in movies: # walk keys
print(m)
'''
Amadeus
Witness
'''
for m in movies.values(): # walk values
print(m)
'''
1984
1985
'''
for (key,value) in movies.items():
print(f"{key} -> {value}")
'''
Amadeus -> 1984
Witness -> 1985
'''
movies['Amadeus']
# 1984
movies['foo']
获得了KeyError:
---------------------------------------------------------------------------
KeyError Traceback (most recent call last)
<ipython-input-40-72c06b90f573> in <module>()
----> 1 movies['foo'] # gets a KeyError
KeyError: 'foo'
总结
这里是一些常用数据类型:
- 整数,例如
-2, 0, 99 - 实数(浮点数),例如
-2.3, 99.1932 - 字符串,例如
"Mary", "President Obama"
以下是常用的数据结构:
- 有序列表
- 集合(只是一个无序的,唯一的列表)
- 列表的列表,例如包含行和列的表或矩阵
- 元组是不可变列表
- 字典,例如将学生姓名映射到他们的学生 ID;我们可以将其视为一个表,其中表中的每一行都将键与值相关联。
请记住,所有变量名实际上都是对内存位置的间接引用。 一切都是指向实现中数据的指针。 这意味着我们可以有两个引用相同内存位置的变量名,因此变量是别名。更改一个变量的元素似乎会更改其他变量元素。
现在我们知道内存中的数据是什么,让我们考虑一下计算模型。
2.3 计算模型
译者:飞龙
现在我们知道计算机如何在内存中存储数据,我们需要对计算机如何处理数据有基本的了解。 让我们探索一下计算机可以执行的最简单,细粒度的操作。 最终,我们将从这些操作中抽取设计模式。程序员考虑高级操作,例如映射,搜索或过滤,但是我们的手指键入与这些高级操作相关联的细粒度代码模式。
规范处理器操作
我们在在内存中表示数据中看到,在处理器处理数据时计算机的内存暂时保存数据。 我们通常将数据从磁盘加载到内存中,并将其组织成适合我们想要执行的计算的结构。 但最后,内存只保存数据。 所有操作都发生在计算机处理器(CPU)中,它执行五个主要操作:
- 将小块数据从内存加载到 CPU 中
- 对 CPU 中的数据执行算术计算
- 将小块数据存储回内存
- 跳转到新位置(这是我们的循环方式)
- 如果条件为真,则跳转到新位置
处理器执行低级机器指令,它执行一个或多个主要操作。 每条指令都做了很少的工作(比如相加两个数字),但处理器可以非常快速地完成它们,大约数十亿个每秒。在这些低级机器指令中编写程序将非常繁琐,因此我们通常使用 Python 等编程语言来简化我们的工作。
为了让您了解这些低级别的机器操作,请考虑以下简单赋值语句。
total = cost + tax
即使是这么简单的事情也需要处理器执行多个低级指令。处理器必须在内存中查看"cost"和"tax"的值,相加这两个值,然后将结果存储到内存中与"total"相关联的地址。
操作顺序
除非相反地给出,否则处理器一个接一个地执行指令。 例如,给定以下伪代码序列,处理器将执行赋值语句,然后执行打印。
cost = 100.0
tax = 8.1
total = cost + tax
print(total)
# 108.1
使用食谱烹饪时,我们熟悉按顺序执行一系列操作的概念。例如,食谱可能会指导我们:
- 将食材放入碗中
- 将成分混合在一起
- 倒入烤盘
- 以 375 度烘烤 40 分钟
我们自然地假设步骤按执行顺序给出。
条件执行
一些食谱给出了条件指令,例如
如果不够甜,加一些糖
类似地,处理器可以有条件地执行一个或一组操作。
x = -3
if x<0:
x = 0
print("was negative")
print(x)
'''
was negative
0
'''
仅当条件表达式为真时才执行条件运算。 为清楚起见,处理器不执行程序中存在的所有操作。
将现实问题映射到条件语句时,您的目标是识别以下关键元素:
- 条件表达式
- 如果它为真,所执行的操作
条件执行的模板如下所示:
if condition:
operation 1
operation 2
...
该条件必须可由计算机操作。例如,条件“猫饿了”不可由计算机操作,因为计算机无法在其内存中测试任何等同于猫饿了的内容。条件几乎总是由算术的相等或关系运算符组成,例如cost > 10,cost + tax < 100或quantity == 4。
是时候引入一种新的数据类型:布尔值,它包含true和false值。 任何相等或关系运算符的结果(值)都是布尔值。 例如,“3 > 2”的计算结果为true,3 > 4的计算结果为false。
在某些情况下,我们希望在任何一种情况下执行操作,如果条件为真,则执行一次操作,如果条件为假,则执行不同的操作。 例如,我们可以表达一个条件运算,来找到两个值x和y中的较大值,如下:
x = 99
y = 210
if x > y: max = x
else: max = y
print(max)
# 210
条件执行有点像执行零次或一次操作,具体取决于条件表达式。 在某些情况下,我们还需要多次执行操作。
重复执行
您将要做的大部分编程都涉及将操作应用于数据,这意味着逐个操作数据元素。这意味着我们需要能够重复指令,但我们很少使用这样的低级构造:while condition,做某些事情。例如,我们很少想要打印五次hello:
for i in range(5):
print("Hello")
'''
Hello
Hello
Hello
Hello
Hello
'''
(range(n) 从 0 到 n-1)
另一方面,在满足条件之前一直存在的通用循环可能很有用,例如“从网络读取数据,直到数据停止”。 或者,我们可以进行计算,例如以下对log 10的粗略近似:
n = 1000
log = 0
i = n
while i > 1:
log += 1
print(i)
i = i / 10
print(f"Log of {n} is {log}")
'''
1000
100.0
10.0
Log of 1000 is 3
'''
但是,大多数情况下,我们正在扫描数据,这意味着“对于每个循环”或“为循环而索引”。
For-each 循环
for-each循环遍历一系列元素(例如列表)但可以是任何可迭代对象,例如磁盘上的文件。 这是您将使用的最常见的循环。这是模版:
for x in iterable_object:
process x in some way
例如:
for name in ['parrt', 'mary', 'tombu']:
print(name.upper())
'''
PARRT
MARY
TOMBU
'''
我们也可以迭代更复杂的对象,例如下面的 numpy 矩阵:
import numpy as np
A = np.array([[19,11],
[21,15],
[103,18],
[99,13],
[8,2]])
for row in A:
print(row)
'''
[19 11]
[21 15]
[103 18]
[99 13]
[8 2]
'''
或者甚至是数据帧:
from pandas import DataFrame
df = DataFrame(data=[[99,'parrt'],[101,'sri'],[42,'kayla']],
columns=['ID','user'])
df
| ID | user |
|---|---|
| 0 | 99 |
| 1 | 101 |
| 2 | 42 |
for row in df.itertuples():
print(f"Info {row.ID:04d}: {row.user}")
'''
Info 0099: parrt
Info 0101: sri
Info 0042: kayla
'''
一个有用的 Python 魔术为我们提供迭代索引以及迭代值:
for i,row in enumerate(A):
print(f"{i+1}. {row}")
'''
1. [19 11]
2. [21 15]
3. [103 18]
4. [99 13]
5. [8 2]
'''
# same as the enumerate
i = 1
for row in A:
print(f"{i}. {row}")
i += 1
'''
1. [19 11]
2. [21 15]
3. [103 18]
4. [99 13]
5. [8 2]
'''
使用此代码模式,我们的目标是找到一个命名良好的迭代变量,识别条件,然后识别要重复的操作。
索引循环
执行索引循环有时很有用。 当我们必须同时遍历多个列表时,这些非常有用。 例如,如果我们有名称和他们的电话号码的列表,我们可能想要使用索引循环:
names = ['parrt', 'mary', 'tombu']
phones = ['5707', '1001', '3412']
for i in range(len(names)):
name = names[i]
phone = phones[i]
print(f"{name:>8}: {phone}")
'''
parrt: 5707
mary: 1001
tombu: 3412
'''
zip 循环
以下是大牛们不使用索引循环(使用foreach),做同样的事情的方式:
for name, phone in zip(names,phones):
print(f"{name:>8}: {phone}")
'''
parrt: 5707
mary: 1001
tombu: 3412
'''
for i, (name, phone) in enumerate(zip(names,phones)):
print(f"{i}. {name:>8}: {phone}")
'''
0. parrt: 5707
1. mary: 1001
2. tombu: 3412
'''
列表推导式
names = ['parrt', 'mary', 'tombu']
[name.upper() for name in names]
# ['PARRT', 'MARY', 'TOMBU']
[name for name in names] # make (shallow) copy of list
# ['parrt', 'mary', 'tombu']
[name for name in names if name.startswith('m')]
# ['mary']
[name.upper() for name in names if name.startswith('m')]
# ['MARY']
Quantity = [6, 49, 27, 30, 19, 21, 12, 22, 21]
[q*10 for q in Quantity]
# [60, 490, 270, 300, 190, 210, 120, 220, 210]
Quantity2 = [6, 49, 0, 30, -19, 21, 12, 22, 21]
[q*10 for q in Quantity2 if q>0]
# [60, 490, 300, 210, 120, 220, 210]
# Find indexes of all values <= 0 in Quantity2
[i for i,q in enumerate(Quantity2) if q<=0]
# [2, 4]
[f"{name}: {phone}" for name, phone in zip(names,phones)]
# ['parrt: 5707', 'mary: 1001', 'tombu: 3412']
转换公式
来自数学的 Sigma 符号以直接的方式转换为索引循环。例如:
我们从求和中选取元素并将它们插入模板中来获得索引循环。
练习
练习:给定一个包含十进制数字的字符串,例如s ="501",将该数字转换为整数并打印出来。 从期望的结果n反着做一遍。 回想一下501 = 5 * 100 + 0 * 10 + 1 * 1。 从最简单的 Python 结构开始,然后使用 Python 中的任何强大结构整理它。 提示:大牛最终会使用“霍纳法则”。如果字符串为空会发生什么? 你不能使用int('501')但你需要在单个数字上使用int('5')。
练习:重用该模式将包含二进制数字的字符串(例如s ="1101")转换为整数n,并将其打印出来。1101二进制是十进制的 13。 1101 是$1 * 2 ^ 3 + 1 * 2 ^ 2 + 0 * 2 ^ 1 + 1 * 2 ^ 0。
练习:给定两个列表,例如a = [9,3]和b = [1,4,10],创建并打印一个新列表c,包含来自输入列表的交替元素。 在这种情况下,输出将是“[9,1,3,4,10]”。首先假设相同数量的元素,然后尝试更一般的情况。 如果一个或两个列表都是空的,会发生什么?
练习:Python 有一个名为zip(a, b)的内置函数,它是一种方便的方法,可以获得元组列表,元组包含列表a和b中元素。 例如,如果a = [9,3]和b = [1,4,10],zip(a, b)给出一系列元组(9, 1), (3, 4)。 内置的zip在其中一个列表没有元素时停止,但是我们想用None填充缺少的元素:你应该得到输出列表c = [(9, 1), (3, 4), (None, 10)]。 在本练习中,我们使用上一个练习中的想法甚至代码本身来实现您自己的zip函数。 唯一的区别是你应该用None填充缺少的元素。
如果你卡住了,或者只是检查你的答案,你可以查看我的答案。
总结
除了与存储器的传输数据之外,处理器主要执行算术运算,例如cost + tax。 处理器还可以有条件地或重复地执行操作。
将现实问题映射到伪代码时,您将遵循程序或函数的工作计划,并最终从所需结果反向工作,来确定合适的操作序列。 这些操作将映射到我们的高级编程操作或这里描述的低级伪代码模式。
如果无法为某个问题确定更高级别的操作,请尝试将其映射到条件操作或一个或多个操作的循环。
对于条件,您必须弄清条件布尔表达式以及应有条件地执行的操作:
if condition:
operation 1
operation 2
...
如果您需要在条件失败时执行代码,请使用以下模板:
if condition:
operation 1
operation 2
...
else:
operation 1
operation 2
...
对于重复执行,我们有一个通用循环,在满足条件时执行一个或多个操作:
循环设置,通常是初始计数器或循环中更新的值。
while condition:
operation 1
operation 2
...
一个非常常见的循环版本遍历一个序列,例如一个列表,其中一个变量一次接受一个序列的每个值:
for x in sequence:
operate on x
在同时迭代多个列表时,我们使用索引列表的形式:
for i in some integer_set or range:
operation 1
operation 2
...
2.4 Python 中的编程模式
原文:Programming Patterns in Python
译者:飞龙
现在我们已经了解了计算机如何组织数据,并进行一些低级编程操作,现在让我们看一些常见的高级编程模式。 每一个这些操作都有一个使用条件和循环模式的实现,我们可以使用 python 语法很容易地表达。我们也可以使用现有的库函数来实现相同的功能,我们也将探索它们。
当我们进行时,你会发现程序设计和编程是一个单词关联的游戏。 高级操作(例如map)应该在您的脑海中触发伪代码(类似英语的“代码”)的循环模板,然后应该触发for-each Python 代码模板。
请记住,我们通过选择和应用操作来设计程序,而不是特定的代码序列。编程是设计过程中的最后一步,我们在这里获得可执行文档。因此,直观地思考如何操作数据列表,或从数据中提取信息。我经常把事情画出来或者把它们放到电子表格中,来帮助我想象。在纸上手动移动一些数据有助于我理解要执行的操作。
在设计了高级操作的序列或组合之后,我们可以用伪代码或直接使用 Python 语法来写出东西。随着您获得更多经验,从操作直接转到代码将更容易,但我们仍然在使用操作而不是代码来设计程序。对于复杂的问题,尽管有 35 年以上的编程经验,我仍然写出伪代码。
编程模式
选择程序的整体计划时,程序员从一组模式或模板中提取。个别行动本身也是如此。程序员有一个常见操作目录,他们在选择计划步骤时依赖他。
毫无疑问,您熟悉以下操作:
- 求和列表中的数字
- 计数列表中的数字
但我们可以将这些进一步抽象为:
- 查找满足条件的列表中的所有值
- 对列表中的每个元素应用操作
- ......
操作越抽象,它就越广泛适用。我们使用的操作类型部分取决于程序员的风格,但很大程度上受编程语言的能力及其预先存在的功能库的影响。
数据练习
在我们浏览这些材料时,我们将做几个小练习。 请将此 Python 代码剪切并粘贴到您正在使用的笔记本中。
请尝试在不查看练习描述正下方的解决方案的情况下进行练习。
UnitPrice = [38.94, 208.16, 8.69, 195.99]
Shipping = [35, 68.02, 2.99, 3.99, 5.94, 4.95, 7.72, 6.22]
names=['Xue', 'Mary', 'Bob']
Oscars = [
[1984, "A Soldier's Story", 0],
[1984, 'Places in the Heart', 0],
[1984, 'The Killing Fields', 0],
[1984, 'A Passage to India', 0],
[1984, 'Amadeus', 1],
[1985, "Prizzi's Honor", 0],
[1985, 'Kiss of the Spider Woman', 0],
[1985, 'Witness', 0],
[1985, 'The Color Purple', 0],
[1985, 'Out of Africa', 1]
]
Quantity = [6, 49, 27, 30, 19, 21, 12, 22, 21]
A = [
[1, 3, 10],
[4, -9, 0],
[2, 5, 8]
]
B = [
[7, 4, 0],
[4, 3, 1],
[1, 6, 8]
]
first=['Xue', 'Mary', 'Robert']
last=['Li', 'Smith', 'Dixon']
累积
让我们从一个非常简单但非常常见的模式开始,称为累积。该模式遍历一系列元素并累积一个值。例如,为了对序列中的数字求和,我们使用带有+运算符的累加器运算。当我们遍历序列时,我们更新一个初始化为 0 的流动总和:
在 Excel 中,这就像在单元格中使用sum(...)。在 Python 中,这种模式的实现有一个初始化步骤和一个循环:
sum = 0
for q in Quantity:
sum += q # same as: sum = sum + q
print(sum)
# 207
我们可以使用我们想要的任何其他算术运算符,例如*。实际上,我们可以使用任何带有两个操作数并返回新值的函数。 对于求和,函数的两个“输入”数字是先前的累计值和序列中的下一个值。该函数的结果是新的累计值。+和*是最常见的运算符。
您还将在 Hadoop 和 Spark 的分布式计算世界中,看到这个名为reduce(归约)的操作,就像map/reduce那样。
计数器是累加器的一种特殊情况,它计算序列中元素的数量。它还使用+运算符,但两个“输入”数字是先前的累计值和固定的值 1,而不是序列中的下一个元素。
我们可以更新多个流动的累计值,而不只是一个。例如,假设我们想要计算序列中偶数和奇数值的数量。我们需要两个累加器,从零开始,但操作是相同的:
+1表示每一步应用的“向累加值加 1”的操作,但仅在当前值为偶数或奇数时才应用。在 Python 代码中,我们将执行以下操作:
even = 0
odd = 0
for q in Quantity:
if q % 2 == 0: even += 1 # % is mod operator
else: odd += 1
print(even, odd)
# 4 5
映射
也许最常见的操作是,将一个序列映射到另一个序列,将运算符或函数应用于每个元素。(它就像一个累加器,它累积了一个项目列表,而不是一个单独的值。)例如,使用电子表格创建一个新列,包含 5% 折扣的单价,在电子表格中如下所示:
我们可以使用列表字面值,在时间 0 处表示两个列表:
UnitPrice = [38.94, 208.16, 8.69, 195.99]
Discounted = [] # empty list
换句话说:
从高级操作到伪代码到代码,我们头脑中的翻译过程是单词关联游戏。当你的大脑看到将一列数据映射到另一个数据的操作时,请考虑“映射”。当你的大脑听到“映射”,它应该生成适当的伪代码循环,适当地填充片段。当你的大脑听到“对于每个等等等等”时,请考虑“哦,for-each 循环”并使用适当的编程模式:
Discounted = [] # empty list
for price in UnitPrice:
Discounted.append(price * 0.95)
print(Discounted)
# [36.992999999999995, 197.75199999999998, 8.2555, 186.1905, 20.691, 6.308, 6.935, 40.62199999999999, 131.23299999999998]
请注意,我已将空列表的初始化包含在此代码段中。原因是我们真的想在心理上,将初始化与此代码模式相关联。
即使在微观层面,也要考虑将操作映射到代码。例如,当我想到“将x添加到列表y”时,我的大脑会将其转换为y.append(x)。
用于映射操作的列表推导式
这是一种常见的模式,Python 有一个显式结构,使事情变得更容易。它被称为列表推导式,它实际上只是简写,看起来更像数学集符号:
Discounted = [price * 0.95 for price in UnitPrice] # a list comprehension
print(Discounted)
# [36.992999999999995, 197.75199999999998, 8.2555, 186.1905, 20.691, 6.308, 6.935, 40.62199999999999, 131.23299999999998]
练习
在不查看我们刚才所做的代码的情况下,尝试使用列表推导来为映射操作编写代码,该列表推导将价格除以 2,再次将值放在Discounted中。不要剪切/粘贴。输入它!
Discounted = [price/2 for price in UnitPrice] # a list comprehension
print(Discounted)
# [19.47, 104.08, 4.345, 97.995]
练习
给定一个名称列表,['Xue', 'Mary', 'Robert'],用代码实现一个映射操作,将名称转换为namelens列表,它包含名称长度。提示:函数调用len('Xue')返回 3。不要剪切/粘贴。输入它!
names = ['Xue', 'Mary', 'Robert']
namelens = [len(name) for name in names]
print(namelens)
# [3, 4, 6]
组合
让我们看一下代码模式,一次遍历两个列表,将结果放在第三个列表中。例如,要计算销售交易的成本,我们将数量乘以单位价格。在电子表格中,如下所示:
将该公式拖到“成本”列中,将公式应用于以下行,从而填充新列。
以编程方式,我们正在做的是将两个不同的序列的第 i 个元素相乘,并将结果放在输出序列的第 i 个位置:
一开始,我们有以下数据:
Quantity = [6, 49, 27, 30, 19, 21, 12, 22, 21]
UnitPrice = [38.94, 208.16, 8.69, 195.99, 21.78, 6.64, 7.3, 42.76, 138.14]
遍历多个列表时,我们通常需要使用索引循环而不是 for-each 循环:
Cost = []
for i in range(len(Quantity)): # from 0 to length of Quantity-1, inclusively
Cost.append( Quantity[i] * UnitPrice[i] )
print(Cost)
# [233.64, 10199.84, 234.63, 5879.700000000001, 413.82000000000005, 139.44, 87.6, 940.7199999999999, 2900.9399999999996]
用于组合操作的列表推导式
或者,更好的是,使用列表推导式:
Cost = [Quantity[i] * UnitPrice[i] for i in range(len(Quantity))]
print(Cost)
# [233.64, 10199.84, 234.63, 5879.700000000001, 413.82000000000005, 139.44, 87.6, 940.7199999999999, 2900.9399999999996]
格式化技巧:
f"{3.14159:.2f}"
# '3.14'
[f"{c:.2f}" for c in Cost]
'''
['233.64',
'10199.84',
'234.63',
'5879.70',
'413.82',
'139.44',
'87.60',
'940.72',
'2900.94']
'''
[round(c,2) for c in Cost]
# [233.64, 10199.84, 234.63, 5879.7, 413.82, 139.44, 87.6, 940.72, 2900.94]
请注意,您可能想在列表推导中使用两个for循环,但是您可能获得数量的每个值和价格的每个值的叉乘。这不是我们想要的,正如你在这里看到的:
print( [q*p for q in Quantity for p in UnitPrice] ) # WRONG!
'''
[233.64, 1248.96, 52.14, 1175.94, 130.68, 39.839999999999996, 43.8, 256.56, 828.8399999999999, 1908.06, 10199.84, 425.81, 9603.51, 1067.22, 325.35999999999996, 357.7, 2095.24, 6768.86, 1051.3799999999999, 5620.32, 234.63, 5291.7300000000005, 588.0600000000001, 179.28, 197.1, 1154.52, 3729.7799999999997, 1168.1999999999998, 6244.8, 260.7, 5879.700000000001, 653.4000000000001, 199.2, 219.0, 1282.8, 4144.2, 739.8599999999999, 3955.04, 165.10999999999999, 3723.8100000000004, 413.82000000000005, 126.16, 138.7, 812.4399999999999, 2624.66, 817.74, 4371.36, 182.48999999999998, 4115.79, 457.38, 139.44, 153.29999999999998, 897.9599999999999, 2900.9399999999996, 467.28, 2497.92, 104.28, 2351.88, 261.36, 79.67999999999999, 87.6, 513.12, 1657.6799999999998, 856.68, 4579.5199999999995, 191.17999999999998, 4311.780000000001, 479.16, 146.07999999999998, 160.6, 940.7199999999999, 3039.08, 817.74, 4371.36, 182.48999999999998, 4115.79, 457.38, 139.44, 153.29999999999998, 897.9599999999999, 2900.9399999999996]
'''
zip函数
我们也可以使用zip,这是一个非常有用的函数,它从每个列表中提取一个值,以便在每次迭代时生成一个元组:
Cost = []
for q,u in zip(Quantity,UnitPrice):
Cost.append( q * u )
print(Cost)
# [233.64, 10199.84, 234.63, 5879.700000000001, 413.82000000000005, 139.44, 87.6, 940.7199999999999, 2900.9399999999996]
或者,使用列表推导式:
Cost = [q * u for q,u in zip(Quantity,UnitPrice)]
print(Cost)
# [233.64, 10199.84, 234.63, 5879.700000000001, 413.82000000000005, 139.44, 87.6, 940.7199999999999, 2900.9399999999996]
分割
组合的反面是分割,我们将流拆分为两个或更多个新的流。 例如,我经常需要将列表中的全名拆分为它们的名字和姓氏。 在电子表格中,我们创建一个空白列:
然后按空格字符拆分(在 Excel 中,使用Data > Text to Columns)获得两个新列:
我们可以使用组合操作与字符串连接运算符“撤消”此拆分,该操作符将名字和姓氏组合成一个包含全名的新流。
拆分的另一个常见用途是接受表示一串数字的字符串,并将其拆分为包含这些数字的列表:
'1 2 3'.split(' ')
# ['1', '2', '3']
'1 2 3'.split(' ')
# ['1', '2', '', '', '', '3']
values = '1 2 3'.split(' ')
[int(v) for v in values]
# [1, 2, 3]
我们甚至可以遍历列表来获得矩阵(列表的列表)
rows = [
'1 2 3',
'3 4 5'
]
matrix = [[int(v) for v in row.split(' ')] for row in rows]
matrix
# [[1, 2, 3], [3, 4, 5]]
from lolviz import *
objviz(matrix)
切片
到目前为止,我们检查过的大多数操作,都会产生与输入序列大小相同的列表或序列,但是有许多操作会产生数据的子集。第一个这样的操作是slice,它提取列表的子集。(同样,在这里我明确使用术语“列表”,来指示切片通常作用在适合内存的数据结构上。)
切片操作是两个值的函数,列表中的开始和结束位置。
names=['Xue', 'Mary', 'Bob']
print(f"Length {len(names)}")
print(names[0:1])
print(names[0:2])
print(names[0:3])
print(names[2:3])
'''
Length 3
['Xue']
['Xue', 'Mary']
['Xue', 'Mary', 'Bob']
['Bob']
'''
警告:大多数语言和库假设结束切片位置是排除的,这意味着在这种情况下切片从位置 0 到位置 3。 为了使事情变得更复杂,Python 而不是 R,从 0 开始计数而不是 1。在这方面很难在 Python 和 R 之间来回切换,因此最好在这里强调一下,以便记住它。
过滤
用于从列表或序列中提取数据的最常用操作称为过滤。 例如,使用 Excel 的过滤机制,我们可以对Shipping列过滤少于 10 美元的值:
过滤操作类似于映射操作,因为计算应用于输入流的每个元素。映射将一个函数应用于序列的每个元素,并创建一个相同大小的新序列。过滤用特定条件测试每个元素,如果为真,则将该元素添加到新序列。
过滤操作只是一个映射,有条件地将元素添加到目标列表:
Shipping = [35, 68.02, 2.99, 3.99, 5.94, 4.95, 7.72, 6.22]
Shipping2 = []
for x in Shipping:
if x < 10:
Shipping2.append(x)
print(Shipping2)
# [2.99, 3.99, 5.94, 4.95, 7.72, 6.22]
用于过滤操作的列表推导式
但是,我们应该使用推导式理解的变体,我们在映射操作模式中看到它:
Shipping2 = [x for x in Shipping if x < 10]
print(Shipping2)
# [2.99, 3.99, 5.94, 4.95, 7.72, 6.22]
我们也可以过滤一列,但将每行中的数据保持在一起。这是一个使用 Excel 的例子,它从被提名者列表中过滤奥斯卡获奖者(条件是winner等于 1):
Oscars = [
[1984, "A Soldier's Story", 0],
[1984, 'Places in the Heart', 0],
[1984, 'The Killing Fields', 0],
[1984, 'A Passage to India', 0],
[1984, 'Amadeus', 1],
[1985, "Prizzi's Honor", 0],
[1985, 'Kiss of the Spider Woman', 0],
[1985, 'Witness', 0],
[1985, 'The Color Purple', 0],
[1985, 'Out of Africa', 1]
]
objviz(Oscars)
循环结构的代码看起来像(直接跳到列表推导式形式):
Oscars2 = [movie for movie in Oscars if movie[2]==1]
print(Oscars2)
objviz(Oscars2)
# [[1984, 'Amadeus', 1], [1985, 'Out of Africa', 1]]
输出是列表的列表,每个电影为一行的过滤表。
注意我们如何测试movie [2]行中的一个列值,但是将整行添加到新表中。 如果我们只是将winner列值添加到新列表中,我们最终会得到一个列表:1, 1, 1, ..., 1。
练习
将Oscars列表中,所有具有 3 个单词的标题的电影,过滤为Oscars2。 要将字符串分成单词列表,请使用title.split('')。如果该列表的长度“len()”为3,则将整行复制到Oscars2。
Oscars2 = [movie for movie in Oscars if len(movie[1].split(' '))==3]
print(Oscars2)
# [[1984, "A Soldier's Story", 0], [1984, 'The Killing Fields', 0], [1985, 'The Color Purple', 0], [1985, 'Out of Africa', 1]]
练习
我们还可以在列表推导中使用表达式,而不仅仅是引用迭代值x。如果少于 10 美元,请使用列表推导的过滤变体,将Shipping中的运费加倍。将结果放在列表Shipping2中。
Shipping2 = [x*2 for x in Shipping if x < 10]
print(Shipping2)
# [5.98, 7.98, 11.88, 9.9, 15.44, 12.44]
练习
给出列表names=['Xue', 'Mary', 'Bob'],对于那些以X开头的名字,将列表过滤为names2。 回想一下name [i]在name中产生第i个字符。或者使用name.startswith(...)。
names=['Xue', 'Mary', 'Bob']
names2 = [name for name in names if name.startswith('X')]
print(names2)
# ['Xue']
搜索
过滤操作查找序列中满足特定条件的所有元素,但通常我们想知道哪个元素首先(或最后)满足条件。(或者,我们通常只需要知道是否存在特定元素。)这将我们带到搜索操作。最常见的是,搜索返回序列中的第一个(或最后一个)位置,而不是该位置的值。如果我们有位置,通常称为索引,我们总是可以请求序列中该位置的值。如果未找到该元素,则搜索返回无效索引-1。
在某种意义上,搜索是过滤的变体。不同之处在于,我们在找到条件为真的元素时所做的事情。搜索不再将该元素添加到目标列表中,而是退出循环。
first=['Xue', 'Mary', 'Robert'] # our given input
target = 'Mary' # searching for Mary
index = -1
for i in range(len(first)): # i is in range [0..n-1] or [0..n)
if first[i]==target:
index = i
break
print(index)
# 1
无论循环类型如何,break语句都会打破闭合的循环。
搜索操作甚至可以在字符串(字符列表)中使用,来找到感兴趣字符的位置。 例如,要将全名切割为名字和姓氏,我们可以将搜索空格字符与两个切片操作组合在一起。 给定全名Xue Li,搜索空格字符将返回第四个位置或索引 3。要提取第一个名称,我们将从索引 0 切片到索引 3,仅排除右侧。 为了获得姓氏,我们从索引 4 切换到 6,仅排除右侧。
这就是 python 的样子:
name = 'Xue Li'
# SEARCH
index = -1
for i in range(len(name)):
if name[i]==' ':
index = i
break
print(f"Index of space is {index}")
# SLICE
print(f"First: {name[0:index]}")
print(f"Last: {name[index+1:]}") # or name[index+1:len(name)]
'''
Index of space is 3
First: Xue
Last: Li
'''
为了确定字符串结尾的索引,程序员倾向于使用字符串的长度。长度可以是一个索引,其值是字符串末尾的后一个值,这是使用排除性右索引时,我们想要用于切片的。
嵌套循环
我们有时需要重复重复的指令,我们称之为嵌套循环。在分析领域,嵌套循环非常重要,因为我们使用嵌套循环来处理矩阵,图像和数据表。
处理矩阵
回想一下,虽然我们人类可以同时查看整个矩阵,但计算机会逐个检查每个元素。 看看这个 3x3 矩阵:
我们可以使用列表的列表在 python 中表示:
A = [
[1, 3, 10],
[4, -9, 0],
[2, 5, 8]
]
因为这不是一维数据结构,所以我们不能使用简单的“对于矩阵中的每个元素”循环来检查每个元素。 迭代nrows x ncols矩阵的所有元素的最常见模式如下所示:
for i in 0..nrows-1:
for j in 0..ncols-1:
do something with matrix[i,j]
其中matrix[i, j]访问行i和列j的元素。 这样的嵌套循环给出了i和j的所有可能组合,这是我们在矩阵上操作时所需要的。考虑以下该模板到 Python 的转换,打印出所有二维索引:
nrows = 3 # or len(A)
ncols = 3 # or len(A[0]) length of first row
for i in range(nrows):
for j in range(ncols):
print( i, j )
'''
0 0
0 1
0 2
1 0
1 1
1 2
2 0
2 1
2 2
'''
注意列j值如何比行i值变化得更快。我们可以通过改变循环次序来反转这种遍历顺序:
for j in range(ncols):
for i in range(nrows):
print(i, j)
'''
0 0
1 0
2 0
0 1
1 1
2 1
0 2
1 2
2 2
'''
j循环在外部,它的变化速度比内部i循环慢。
用于获得所有组合的双重for列表推导
列表推导中的双重循环给出了所有组合,在这种情况下是我们想要的。以下代码示例创建坐标的字符串表示的列表:
coords = [f"{i},{j}" for i in range(nrows) for j in range(ncols)]
print(coords)
# ['0,0', '0,1', '0,2', '1,0', '1,1', '1,2', '2,0', '2,1', '2,2']
现在,让我们使用累加器来求和 3x3 矩阵的所有元素,我们让nrows = 3和ncols = 3并使用加法运算:
sum = 0
for i in range(nrows):
for j in range(ncols):
sum = sum + A[i][j]
print(sum)
# 24
处理图像
在这个课程的图像项目中,我们为图像做了很多有趣的事情。 图像只不过是二维矩阵,其条目是从 0 到 255 的像素灰度值。像素值 0 是黑色而 255 是白色。 例如,如果我们放大图像,我们会看到二维矩阵的各个元素(称为像素):
图像和矩阵之间的唯一区别是,我们通常使用x和y坐标而不是行和列来访问图像的元素。访问图像的每个元素的嵌套循环的模板如下所示:
for x in 0..width-1:
for y in 0..height-1:
process pixel[x,y]
由于y坐标的变化快于x坐标,因此内部循环遍历垂直条形。外部循环将x移动到右边的下一个垂直条纹。
练习
作为一个更现实的例子,让我们将两个矩阵A和B相加为C。关键操作是将A[i,j]加上B[i,j]来获得C[i,j]。 在视觉上,它看起来像这样:
写出嵌套的 Python 索引循环,将两个矩阵相加,C = A + B。这里有一些定义可以帮助您入门:
nrows = ncols = 3
A = [
[1, 3, 10],
[4, -9, 0],
[2, 5, 8]
]
B = [
[7, 4, 0],
[4, 3, 1],
[1, 6, 8]
]
# Use list comprehension to init list of lists
C = [[0]*ncols for i in range(nrows)]
A = [
[1, 3, 10],
[4, -9, 0],
[2, 5, 8]
]
B = [
[7, 4, 0],
[4, 3, 1],
[1, 6, 8]
]
# Use list comprehension to init list of lists
C = [[0]*ncols for i in range(nrows)]
for i in range(nrows):
for j in range(ncols):
C[i][j] = A[i][j] + B[i][j]
print(C)
# [[8, 7, 10], [8, -6, 1], [3, 11, 16]]
import numpy as np
np.array(A) + np.array(B)
'''
array([[ 8, 7, 10],
[ 8, -6, 1],
[ 3, 11, 16]])
'''
警告:不要这样做:
D=[[0]*ncols]*nrows
objviz(D)
D[0][1] = 3
D
# [[0, 3, 0], [0, 3, 0], [0, 3, 0]]
# compare to this:
objviz([[0]*ncols for i in range(nrows)])
练习
我们也可以使用嵌套 for-each 循环,而不是使用索引循环(这非常适合于矩阵),像我们迄今为止所做的那样。 试试吧。
给出一个名字列表,first=['Xue', 'Mary', 'Robert']和姓氏列表,last=['Li', 'Smith', 'Dixon'],写 一个嵌套的 Python 循环,用于打印名字和姓氏的每个组合。
first=['Xue', 'Mary', 'Robert']
last=['Li', 'Smith', 'Dixon']
for f in first:
for l in last:
print(f+' '+l)
'''
Xue Li
Xue Smith
Xue Dixon
Mary Li
Mary Smith
Mary Dixon
Robert Li
Robert Smith
Robert Dixon
'''
练习
现在使用双重循环列表推导式重复该练习。
print([f+' '+l for f in first for l in last])
# ['Xue Li', 'Xue Smith', 'Xue Dixon', 'Mary Li', 'Mary Smith', 'Mary Dixon', 'Robert Li', 'Robert Smith', 'Robert Dixon']
2.5 数据别名
译者:飞龙
编程最棘手的事情之一是确切地确定变量所指的数据。 请记住,我们使用data和salary这样的名称来表示保存数据值的内存单元。 名称比物理内存地址更容易记住,但我们可能被愚弄。 例如,显然两个变量x和y都可以具有相同的整数值 7:
x = y = 7
print(x,y)
# 7 7
但是,你知道他们都指的是同一个 7 对象吗? 换句话说,Python 中的变量始终是引用或指向数据的指针,因此变量在技术上并不是持有值。 指针就像电话号码“指向”手机,但指针本身不是手机本身。
我们可以使用内置的id(x)函数来发现这个间接的秘密层次,该函数返回由x指向的物理内存地址。 为了证明这一点,让我们问一下x和y指向的是什么:
x = y = 7
print(id(x))
print(id(y))
'''
4468307488
4468307488
'''
哇! 他们是一样的。 该数字表示 Python 存储共享对象 7 的内存位置。
当然,作为程序员,我们并不认为这些原子元素指的是同一个对象;请记住他们这样做。 我们更有可能将它们视为相同数字的副本,因为lolviz在视觉上显示:
from lolviz import *
callviz(varnames=['x','y'])
让我们验证字符串是否发生了同样的事情:
name = 'parrt'
userid = name # userid now points at the same memory as name
print(id(name))
print(id(userid))
'''
4506178760
4506178760
'''
好的,很好,所以我们实际上共享相同的内存地址来保存字符串'parrt',并且两个变量名都指向同一个共享空间。我们在语言实现中称之为别名。
当我们开始更改共享数据时,事情才会变得怪异。整数和字符串不会发生这种情况,因为它们是不可变的(无法更改)。让我们看一个列表的两个相同副本:
you = [1,3,5]
me = [1,3,5]
print(id(you))
print(id(me))
callviz(varnames=['you','me'])
'''
4508962504
4508962440
'''
这些列表具有相同的值,但存在不同的内存地址。他们不是别名;它们不是共享的。因此,更改一个不会改变另一个:
you = [1,3,5]
me = [1,3,5]
print(you, me)
you[0] = 99
print(you, me)
'''
[1, 3, 5] [1, 3, 5]
[99, 3, 5] [1, 3, 5]
'''
另一方面,让我们看看如果我们让you和me共享相同的列表副本(指向相同的内存位置)会发生什么:
you = [1,3,5]
me = you
print(id(you))
print(id(me))
print(you, me)
callviz(varnames=['you','me'])
'''
4509139464
4509139464
[1, 3, 5] [1, 3, 5]
'''
现在,更改一个似乎改变另一个,但实际上两者都刚好引用内存中的相同位置:
you[0] = 99
print(you, me)
callviz(varnames=['you','me'])
# [99, 3, 5] [99, 3, 5]
不要混淆“更改列表元素”和“更改指向列表的指针”:
you = [1,3,5]
me = you
callviz(varnames=['you','me'])
me = [9,7,5] # doesn't affect `you` at all
print(you)
print(me)
callviz(varnames=['you','me'])
'''
[1, 3, 5]
[9, 7, 5]
'''
当我们将列表或其他数据结构传递给函数时,这种数据别名大量存在。 将Quantity列表传递给其参数名为data的函数,意味着这两个是别名。 我们将在使用函数组织代码的“符号可见性”部分中,更详细地查看这个现象。
浅复制
X = [[1,2],[3,4]]
Y = X.copy() # shallow copy
callviz(varnames=['X','Y'])
X[0][1] = 99
callviz(varnames=['X','Y'])
print(Y)
# [[1, 99], [3, 4]]
2.6 使用函数组织你的代码
原文:Organizing your code with functions
译者:飞龙
几年前,我学会了从头开始制作泰国红咖喱酱,包括烘烤,然后在巨大的研钵和杵中研磨和捣碎种子。这需要很长时间,所以我一般都会从商店购买现成的咖喱酱。
类似地,大多数烹饪书提供了许多其他食谱所使用的基本食谱,例如制作调味汁。烹饪书被组织成一系列可执行的食谱,其中一些“调用”其他食谱。 为了做晚餐,我打开一本食谱,获取一些原料,然后按照特定的顺序执行一个或多个食谱。
编写程序的方式也是一样的。打开烹饪书与导入库相同。获取原材料可能意味着将数据加载到内存中。主程序调用函数(食谱)来完成特定任务。作为编写程序的一部分,我们通常会将代码的逻辑部分分解为特定于我们问题的函数,而库中的函数则倾向于广泛适用。
我们组织代码的方式很重要。 如果我们对风格和组织不严格,程序很快就会成为难以理解的老鼠窝。 以下是我们将编写的 Python 程序的一般结构:
- 导入任何库
- 定义任何常量,简单的数据值
- 定义任何函数
- 主程序体
作为子程序的函数
分组为单个命名实体的一系列操作称为函数。函数就像迷你程序或子程序,我们可以像完整的程序一样规划。
Python 程序由零个或多个函数和所谓的“主”程序组成,它由让事情开始的一系列操作组成。
函数不从磁盘加载数据,而是操作由调用程序提供给它们的数据。 这个输入数据类似于食谱的成分列表,并以一个或多个命名形式参数(或者称为参数)的形式指定。函数不像程序那样打印结果或显示图形,而是返回值。函数是指广泛有用的辅助例程。
我们通过确定以下内容开始规划函数:
1.描述性函数名称
2.它操作的值的类型(参数类型)
3.它返回的值的类型(返回类型)
4.函数做了什么和返回的值
如果我们无法确切地指定函数的内部和外部,那么就没有希望确定处理步骤,更不用说实现该函数了的 Python 代码了。
与程序的工作计划一样,我们手动写出一些示例函数调用,来展示输入数据和输出数据。
一旦我们完全理解了我们的目标,我们就会规划出函数所需的操作顺序,来计算所需的结果。在设计整个程序时,我们从返回值开始,向后工作,以相反的顺序确定操作。注意:操作应该纯粹是数据的函数,数据作为参数传递给它们 -- 函数应该完全不知道任何其他数据。(当我们实际将函数伪代码转换为 Python 时,会有更多相关内容。)
函数模板
Python 函数就像黑盒子,通常接受输入数据和并yield(或返回)值。每次调用函数都会触发与执行该函数的关联代码,并返回一个或多个结果值。例如,这是一个名为pi的函数,它不带参数,但每次调用时返回值3.14159:
def pi():
return 3.14159
没有参数的函数的代码模板是:
def funcname():
statement 1
statement 2
...
return expression
带有函数名称,与函数关联的语句以及描述返回值的表达式的空位。 没有返回值的函数没有return语句。
我们将语句与 Python 中的函数关联的方式是缩进。 所以return 3.14159是函数的一部分,因为它在函数头之后缩进。与def在同一列中的第一个语句是函数外的第一个语句。
def pi():
return 3.14159
print("this is not part of function")
# this is not part of function
除非我们直接调用该函数,否则 Python 解释器不会执行函数内部的代码。 Python 将函数定义视为:我们可以根据需要调用的“食谱”定义。
函数的定义与调用*函数不同。调用函数需要函数名和任何参数值。在这种情况下,我们没有任何参数,所以我们将函数调用为pi():
pi()
# 3.14159
pi
# <function __main__.pi()>
我们不需要print语句,因为我们在笔记本中执行,而不是在 Python 程序中执行。 如果这是在常规 Python 程序中,我们需要一个print语句:print(pi()),但当然这也适用于此。
每次调用该函数的结果都是3.14159。 函数返回一个值但是不打印任何东西。 例如,如果我们将结果赋给变量,则 Jupyter 笔记本或 Python 交互式 shell 不会打印任何内容:
x = pi()
我们总是将括号放在函数名旁边,从语法上区分函数和变量。即,pi是一个变量引用,但pi()是一个函数调用。
某些函数没有返回值,例如在窗口中显示图像的函数。它有改变显示的副作用但实际上没有返回值。如果函数没有返回值,则省略return语句。这是一个人为的副作用示例,不需要返回值:
def hi():
print('hi')
hi()
# hi
如果你试图使用缺少return的函数的值,Python 会给你所谓的None值。
x = hi()
print(x)
'''
hi
None
'''
当然,我们也可以返回字符串,而不仅仅是数字。 例如,这是一个名为hello的函数,它只返回字符串'hello':
def hello():
return "hello"
def parrt():
return "parrt", 5707
id, phone = parrt()
print(id, phone)
# parrt 5707
现在转到更有趣的案例,这里是带有一个参数的函数的模板:
def funcname(argname):
statement 1
statement 2
...
return expression
如果有两个参数,则函数头如下所示:
def funcname(argname1, argname2):
我们作为程序员的工作是,根据我们的函数工作计划,选择函数中的描述性函数名称,参数名称和语句。
调用带参数的函数看起来像funcname(expression),funcname(expression1, expression2)等等...参数的顺序很重要。Python 将第一个表达式匹配函数定义中给出的第一个参数名称。
让我们看一下 Python 中的编程模式中的一些代码片段,看看我们是否可以抽象一些有用的函数。
求和函数
在计算模型中,我们看到了将数学 Sigma 表示法转换为 python 的代码,因此用于汇总列表中的值的这段代码,应该是非常熟悉的:
Quantity = [6, 49, 27, 30, 19, 21, 12, 22, 21]
sum = 0
for q in Quantity:
sum = sum + q
sum
# 207
此操作是累积,并且存在相关的代码模板,您应该记住它。每当有人说累积,你应该考虑结果的初始化和更新部分结果的循环。
对值求和非常常见,因此我们将功能封装在一个函数中,来避免必须始终剪切和粘贴代码模板。我们的黑盒子与函数规划中的一些示例“输入-输出”对如下:
(列出这样的例子让我们意识到我们需要担心空列表。)
我们通过缩进它然后添加一个函数头来将求和功能分组到一个函数中:
def sum(data):
s = 0
for q in data:
s = s + q
return s # return accumulated value s to invoker (this is not a print statement!)
Quantity = [6, 49, 27, 30, 19, 21, 12, 22, 21]
sum(Quantity) # call sum with a specific list
sum(data=Quantity) # implicit assignment here
# 207
这个函数版本的主要好处是,现在我们有一些通用代码,我们可以通过简单调用sum来调用它们。函数的参数是要求和的数据列表,因此for循环引用它而不是特定的Quantity变量。(请注意,函数内部的变量现在是s而不是sum,来避免与函数名混淆。)
sum([1,2,3])
# 6
您可能想要构建一个直接引用Quantity全局列表而不是参数的函数:
# OMG, this is so horrible I find it difficult to type!
def sum():
s = 0
for q in Quantity:
s = s + q
return s
问题是这个函数现在只适用于一个列表,并且绝不是通用的。这违背了创建函数的目的,因为它不可重用。
由于真正的函数接受列表参数,我们可以将另一个列表传递给函数:
ages = [10, 21, 13]
print(sum(ages))
print(sum([1,3,5,7,9]))
print(sum([ ])) # Empty list
'''
44
25
0
'''
要学习的另一件事是,Python 允许我们在将参数传递给函数时命名参数:
sum(data=ages)
# 44
函数调用sum(Quantity)将数据传递给函数。该函数返回一个值,因此函数调用被认为是一个值,我们可以打印出来,如上所示。像任何值一样,我们可以将调用函数的结果赋给变量:
x = sum(Quantity) # call sum and save result in x
x
# 207
请记住,从函数返回值与打印不同,这是一种副作用。运行程序时,只有print语句将值输出到控制台。不要混淆执行程序与交互式 Python 控制台(或此笔记本),它会自动打印出我们键入的每个表达式的值。例如:
>>> 34
34
>>> 34+100
134
>>>
sum函数有一个参数,但具有两个参数的函数也很常见。
练习
编写一个名为neg的函数,它接受一个数字参数x并返回x的相反数。
def neg(x): return -x
练习
编写一个名为max的函数,接受 2 个数字参数,x和y,并返回两个参数的最大值。
def max(x,y): return x if x>y else y
#same as:
#if x>y: return x
#else: return y
# test it
print(max(10,99))
print(max(99,10))
'''
99
99
'''
请注意,一旦我们使用参数名称,顺序无关紧要:
print(max(x=10, y=99))
print(max(y=99, x=10))
'''
99
99
'''
练习
编写一个名为area的函数,它接受参数半径r,并返回具有该半径的圆的面积(pi * r ** 2)。 提示:回想一下math包有一个名为pi的变量。
import math
def area(r): return math.pi * r**2 # ** is the power operator
# test it
area(1), area(r=2)
# (3.141592653589793, 12.566370614359172)
练习
编写一个名为words的 Python 函数,它接受一个字符串doc,包含一个由单个空格字符分隔的单词序列,并返回一个小写单词列表。X Y z的参数应该返回一个值为['x', 'y', 'z']的列表。 提示:'HI'.lower()求值为'hi'。
def words(doc:str) -> list:
words = doc.split(' ')
return [w.lower() for w in words]
# OR
def words(doc):
doc = doc.lower()
return doc.split(' ')
# OR
def words(doc): return doc.lower().split(' ')
words('Terence Parr is the instructor of MSAN501')
# ['terence', 'parr', 'is', 'the', 'instructor', 'of', 'msan501']
搜索函数
我们已经看到了搜索列表元素的代码,但是特定元素和特定列表是硬编码。也就是说,代码只能使用特定的值,而不是通用的:
first=['Xue', 'Mary', 'Robert'] # our given input
target = 'Mary' # searching for Mary
index = -1
for i in range(len(first)): # i is in range [0..n-1] or [0..n)
if first[i]==target:
index = i
break
index
# 1
有一个我们可以调用的函数会很好,因为搜索是如此常见。首先,我们可以通过缩进和添加函数头来包装函数搜索相关的逻辑。 但是,我们还应该更改列表的名称,使其更通用,并使其成为参数(与搜索目标相同)。
def search(x, data):
index = -1
for i in range(len(data)): # i is in range [0..n-1] or [0..n)
if data[i]==x:
index = i
break
print(index)
first=['Xue', 'Mary', 'Robert']
search('Mary', first) # invoke search with 2 parameters
# 1
我们现在向函数传递两个参数:x是要查找的元素,data是要搜索的列表。 只要我们想要,我们就可以通过调用search来搜索元素列表:
search('Xue', first), search('Robert', first)
'''
0
2
(None, None)
'''
# It is a good idea to test the failure case
search('Jim', first)
# -1
事实证明,我们可以通过用return语句替换break语句来简化该函数。 虽然break语句突破了闭合的循环,但无论函数出现在何处,return语句都会从函数返回。 在当前版本中,如果我们找到该元素,break语句就会突破循环并强制处理器执行循环后的语句,即return语句。因为return语句接受表达式参数,所以我们不需要在单独的变量中跟踪索引。return语句强制处理器立即退出函数并返回指定的值。实际上,return首先突破循环,然后是函数。
这是大牛写这个函数的方式:
def search(x, data):
for i in range(len(data)): # i is in range [0..n-1] or [0..n)
if data[i]==x:
return i # found element, return the current index i
return -1 # failure case; we did not return from inside loop
print(search('Mary', first))
print(search('Xue', first))
print(search('foo', first))
'''
1
0
-1
'''
符号的可见性
在函数外部创建的变量是所谓的全局变量,因为它们存在于全局空间(或帧)中。例如,让我们重新访问sum累加器的非函数版本,其中我添加了lolviz库的调用,来在循环内显示三个全局变量:
from lolviz import *
Quantity = [6, 49, 27, 30, 19, 21, 12, 22, 21]
sum = 0
display(callviz(varnames=['Quantity','sum','q']))
for q in Quantity:
sum = sum + q
display(callviz(varnames=['Quantity','sum','q']))
sum
# 207
这里有三个(全局)变量:Quantity,sum和q。该程序使用所有这些来计算结果。
让我们看看使用累加器的函数版本的“调用栈”是什么样的。
reset -f
from lolviz import *
Quantity = [6, 49, 27, 30, 19, 21, 12, 22, 21]
def sum(data):
s = 0
display(callsviz(varnames=['Quantity','data','s']))
for q in data:
s = s + q
return s
sum(Quantity)
# 207
如您所见,sum函数有一个新的作用域,因为主程序调用了一个函数。该函数有一个名为data的参数和一个名为s的局部变量(我称之为callsviz函数)。请注意,Quantity和data变量都指向相同的共享内存位置! 只不过,名称在不同的上下文(作用域)中定义。这是我们在上一节中讨论过的数据别名。通过遍历data,sum函数实际上是遍历来自外部上下文的Quantity列表。
注意修改数据参数的函数
def badsum(data):
#data = data.copy() # must manually make copy to avoid side-effect
data[0] = 99
display(callsviz(varnames=['Quantity','data','s']))
s = 0
for q in data:
s = s + q
return s
Quantity = [6, 49, 27, 30, 19, 21, 12, 22, 21]
badsum(Quantity)
print(Quantity)
# [99, 49, 27, 30, 19, 21, 12, 22, 21]
当函数返回时,sum的帧消失,只留下全局帧。
def sum(data):
s = 0
for q in data:
s = s + q
return s
print(sum(Quantity))
callsviz(varnames=['Quantity','data','s'])
# 207
reset -f
from lolviz import *
def f(x):
q = 0
g(x)
print("back from g")
display(callsviz(varnames=['x','q','y','z']))
def g(y):
print(y)
display(callsviz(varnames=['x','q','y','z']))
z = 99
f(33)
print("back from f")
display(callsviz(varnames=['x','q','y','z']))
# 33
# back from g
# back from f
可见性规则
现在您已经考虑了上下文的概念,让我们根据上下文为变量的可见性建立一些规则:
- 主程序无法查看函数内的变量和参数;只是因为一个主程序可以调用一个函数,并不意味着它可以看到内部原理。 将函数视为带有参数和返回值的黑盒子。
- 函数在技术上可以看到全局变量,但不作为一般规则。将每个函数所需的全局变量作为参数传递。
后一条规则很好,因为违反规则通常意味着你做的事情是“错误的”。 例如,如果我们调整sum累积函数,来直接引用全局变量Quantity,我们得到:
Quantity = [6, 49, 27, 30, 19, 21, 12, 22, 21]
def sum(data): # parameter not used!
s = 0
for q in Quantity: # uh oh!
s = s + q
return s
问题是,现在,sum仅适用于该全局数据。它通常不是很有用。线索是该函数忽略了data参数。因此,从技术上讲,该函数可以查看全局数据,但这不是一个好主意。(违反此规则来更改全局变量,也是获取难以找到的细微错误的好方法。)
从技术上讲,我们需要看到全局符号(函数)
def f():
g()
def g():
print("hi mom!")
f()
# hi mom!
返回值与打印
仅仅把这个概念灌输给你......
学生最大的困惑之一是返回值和打印结果之间的差异。当我们将计划转换为 Python 代码时,我们会再次看到这一点,但重要的是要立即理解这些差异。
分析世界中的程序,通常从文件读取数据并执行输出,或将数据写入另一个文件。 换句话说,程序与程序之外的世界交互。 程序之外的世界通常是网络,磁盘或屏幕。 相反,我们编写的大多数函数都不会与外部世界交互。
函数计算并返回(返回值)给调用者。除非明确要求使用print语句,否则它们不会向用户打印任何内容。
def pi():
print(3.14159) # This is not a return statement!
print(pi())
'''
3.14159
None
'''
2.7 如何阅读代码
译者:飞龙
从根本上说,程序员与代码交流。我们不仅向计算机,也向其他开发人员表达了我们的想法。到目前为止,我们专注于设计程序和编写 Python 代码。这是关键的创作过程,但是,为了编写代码,程序员必须能够阅读其他人编写的代码。
为什么阅读代码
我们阅读代码以便:
- 获得新体验。就像在自然语言中,我们通过倾听他人来学习说话一样,我们通过识别他人代码中的酷炫模式来学习编程技巧。能够快速阅读代码,使您可以获得观看编程讲座或视频的经验。
- 查找并修改代码段。我们经常可以通过试用 Google 搜索或 StackOverlow找到的代码段,找到编码问题的提示或解决方案。 请注意,您不违反版权法,如果是学生项目,则不违反学术诚信规则。
- 发现库函数或其他共享代码的行为。 从名称或参数列表中并不总是清楚库函数的完整行为。 查看该函数的源代码是了解它的作用的最佳方法。代码就是文档。
- 在我们的代码或其他代码中发现错误。 所有代码都有错误,特别是我们刚刚编写,但没有经过详尽测试的代码。作为编码过程的一部分,我们不断跳来跳去,阅读我们现有的代码库,来确保一切都组合在一起。
在我们讨论库函数时,让我强调一条黄金法则:你永远不应该向你的程序员询问参数的细节和库函数的返回值。你可以通过 PyCharm 中的“跳转到定义”或网络搜索来自己发现它。
本文档的目的是解释程序员如何读取代码。 我们的第一个线索来自于我们不是计算机这一事实,因此,我们不应该像计算机一样阅读代码,一个接一个地检查一个符号。 相反,我们将寻找关键元素和代码模式。
这就是我们用外语阅读句子时所做的事情。 例如,我的法语非常糟糕,因此,在阅读法语句子时,我必须有意识地询问谁在对谁做什么。在实践中,这意味着识别主语,动词和宾语。从这些关键要素中,我试图想象作者心中的思维模式。基本上我试图反转作者所遵循的过程。
在编程世界中,过程如下:代码作者可能会想到“*通过除以 2 *将价格转换为新列表”,然后将它们转换为“映射”的伪代码,最后转换为 Python for循环。在阅读循环代码时,我们的工作是反转过程,并想象作者的原始目标。 我们不是试图通过在我们的头脑或纸上模拟它,来弄清楚代码的突现行为;相反,我们正在寻找模式,它们能够告诉我们正在执行哪些高级操作。
这就是为什么在编写代码时应该强调清晰度,以便读者阅读更多内容。约翰 F. 伍兹 有一个很好的引言,总结了很多东西:
写代码的时候总是想象,维护你代码的家伙是一个知道你住在哪里的暴力精神病患者。
获得程序的要点
在第一次查看教科书时,扫描目录来获得书籍内容的整体视图,是有意义的。 第一次看节目时也是如此。 查看所有文件以及这些文件中包含的函数的名称。 同时,找出主程序的位置。 根据您在程序中的目标,您可能会开始单步执行主程序或立即跳转到感兴趣的函数。
从样例运行或单元测试中查看程序的输入 - 输出对也很有用,因为它可以帮助您了解程序的功能。 从某种意义上说,我们通过检查和测试程序,对程序的工作计划进行逆向工程。 以前,我们在前进方向使用程序的工作计划来设计程序。
获得函数的要点
一旦我们确定了要检查的主程序或函数,就应该对函数的工作计划进行反向工程。 函数的名称可能是函数功能的最大线索,假设代码作者是一个不错的程序员。 (使用像f这样的通用函数名称,是教师在不泄露答案的情况下,编写代码阅读问题的方式。)例如,毫无疑问,以下函数的目标是什么:
def average(...):
...
即使不查看参数或函数语句。
程序员通常会提供函数用法的注释,但要小心。 程序员通常会在不更改注释的情况下更改代码,因此注释会产生误导。可接受的注释可能如下所示:
def average(...):
"Compute and return the average of a list of numbers"
...
如果我们幸运的话,该注释对应于工作计划中的函数目标描述。
下一步是确定参数和返回值。 同样,参数的名称经常告诉我们很多,但不幸的是,Python 通常没有明确的参数类型(它们不会被 Python 检查)所以我们必须自己解决这个问题。 了解值和变量的类型对于理解程序至关重要。 在这样的简单函数中,我们通常可以快速找出参数的类型和返回值。 在其他情况下,我们将不得不深入研究函数的语句来解决这个问题(稍后会详细介绍)。 让我们放大来查看我们函数的更多细节:
def average(data):
...
return sum / n
在这一点上,我们知道data几乎肯定是一个数字列表,函数返回一个数字。 这意味着我们可以填写该功能的工作计划的第一部分。
在函数代码中寻找什么
因为我们事先知道平均值是什么,所以我们可以填写函数目标的工作计划描述。 但是,一般来说,我们必须扫描函数的语句才能弄明白。 (我们可能会很幸运并找到合理的函数注释。)现在让我们看一下完整的函数:
def average(data):
n = len(data)
sum = 0.0
for x in data:
sum = sum + x
return sum / n
缺乏经验的程序员必须单独和逐字地检查函数的语句,模拟计算机来找出突现行为。 相比之下,经验丰富的程序员在代码中寻找模式,代表映射,搜索,过滤等高级操作的实现.....
通过类比,考虑在游戏过程中记住棋盘的状态。 初学者必须单独记住所有东西在哪儿,而国际象棋大师则认为棋盘只是布达佩斯开局的变种。
我们如何知道从哪里开始以及看什么? 那么,让我们回想一下我们的通用数据科学程序模板:
- 获取数据,这意味着找到合适的文件或从 Web 收集数据并存储在文件中
- 从磁盘加载数据并放入组织成数据结构的内存中
- 规范,清理或以其他方式准备数据
- 处理数据,这可能意味着训练机器学习模型,计算汇总统计量或优化成本函数
- 输入结果,可以是任何东西,从简单地打印答案,到将数据保存到磁盘,以及生成花哨的可视化
该过程的要点是,将数据加载到方便的数据结构中并对其进行处理。加载数据,创建数据结构和处理数据结构有什么共同之处?它们都重复执行一组操作,这意味着处理数据的程序的要点是循环。(甚至有一本着名的书名为算法+数据结构=程序,其中算法表示伪代码或代码描述的过程。)没有循环的程序可能会非常无聊,因为它无法遍历数据结构或处理数据文件。
从这里,我们可以得出结论,所有的动作都发生在循环中,所以我们应该首先在代码中寻找循环**。阅读代码是在函数代码中找到这样的模板的问题,它立即告诉我们作者想要的操作或模式的类型。
识别代码中的编程模式
让我们深入研究一些循环示例,尝试识别高级模式和相应的操作。 要寻找的关键要素是我们研究的模板中的空位。 这通常意味着识别循环变量,循环边界,我们正在遍历的数据结构以及对数据元素执行的操作。目标是对代码作者的意图进行逆向工程。
练习:首先,上面的sum函数中的代码模式的对应操作是什么?
sum = 0.0
for x in data:
sum = sum + x
那是一个累积器。
练习:让我们看一个循环,我故意使用蹩脚的变量名称,所以你必须专注于功能。
foo = []
for blah in blort:
foo.append(blah * 2)
这是一个映射操作,我们可以从空目标列表的初始化和foo.append(...)调用中看到。 除了目标列表是blah的函数,它来自源列表blort之外,blah * 2与寻找模式无关。
练习:你在下面的代码中看到了什么样的循环(for-each,索引,嵌套等等)? 代码执行什么样的高级操作?
blort = []
for boo in range(len(foo)):
blort.append(foo[boo] * 2)
这是一个索引循环,它再次执行映射操作。 它是一个索引循环的线索是,边界是range(len(foo)),它给出一系列索引。 由于blort.append和foo[boo]的引用,我们知道它是一个映射操作。 因为[boo]索引运算符,我们知道foo是某种类型的列表。
练习:对应此代码中模式的高级操作是什么:
foo = []
for i in range(len(X)):
foo.append(X[i]+Y[i])
它将两列(列表)组合成目标列/列表foo。我们知道X和Y是列表,因为[i]数组索引。
练习:此代码执行什么高级数学运算?
for i in range(n):
for j in range(n):
C[i][j] = A[i][j] + B[i][j]
矩阵加法。这里重要的是要认识到,嵌套的索引循环给出了在[0..n]范围内的循环变量i和j的所有组合。 执行此操作的最常见原因之一是迭代矩阵或图像的元素。 这里的答案也可能是图像加法。
练习:这个循环打印了多少个hi?
for i in range(n):
for j in range(n):
print('hi')
n * n。内循环n次。外循环意味着我们执行整个内循环n次。
练习:
blort = []
for foo in A:
for bar in B:
blort.append(foo + bar)
这从A和B的所有可能组合中找到所有情况。
练习:这段代码在做什么? 即,循环完成后,blort的值是多少?
blort = float('-inf')
for x in X:
if x > blort:
blort = x
print(blort)
X的最大值。
无论何时在循环内部看到if语句,请考虑过滤或搜索或条件累积。 它通常是其中一个的变体。这假设条件表达式是直接或间接的循环变量的函数。
练习:这个变体打印了什么?
blort = float('-inf')
for i in range(len(X)):
if X[i] > blort:
blort = X[i]
print blort
完全一样的东西; blort是X的最大值。 您会看到一个条件表达式,它是循环内部循环变量的函数。这只是前一次的重组。
练习:这段代码的目标是什么? 即,循环后它为foo打印的值是多少?
foo = -1
bar = -99999
for i in range(len(X)):
if X[i] > bar:
bar = x
foo = i
print(foo)
X的最大值索引(argmax)。 我们知道与条件相关的代码,是从前面的例子中找出最大值,但它也跟踪了索引i。
可以把它想象成你已经想到的标准模式,但这种变体可以做一些额外的事情。 然后问两者之间有什么区别。
这是尝试理解输入 - 输出对是什么的一个很好的例子(虽然我们在这里谈论的是代码段而不是完整的函数)。 在最大值的计算中,输出是取自X的值。 在这种情况下,打印出的值是0..len(X)-1中的索引。
练习:描述此代码完成后bar的值。
foo = []
bar = []
for blah in blort:
foo.append(blah * 2)
for zoo in foo:
if zoo>10:
bar.append(zoo)
这里有很多东西,但它实际上只不过是一个序列中的两个模式。 第一个模式是一个映射操作,它将blort中的值加倍,来创建foo列表,该列表由第二个循环使用。第二个循环只是一个过滤,它将所有> 10的值从foo提取到bar。
练习:执行此代码后,a和b是什么值?
a = 0
b = 0
for x in X:
if x < 10:
a = a + 1
else:
b = b + 1
这是一个具有条件的双重累积。 它是一个累积,因为它在循环中更新至少一个变量。 它有一个累积条件,因为它是一个累积循环中的条件,其中条件表达式测试循环迭代器的值。 a是X中小于 10 的值的数量,b是大于或等于 10 的值的数量。
练习:循环后Y是什么值?
a = 2
b = 5
Y = []
for i in range(len(X)):
if i>=a and i<=b:
Y.append(X[i])
此循环实现切片操作,从列表中提取元素的子集。 在这种情况下,它选择范围a..b中的X的元素,包含边界,并将它们添加到Y。
该实现效率非常低,因为它遍历整个列表来获取范围内的元素。 如果我们将循环的边界更改为所需范围,它会更快更容易理解:
a = 2
b = 5
Y = []
for i in range(a, b+1): # range is [a,b]
Y.append(X[i])
总结
编写代码是成为程序员的重要部分。代码是程序员的沟通方式。 这是我们有效地使用额外的库,调试,以及获得经验的方式。
阅读代码的技巧是翻转从函数工作计划到伪代码再到 Python 代码的编程过程。 最大的线索来自变量和函数名称,可能还有代码注释。 然后,我们查找代码中所表达的代码模板,根据该模板的选择,对作者的原始意图进行反向工程。 例如,询问代码代表映射还是搜索操作。 不要试图模仿计算机,并使用表达式值和时间的图表来猜测突现行为。 有时你必须这样做才能进行调试,但总的来说,你的目标是猜测代码作者的意图。
因为阅读代码是您流程的重要组成部分,所以通过编写高质量的代码,善待其他开发人员和您未来的自已。 这包括选择优秀的变量和函数名称以及编写清楚说明您意图的代码。
2.8 面向对象编程
原文:Object-oriented programming
译者:飞龙
大揭秘
到目前为止,我们一直在使用函数和函数包,以及定义我们自己的函数。 但事实证明,我们一直在使用对象,我们只是没有认识到它们。 例如,
x = 'Hi'
x.lower()
# 'hi'
字符串x是我们可以发送消息的对象。
print( type(x) )
# <class 'str'>
甚至整数都是对象:
print(dir(99))
# ['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']
类是对象的蓝图,基本上是类型的名称,在这种情况下是str。 对象称为类的实例。
在x.lower()中,我们将lower消息发送到x字符串对象。 消息实际上只是与类/对象相关的函数。
x.lower
# <function str.lower>
在不支持对象学习编程的语言中,我们会做类似的事情:
lower(x)
Python 有函数和对象重编程,这就是为什么有x.lower()和:
len(x)
# 2
函数或“消息”的选择取决于库设计者,但是lower仅对字符串有意义,因此将它与str的定义组合起来是有意义的。
然而,在实现方面,x.lower()实际上实现为str.lower(x)其中str是字符串的类定义。 电脑处理器了解函数调用; 他们不理解对象,所以我们在 Python 解释器本身中执行了这个翻译。
包 VS 对象成员
让我们直截了当。点.运算符在 Python 中重载,表示包成员和对象成员访问。你已经熟悉了这个:
import numpy as np
np.array([1,2,3])
# array([1, 2, 3])
import math
math.log(3000)
# 8.006367567650246
阅读代码时,这是一个常见的混淆点。 当我们看到a.f()时,我们不知道函数f是由a标识的包的成员,还是由a引用的对象的成员。
在wordsim项目中,你定义了一个名为wordsim.py的文件,然后我的test_wordsim.py文件执行from wordsim import *来导入wordsim.py中的所有函数。
练习
在下文中,将标识符(单词)标识为包或函数或字段:
np.log(3)np.linalg.norm(v)from sklearn.ensemble import RandomForestRegressorpd.read_csv("foo.csv")pd.read_csv'hi'.lower()'hi'.lowerdf_train.columnsnp.piimg = img.convert("L")
现在,确定子表达式的数据类型,并将标识符(单词)标识为包或函数或字段:
df["saledate"].dt.yeardf_train.isnull().any().head(60)
字段 VS 方法
对象具有函数,我们将其称为方法,以将它们与不与对象关联的函数区分开来。 对象也有变量,我们称之为字段或实例变量。
字段是对象的状态。 方法是对象的行为。
我们一直在使用字段,例如df.columns,它获取数据帧中的列名的列表。
import datetime
now = datetime.date.today()
print( type(now) )
print( now.year ) # access field year
print( now.month )
'''
<class 'datetime.date'>
2018
8
'''
如果尝试在没有括号的情况下访问对象函数,则表达式将计算为函数对象本身而不是调用它:
s='hi'
s.title
# <function str.title>
简单的对象定义
类是多个对象的蓝图,通常称为实例。 类*封装了对象的状态和行为。
想象一下外星人在你家后院的土地上,并要求你描述一辆汽车。 您可能会描述其属性,例如轮子的数量及其功能,例如可以启动和停止。 这些是状态和行为。 通过定义它们,我们有效地定义了对象。 类名仅仅为实体命名。
按照惯例,类名称应该像“Point”一样大写。
作为对象替代品的元组
对象的字段是我们想要关联在一起的数据项。 例如,如果我想跟踪书名/作者,我可以使用元组列表:
from lolviz import *
books = [
('Gridlinked', 'Neal Asher'),
('Startide Rising', 'David Brin')
]
objviz(books)
for b in books:
print(f"{b[1]}: {b[0]}")
'''
Neal Asher: Gridlinked
David Brin: Startide Rising
'''
# Or, more fancy
for title, author in books:
print(f"{author}: {title}")
'''
Neal Asher: Gridlinked
David Brin: Startide Rising
'''
为了在两种情况下访问元组的元素,我们必须跟踪我们头脑中的顺序。 换句话说,我们必须访问元组元素,就像它们是列表元素一样。
形式对象
更好的方法是正式声明,作者和标题数据元素应该封装到称为书籍的单个实体中。我认为 Python 的规范非常古怪,但它非常灵活。 例如,我们可以定义一个没有方法没有字段的对象,但是可以使用赋值语句动态添加字段:
class Book:
pass
b = Book()
print(b)
b.title = 'Gridlinked'
b.author = 'Neal Asher'
print(b.title, b.author)
objviz(b)
'''
<__main__.Book object at 0x115c51fd0>
Gridlinked Neal Asher
'''
但这并不能让我们定义与该对象相关的方法(很容易)。让我们看看我们的第一个真正的类定义,它包含一个名为构造器的函数。
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
self.chapters = []
构造器通常根据参数设置初始和默认字段值。
在对象中定义的所有方法,函数必须有一个名为self的显式第一个参数。 这是正在考虑的对象。
然后我们可以使用实例创建语法Book(..., ...)来创建一列Book类的书籍对象或实例:
books = [
Book('Gridlinked', 'Neal Asher'),
Book(title='David Brin', author='Startide Rising')
]
objviz(books)
for b in books:
print(f"{b.author}: {b.title}") # access fields
'''
Neal Asher: Gridlinked
Startide Rising: David Brin
'''
请注意,我们不会将self参数传递给构造函数。 它在调用一侧隐藏,但在定义一侧出现!
顽皮的行为
还要注意我们一直在使用构造函数设置对象的字段,Python 以其无限的灵活性允许你做非常顽皮的事情,比如在任意对象上设置字段:
class Foo:
pass # just says "empty"
x = Foo()
x.foo = 3
即使类本身没有定义foo,也不会出错!
您甚至可以动态添加方法。
定义方法
如果您尝试打印一本书,您将只看到类型信息和物理内存地址:
print(books[0])
# <__main__.Book object at 0x115c51eb8>
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
def __str__(self): # called when conversion to string needed like print
return f"Book({self.title}, {self.author})"
def __repr__(self): # called in interactive mode
return self.__str__() # call the string
books = [
Book('Gridlinked', 'Neal Asher'),
Book('Startide Rising', 'David Brin')
]
print(books[0]) # calls __str__()
books[0] # calls __repr__()
'''
Book(Gridlinked, Neal Asher)
Book(Gridlinked, Neal Asher)
'''
确保使用self.x来引用字段x,否则你在方法中创建一个局部变量:
class Foo:
def __init__(self):
self.x = 0
def foo(self):
x = 3 # WARNING: does not alter the field! should be self.x
让我们创建另一种设置销售图书数量的方法。
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
self.sold = 0 # set default
def sell(self, n):
self.sold += n
def __str__(self): # called when conversion to string needed like print
return f"Book({self.title}, {self.author}, sold={self.sold})"
def __repr__(self): # called in interactive mode
return self.__str__() # call the string
b = Book('Gridlinked', 'Neal Asher')
print(b)
b.sell(100) # Book.sell(b, 100)
print(b)
'''
Book(Gridlinked, Neal Asher, sold=0)
Book(Gridlinked, Neal Asher, sold=100)
'''
注意:在方法定义中,我们调用同一对象上的其他方法,使用self.foo(...)调用方法foo。
理解方法和函数的关键
b.sell(100)方法调用由 Python 解释器翻译并执行为函数调用Book.sell(b, 100)。 b变成参数self,所以sell()函数正在更新book b。
为什么我们更喜欢b.sell(100)而不是Book.sell(b, 100):我们不仅仅在函数,并且在对象之间来回传递消息。 我们说dog.bark(),不是bark(dog),或我们说ball.inflate(),而不是inflate(ball)。
练习
真实世界的对象包含......和......
软件对象的状态存储在......
软件对象的行为通过......公开
软件对象的蓝图称为...
练习
定义一个名为Point的类,它有一个构造函数,接受x,y坐标并使它们成为类的字段。
定义方法distance(q),它接受一个Point并返回从self到q的欧几里德距离。
使用这个来测试:
p = Point(3,4)
q = Point(5,6)
print(p.distance(q))
添加方法__str__,以便print(q)打印出类似(3, 4)的东西。
答案
import numpy as np
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def distance(self, other):
return np.sqrt( (self.x - other.x)**2 + (self.y - other.y)**2 )
def __str__(self):
return f"({self.x},{self.y})"
p = Point(3,4)
q = Point(5,6)
print(p, q)
print(p.distance(q))
'''
(3,4) (5,6)
2.8284271247461903
'''
继承
定义一些与我们已经理解的东西相关的新东西,通常要容易得多。 在编程中也是如此。 让我们从一个帐户对象开始:
class Account:
def __init__(self, starting):
self.balance = starting
def add(self, value):
self.balance += value
def total(self):
return self.balance
a = Account(100.0)
a.add(15)
a.total()
# 115.0
objviz(a)
继承的行为类似于import,或从另一个类导入操作到新类。 (请注意,这不是真的,但我们可以将其视为包含,对于我们目的。)
如果我们不指定超类,则类object是隐式超类。 该类称为类层次结构的根,并定义了许多标准方法:
x = object() # yes, we can make a generic object
print(dir(x))
# ['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
我们可以定义一个有息账户,因为它与普通账户不同:
class InterestingAccount(Account): # derive from super class to get subclass
def __init__(self, starting, rate):
self.balance = starting # super().__init__(starting)
self.rate = rate
def total(self): # OVERRIDE method
return self.balance + self.balance * self.rate
b = InterestingAccount(100.0, 0.15)
b.add(15)
b.total()
objviz(b)
要点是我们可以使用add()而不必在InterestingAccount中重新定义它,而InterestingAccount也可以覆盖帐户的total()。 我们已经复用和覆盖以前的功能。您可以将超类视为定义一些初始函数,我们可以在子类中重用或覆盖他们。
我们还可以通过添加不在超类中的方法来扩展功能。
class InterestingAccount(Account): # derive from super class to get subclass
def __init__(self, starting, rate):
super().__init__(starting) # does self.balance = starting above
self.rate = rate
def total(self): # OVERRIDE method
return self.balance + self.balance * self.rate
def profit(self):
return self.balance * self.rate
b = InterestingAccount(100.0, 0.15)
b.add(15)
b.profit()
# 17.25
a = Account(100.0)
b = InterestingAccount(100.0, 0.15)
print(type(a))
print(type(b))
'''
<class '__main__.Account'>
<class '__main__.InterestingAccount'>
'''
类的定义实际上本身是对象,您可以使用任何对象的秘密字段访问它们:
print(b.__class__)
print(b.__class__.__base__)
'''
<class '__main__.InterestingAccount'>
<class '__main__.Account'>
'''
练习
- 什么是类?
- 类和实例之间有什么区别?
- 使用不带参数的构造函数定义类
Foo的新实例。 - 访问对象字段的语法是什么?
- 方法与函数有何不同?
__init__方法有什么作用?- 给定类
Employee和Manager,哪个是子类或者超类? - 子类中的方法可以调用超类中定义的方法吗?
- 如何覆盖从超类继承的方法?
动态调度(高级)
当你调用b.add(15)时,Python 在b(InterestingAccount)的对象定义中查找函数add。 因为我们从超类继承了该方法,所以子类知道它。 当我们调用b.total()时,Python再次查找InterestingAccount中的方法并找到一个重写方法。 这就是为什么b.total()不会调用Account版本。
这种行为是可取的,但起初非常混乱。 下面是一个实例,其中我添加了一个__str__方法给超类:
class Account:
def __init__(self, starting):
self.balance = starting
def add(self, value):
self.balance += value
def total(self):
return self.balance
def __str__(self):
return f"Balance {self.total()}" # can call 2 different functions
class InterestingAccount(Account): # derive from super class to get subclass
def __init__(self, starting, rate):
self.balance = starting
self.rate = rate
def total(self): # OVERRIDE method
return self.balance + self.balance * self.rate
def profit(self):
return self.balance * self.rate
微妙的部分是Account中的__str__调用Account.total()或InterestingAccount.total(),具体取决于self的类型:
a = Account(100.0)
b = InterestingAccount(100.0, 0.15)
print(a) # calls Account.total()
print(b) # calls InterestingAccount.total()
'''
Balance 100.0
Balance 115.0
'''
练习
定义一个继承自Point的Point3D。
定义接受x, y, z值并设置字段的构造函数。 调用super().__init__(x, y)来调用超类的构造函数。
定义/覆盖distance(q),以便它处理 3D 字段值来返回距离。
使用这个来测试:
p = Point3D(3,4,9)
q = Point3D(5,6,10)
print(p.distance(q))
添加方法__str__,以便print(q)打印出类似(3, 4, 5)的东西。记住:
\(dist(x,y) = \sqrt{(x_1-y_1)^2 + (x_2-y_2)^2 + (x_3-y_3)^2)}\)
答案
import numpy as np
class Point3D(Point):
def __init__(self, x, y, z):
# reuse/refine super class constructor
super().__init__(x,y)
self.z = z
def distance(self, other):
return np.sqrt( (self.x - other.x)**2 +
(self.y - other.y)**2 +
(self.z - other.z)**2 )
def __str__(self):
return f"({self.x},{self.y},{self.z})"
p = Point3D(3,4,9)
q = Point3D(5,6,10)
print(p.distance(q))
# 3.0
理由和一般思想
因为猎人 - 收集者的思想将世界视为通过发送消息交互的对象集合,所以 OO 编程范例很好地映射到现实世界问题,我们试图通过计算机模拟它们。 此外,在使用我们的思维方式编程时,我们处于最佳状态。
通常,在编写软件时,我们会尝试将现实实体映射到编程结构中。 如果我们有了单词问题,名词通常会成为对象,而动词通常会成为这些对象中的方法。
因为我们可以指定不同类型的对象如何相似,所以我们可以定义新对象,因为它们与现有对象不同。 通过按类别/共性/相似性正确地关联类似的类,作为继承的副作用,代码重用就出现了。
非 OO 语言是不灵活/脆弱的,因为必须指定确切的变量类型。在 OO 语言中,多态是使用单个类型引用,来引用相似但不同类型的分组的能力。
二、设计和构建程序
3.1 加载文件
译者:飞龙
本课程的目的是学习如何从笔记本电脑磁盘上的文件中提取数据。 我们将加载文本文件中的单词和数据文件中的数字。 在此过程中,我们将了解文件名和文件路径的更多信息。 我们的通用分析程序模板的前两个元素,表示获取数据然后将其加载到数据结构中:
- 获取数据,这意味着找到合适的文件,或从 Web 收集数据并存储在文件中
- 从磁盘加载数据并放入组织成数据结构的内存中
目前,我们只需手动从网上下载现成的数据文件,即可满足第一步。在 MSAN692 - 数据采集中,我们将学习如何以编程方式从 Web 提取数据。 本讲座重点介绍分析程序模板的第二步。
随着我们继续,我将反复要求你输入一些这样的例子。 了解从文件加载数据的代码模式至关重要。 请输入您的代码,不要剪切和粘贴。
什么是文件
正如我们之前所讨论的,磁盘和 RAM 都是内存的形式。 RAM 比磁盘快得多(但更小)但是当电源耗尽时 RAM 全部消失。 另一方面,磁盘是持久的。 文件只是由文件名标识的,磁盘上的一大块数据。 您一直使用文件。例如,我们可以双击文本文件或 Excel 文件,这将打开一个应用程序来显示这些文件。
我们需要能够编写从文件中读取数据的 Python 程序,就像 Excel 那样。 在 Python 程序中访问 RAM 中的数据非常容易,我们只需使用索引来引用列表中的各种元素,例如names [i]。 访问文件数据不太方便,因为我们必须首先将文件显式加载到工作内存中。 例如,我们可能希望将文件中的名称列表加载到names列表中。
如果文件太大而无法同时放入内存,我们必须以块的形式处理数据。现在,让我们假设所有文件都适合内存。
即便如此,访问文件也有点麻烦,因为我们必须明确地让 Python 打开一个文件,然后(通常)在我们完成后关闭它。 我们还必须区分对文件的读取和写入,这决定了我们打开文件的模式。 我们可以在读,写或追加模式下打开文件。 对于本实验,我们只关注“打开文件来读取”的默认情况。 以下是在读取模式(默认)下打开一个名为foo.txt的文件,然后立即关闭该文件的方式:
f = open('foo.txt') # open for read mode
f.close() # ok, we're done
嗯......从open()返回什么样的对象并存储在f中? 为什么我们要关闭文件?
文件描述符
当我们打开文件时,Python 为我们提供了一个“文件对象”,它实际上只是操作系统为我们提供的句柄或描述符。 它是一个唯一的标识符,以及操作系统识别我们使用的文件的方式。 文件对象不是文件名,也不是磁盘上的文件本身。它实际上只是一个描述符和文件的引用。
我们将使用文件名来使用open()获取文件对象,并使用文件对象来获取文件内容。
f = open("data/prices.txt") # or just "prices.txt"
print(type(f))
print(f)
f.close()
print(f.closed)
'''
<class '_io.TextIOWrapper'>
<_io.TextIOWrapper name='data/prices.txt' mode='r' encoding='UTF-8'>
True
'''
(将TextIOWrapper看作文件。)
关闭操作通知操作系统您不再需要该资源。 操作系统一次只能打开这么多文件,因此您应该在使用完毕后关闭文件。
稍后,当您学习将数据写入文件时,关闭操作也很重要。 关闭文件会刷新内存缓冲区中的,需要写入的所有数据。 从Python文档:
“编写程序的一个常见的错误是,你使用代码将所需的所有数据添加到文件中,但程序最终不会创建文件。通常这意味着你忘了关闭文件。”
为了避免混淆,请记住这个比喻。 您的房屋内容(文件)与您的地址(文件名)不同,并且与写有地址的文件(文件描述符)不同。进一步来说:
-
文件名是一个标识磁盘上文件的字符串。 它可以是完全限定的或相对于当前工作目录。
-
文件对象不是文件名,也不是磁盘上的文件本身。 它实际上只是一个描述符和文件的引用。
-
文件的内容不同于 Python 给我们的文件名和文件对象(描述符)。
Python WITH 语句
更新版本的 Python 提供了一种很好的机制来避免忘记文件关闭操作。 with语句更通用,但我们只是用它来自动关闭文件。 即使with语句中有强制程序终止的异常,关闭操作也会发生。 这是使用方式:
with open("data/prices.txt") as f:
contents = f.read()
print(type(contents))
print(contents[0:10])
print(f.closed)
'''
<class 'str'>
0.605
0.60
True
'''
文件名称和路径
你知道文件名是什么,因为之前你已经创建了很多文件。 (顺便说一下,另一个提醒是,不要在文件或目录名中使用空格。)路径是目录或文件的唯一限定符或定位符。一个完全限定的文件名给出了来自文件系统根目录的描述,用/分隔。文件系统的根在路径名的开头用/(正斜杠)标识。您可能习惯将其视为“Macintosh HD”,但从编程的角度来看,它只是/。在 Windows 上,我们不会在这里考虑,根目录包括盘符和反斜杠,如C:\。 这是一个有用的图表,显示了名为view.py的文件的完全限定路径名的成分:
作为简写,你可以用~开始一个路径,这意味着“我的主目录”。 在 Mac 上为/Users/parrt或您的任何用户 ID。 在 Linux 上,它可能是/home/parrt。
路径中的最后一个元素是文件名或目录。 例如,要引用上图中保存view.py的目录,请使用路径/Users/parrt/classes/msan501/images-parrt。或者,使用简写,完全限定的路径是~/classes/msan501/images-parrt。这是一个使用一些完全限定路径的 bash 会话示例:
$ ls /Users/parrt/classes/msan501/images-parrt/view.py
/Users/parrt/classes/msan501/images-parrt/view.py
$ cd /Users/parrt/classes/msan501/images-parrt
$ pwd
/Users/parrt/classes/msan501/images-parrt
$ cd ~/classes/msan501/images-parrt
$ pwd
/Users/parrt/classes/msan501/images-parrt
当前工作目录
所有程序都以当前工作目录的概念运行。 因此,如果一个程序在~/classes/msan501/images-parrt目录中运行,那么程序可以仅仅使用文件名,引用那个目录中的任何数据文件 - 不需要路径。例如,让我们使用ls程序来演示不同类型的路径。
$ cd ~/classes/msan501/images-parrt
$ ls
view.py
$ ls /Users/parrt/classes/msan501
images-parrt/
$ ls /Users/parrt/classes
msan501/
任何不以~或/开头的路径都称为相对路径名。为了完整起见,请注意..表示当前工作目录上级的目录:
$ cd ~/classes/msan501/images-parrt
$ ls ..
images-parrt/
$ ls ../..
msan501/
有时您会看到我使用/ tmp,这是一个临时目录或垃圾场。重新启动时,该目录中的所有文件通常都会被删除。
加载文本文件
正如我们在课程早期讨论的那样,文件只是一些位。这就是我们解释有意义的位的方式。 这些位可以代表图像,电影,文章,数据,Python 程序文本等等。 让我们将任何包含字符的文件成为文本文件,任何其他文件成为二进制文件。
文本文件通常是每个字符 1 个字节(8 位),并具有行的概念。 一行只是以\r\n(Windows)或\n(UNIX,Mac)终止的字符序列。 文本文件通常是一系列行。 下载此示例文本文件IntroIstanbul.txt,以便我们可以使用。 您可以将它保存在/tmp或您在课堂作业中使用的任何目录中。 出于本讨论的目的,我将数据文件放在此notes目录的名为data的子目录中。
该文件的前 10 行如下所示:
! head -10 data/IntroIstanbul.txt
'''
The City and ITS People
Istanbul is one of the worlds most venerable cities. Part
of the citys allure is its setting, where Europe faces Asia across
the winding turquoise waters of the Bosphorus, making it the only city
in the world to bridge two continents.
'''
你可以忽略前面的!,因为它只是告诉这个 Jupyter 笔记本运行后面的终端命令。 如果你想要的话,在这种情况下你可以把!想象成$终端提示符。
现在,让我们以原始方式而不是文本编辑器检查文件的内容。 od命令(八进制转储)对于查看文件的字节很有用。 使用选项-c将内容看作 1 字节字符:
! od -c data/IntroIstanbul.txt | head -5
'''
0000000 \n \n \n \n
0000020 \n T h e C
0000040 i t y a n d I T S P e o p
0000060 l e \n I s t a n
0000100 b u l i s o n e o f t h
'''
那个| head -5(竖线|看起来像一个管道)将od命令通过管道连接到head程序,它给出了前五行的输出。 当我们有很多输出时,我们也可以将输出传递给more程序来对长输出进行分页。
$ od -c data/IntroIstanbul.txt | more
...
你看到的\n字符代表我们知道的回车符。左边的数字是文件中的字符偏移量(看起来它们是八进制而不是十进制,顺便说一句;使用-A d来获取十进制地址)。
让我们看一些处理文本文件的常见编程模式。
模式:将所有文件内容加载到字符串中
操作序列是打开,加载,关闭。
with open('data/IntroIstanbul.txt') as f:
contents = f.read() # read all content of the file
print(contents[0:200]) # print just the first 200 characters
'''
The City and ITS People
Istanbul is one of the worlds most venerable cities. Part
of the citys allure is its setting, where Europe faces Asia across
'''
练习
不要剪切和粘贴,键入该序列并确保您可以从 Python 打印文件的内容。 使用您保存IntroIstanbul.txt的任何目录,而不是data。
模式:将文件的所有单词加载到列表中
这个模式只是前面我们split()空格字符来获取列表的扩展:
with open('data/IntroIstanbul.txt') as f:
contents = f.read() # read all content of the file
words = contents.split(' ')
print(words[0:100]) # print first 100 words
# ['\n', '', '\n', '', '\n', '', '', '', '\n', '', '', '', '', '', '\n', '', '', '', '', '', '', '', 'The', 'City', 'and', 'ITS', 'People\n', '', '', '', '', '', '', '', 'Istanbul', 'is', 'one', 'of', 'the', 'worlds', 'most', 'venerable', 'cities.', 'Part\n', '', '', '', '', '', '', '', 'of', 'the', 'citys', 'allure', 'is', 'its', 'setting,', 'where', 'Europe', 'faces', 'Asia', 'acr\xadoss\n', '', '', '', '', '', '', '', 'the', 'winding', 'turquoise', 'waters', 'of', 'the', 'Bosphorus,', 'making', 'it', 'the', 'only', 'city\n', '', '', '', '', '', '', '', 'in', 'the', 'world', 'to', 'bridge', 'two', 'continents.\n', '', '', '', '']
因为我们在空格字符上进行拆分,所以一行中的换行符和多个空格字符会产生无效的“单词”。我们需要在使用之前将该列表转换为新列表。
练习
使用过滤编程模式,过滤words中的大于 1 个字符的单词;放入另一个名为words2的列表中。提示,len(s)获取字符串s的长度。【答案】
练习
通过编写一个名为getwords的函数,将所有这些放在一起,该函数将filename作为参数并返回长于一个字符的单词列表。这是“将文件的所有单词加载到列表中”模式和上一个练习的组合。【答案】
加载文件的所有行
将文件内容读入字符串并不总是那么有用。我们通常希望处理我们单词,或文本文件的行,像刚才看到的那样。自然语言处理(NLP)将专注于使用单词,但让我们看一些数据文件,这些文件通常将文件结构化为数据行。每行代表观测,数据点或记录。
我们可以用\n来分割文本内容来获取行,但是 Python 为我们提供了这样的功能。 为了给我们一些数据,请下载price.txt,其中包含价格清单,每个价格一行。 这是另一种非常常见的编程模式:
模式:将文件的所有行读入列表。
序列是打开,读取行,关闭:
with open('data/prices.txt') as f:
prices = f.readlines() # get lines of file into a list
prices[0:10]
'''
['0.605\n',
'0.600\n',
'0.594\n',
'0.592\n',
'0.600\n',
'0.616\n',
'0.623\n',
'0.628\n',
'0.630\n',
'0.629\n']
'''
练习
不要剪切和粘贴,输入该代码并确保您可以将文件的行读入列表。
练习
在列表的每个元素上使用strip()函数,以便得到:['0.605', '0.600', '0.594', ...]。 使用列表推导式将价格映射到价格列表的新版本。【答案】
将字符串列表转换为 numpy 数组
数字末尾有\n字符,但这不是问题,因为我们可以使用 NumPy 轻松转换它:
import numpy as np
prices2 = np.array(prices, dtype=float) # convert to array of numbers
print(type(prices2))
print(prices2[0:10])
from lolviz import *
objviz(prices2)
'''
<class 'numpy.ndarray'>
[0.605 0.6 0.594 0.592 0.6 0.616 0.623 0.628 0.63 0.629]
'''
练习
将此转换添加到上一个练习中,并确保获得了array输出。 (我试图给你键入代码的重复经验,从文件中读取数据并以某种方式处理它。)【答案】
加载 CSV 文件
我们来看一个更复杂的数据文件。 下载heights.csv,其开头如下:
! head -4 data/player-heights.csv
'''
Football height, Basketball height
6.329999924, 6.079999924
6.5, 6.579999924
6.5, 6.25
'''
它仍然是一个文本文件,但现在我们开始认为文本文件可能遵循特定的格式。 在这种情况下,我们将其识别为逗号分隔值(CSV)文件。 它还有给列命名的标题行,这意味着我们需要区分对待第一行和文件其余部分。
模式:将 CSV 文件加载到列表的列表中
我们已经知道如何打开文件并获取行,所以让我们这样做,并将行分为标题和数据成分:
import numpy as np
with open('data/player-heights.csv') as f:
lines = f.readlines()
lines = [line.strip() for line in lines] # remove \n on end
lines[0:5]
'''
['Football height, Basketball height',
'6.329999924, 6.079999924',
'6.5, 6.579999924',
'6.5, 6.25',
'6.25, 6.579999924']
'''
header = lines[0]
data = lines[1:] # slice
# print it back out
print(header)
for d in data[0:5]:
print(d)
'''
Football height, Basketball height
6.329999924, 6.079999924
6.5, 6.579999924
6.5, 6.25
6.25, 6.579999924
6.5, 6.25
'''
练习
作为练习,输入该代码并确保打印出标题和前五行数据。
练习
数据的每一行都是一个包含两个数字的字符串。 我们需要使用split(',')将该字符串转换为带有两个浮点数的列表。 将所有两个元素的列表组合成一个整体列表,为我们提供了我们需要的二维表,这是我们的下一个练习。
编写一个名为getcsv(filename)的函数,它返回一个行列表的列表,其中第一行是标题行。 去掉行末尾的任何\n字符。 输出应如下所示:
[['6.329999924', ' 6.079999924'], ['6.5', ' 6.579999924'], ['6.5', ' 6.25']]
尽可能使用列表推导。【答案】
练习
import pandas as pd,然后将上一个练习中的数据转换为数据帧。 Pandas 不会自动理解第一行是标题,因此将切片data[1:]用作pd.DataFrame()数据帧构造函数的第一个参数,然后传递data[0]作为 columns参数。 打印出来,你应该看到类似的东西:
Football height Basketball height
0 6.329999924 6.079999924
1 6.5 6.579999924
2 6.5 6.25
...
使用 Pandas 加载 CSV 文件
当然,加载 CSV 是数据科学家需要一直做的事情,所以你可以在 Pandas 中使用一个简单的函数,你可能会变得非常熟悉:
import pandas as pd
prices = pd.read_csv('data/prices.txt', header=None)
prices.head(5)
| 0 | |
|---|---|
| 0 | 0.605 |
| 1 | 0.600 |
| 2 | 0.594 |
| 3 | 0.592 |
| 4 | 0.600 |
(header=None表示文件的第一行中没有列名。)
这甚至适用于带有标题行的 CSV 文件:
data = pd.read_csv('data/player-heights.csv')
data.head(5)
| Football height | Basketball height | |
|---|---|---|
| 0 | 6.33 | 6.08 |
| 1 | 6.50 | 6.58 |
| 2 | 6.50 | 6.25 |
| 3 | 6.25 | 6.58 |
| 4 | 6.50 |
我们将在数据帧中再次看到这些内容。
逐行处理文件
将文本行放入内存的先前机制很有效,只是它需要我们将所有内容一次性加载到内存中。这是非常低效的,并且将我们可以处理的数据的大小限制为我们拥有的内存量。
模式:多次地逐行读取数据
我们可以使用for循环,其中数据序列是文件描述符:
with open('data/prices.txt') as f:
for line in f: # for each line in the file
print(float(line)) # process the line in some way
n = 5
with open('data/prices.txt') as f:
for line in f: # for each line in the file
if n>0:
print(float(line)) # process the line in some way
n -= 1
'''
0.605
0.6
0.594
0.592
0.6
'''
练习
键入处理文件的行的新版本。不要剪切和粘贴!
练习
创建一个名为getsum的函数,它将文件名字符串作为参数,并返回该文件中行值之和。 不要使用readlines()函数。 在列表推导式中使用for line in f循环手动获取行的列表。 使用文件名data/prices.txt调用getsum并打印出价格总和。 使用sum(...)来求和列表推导创建的列表。
关闭文件后的操作
请记住,关闭文件后,您无法再从中读取数据:
f = open('data/prices.txt')
f.close()
f.read()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: I/O operation on closed file
总结
本课程的主要编程模式是:
- 模式:将所有文件内容加载到字符串中。
- 模式:将文件的所有单词加载到列表中。
- 模式:将文件的所有行读入列表。
- 模式:将数字列表加载到 numpy 数组中。
- 模式:将 CSV 文件加载到 2D numpy 数组中。
您应该能够快速,轻松地编写这些模式,而无需从 stackoverflow 中剪切和粘贴。
3.2 数据帧
译者:飞龙
我们将使用真实的 kaggle 比赛数据集来探索 Pandas 数据帧。获取rent.csv.zip文件并解压缩。
import pandas as pd
df = pd.read_csv("data/rent.csv", parse_dates=['created'])
df.head(2)
| bathrooms | bedrooms | building_id | created | description | display_address | features | interest_level | latitude | listing_id | longitude | manager_id | photos | price | street_address | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1.5 | 3 | 53a5b119ba8f7b61d4e010512e0dfc85 | 2016-06-24 07:54:24 | A Brand New 3 Bedroom 1.5 bath ApartmentEnjoy ... | Metropolitan Avenue | [] | medium | 40.7145 | 7211212 | -73.9425 | 5ba989232d0489da1b5f2c45f6688adc | ['https://photos.renthop.com/2/7211212_1ed4542... | 3000 | 792 Metropolitan Avenue |
| 1 | 1.0 | 2 | c5c8a357cba207596b04d1afd1e4f130 | 2016-06-12 12:19:27 | Columbus Avenue | ['Doorman', 'Elevator', 'Fitness Center', 'Cat... | low | 40.7947 | 7150865 | -73.9667 | 7533621a882f71e25173b27e3139d83d | ['https://photos.renthop.com/2/7150865_be3306c... | 5465 | 808 Columbus Avenue |
df.head(2).T
| 0 | 1 | |
|---|---|---|
| bathrooms | 1.5 | 1 |
| bedrooms | 3 | 2 |
| building_id | 53a5b119ba8f7b61d4e010512e0dfc85 | c5c8a357cba207596b04d1afd1e4f130 |
| created | 2016-06-24 07:54:24 | 2016-06-12 12:19:27 |
| description | A Brand New 3 Bedroom 1.5 bath ApartmentEnjoy ... | |
| display_address | Metropolitan Avenue | Columbus Avenue |
| features | [] | ['Doorman', 'Elevator', 'Fitness Center', 'Cat... |
| interest_level | medium | low |
| latitude | 40.7145 | 40.7947 |
| listing_id | 7211212 | 7150865 |
| longitude | -73.9425 | -73.9667 |
| manager_id | 5ba989232d0489da1b5f2c45f6688adc | 7533621a882f71e25173b27e3139d83d |
| photos | ['https://photos.renthop.com/2/7211212_1ed4542... | ['https://photos.renthop.com/2/7150865_be3306c... |
| price | 3000 | 5465 |
| street_address | 792 Metropolitan Avenue | 808 Columbus Avenue |
观察数据
df.info()
'''
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 49352 entries, 0 to 49351
Data columns (total 15 columns):
bathrooms 49352 non-null float64
bedrooms 49352 non-null int64
building_id 49352 non-null object
created 49352 non-null datetime64[ns]
description 47906 non-null object
display_address 49217 non-null object
features 49352 non-null object
interest_level 49352 non-null object
latitude 49352 non-null float64
listing_id 49352 non-null int64
longitude 49352 non-null float64
manager_id 49352 non-null object
photos 49352 non-null object
price 49352 non-null int64
street_address 49342 non-null object
dtypes: datetime64[ns](1), float64(3), int64(3), object(8)
memory usage: 5.6+ MB
'''
df.describe()
| bathrooms | bedrooms | latitude | listing_id | longitude | price | |
|---|---|---|---|---|---|---|
| count | 49352.00000 | 49352.000000 | 49352.000000 | 4.935200e+04 | 49352.000000 | 4.935200e+04 |
| mean | 1.21218 | 1.541640 | 40.741545 | 7.024055e+06 | -73.955716 | 3.830174e+03 |
| std | 0.50142 | 1.115018 | 0.638535 | 1.262746e+05 | 1.177912 | 2.206687e+04 |
| min | 0.00000 | 0.000000 | 0.000000 | 6.811957e+06 | -118.271000 | 4.300000e+01 |
| 25% | 1.00000 | 1.000000 | 40.728300 | 6.915888e+06 | -73.991700 | 2.500000e+03 |
| 50% | 1.00000 | 1.000000 | 40.751800 | 7.021070e+06 | -73.977900 | 3.150000e+03 |
| 75% | 1.00000 | 2.000000 | 40.774300 | 7.128733e+06 | -73.954800 | 4.100000e+03 |
| max | 10.00000 | 8.000000 | 44.883500 | 7.753784e+06 | 0.000000 | 4.490000e+06 |
df.price.sort_values(ascending=False).head(10)
'''
19558 4490000
9590 1150000
30689 1070000
29665 1070000
10581 135000
25538 111111
45674 100000
29082 90000
7336 85000
47995 80000
Name: price, dtype: int64
'''
获取列
df.price.head(5) # works only on right hand side of assignment or in expression
'''
0 3000
1 5465
2 2850
3 3275
4 3350
Name: price, dtype: int64
'''
df['price'].head(5)
'''
0 3000
1 5465
2 2850
3 3275
4 3350
Name: price, dtype: int64
'''
列的计算
可以获取值并取平均值:
prices = df['price']
avg_rent = prices.mean()
print(f"Average rent is ${avg_rent:.0f}")
# Average rent is $3830
df.latitude.min(), df.latitude.max()
# (0.0, 44.8835)
bybaths = df.groupby(['bathrooms']).mean()
bybaths
| bedrooms | latitude | listing_id | longitude | price | |
|---|---|---|---|---|---|
| bathrooms | |||||
| --- | --- | --- | --- | --- | --- |
| 0.0 | 0.849840 | 40.756168 | 7.027187e+06 | -73.970516 | 4001.530351 |
| 1.0 | 1.251662 | 40.740873 | 7.024082e+06 | -73.954589 | 3233.940490 |
| 1.5 | 2.271318 | 40.748909 | 7.026701e+06 | -73.965980 | 4257.066667 |
| 2.0 | 2.685117 | 40.750483 | 7.022952e+06 | -73.974603 | 5501.066971 |
| 2.5 | 2.851986 | 40.759694 | 7.018145e+06 | -73.968822 | 9028.920578 |
| 3.0 | 3.293960 | 40.764863 | 7.028051e+06 | -73.999571 | 10391.060403 |
| 3.5 | 3.542857 | 40.764344 | 7.061043e+06 | -73.964676 | 14534.785714 |
| 4.0 | 4.301887 | 40.513449 | 7.025477e+06 | -73.492230 | 16239.213836 |
| 4.5 | 4.137931 | 40.780462 | 7.058375e+06 | -73.900438 | 20868.000000 |
| 5.0 | 4.400000 | 38.723515 | 7.024429e+06 | -70.273665 | 27214.700000 |
| 5.5 | 4.400000 | 40.745340 | 6.975588e+06 | -73.975340 | 26300.000000 |
| 6.0 | 5.250000 | 40.762925 | 7.005858e+06 | -73.966400 | 50250.000000 |
| 6.5 | 7.000000 | 40.833600 | 7.198431e+06 | -74.216000 | 14500.000000 |
| 7.0 | 5.000000 | 40.733500 | 6.840943e+06 | -73.997400 | 60000.000000 |
| 10.0 | 2.000000 | 40.763300 | 6.849204e+06 | -73.984900 | 3600.000000 |
bybaths = bybaths.reset_index() # overcome quirk in Pandas
bybaths.head(3)
| bathrooms | bedrooms | latitude | listing_id | longitude | price | |
|---|---|---|---|---|---|---|
| 0 | 0.0 | 0.849840 | 40.756168 | 7.027187e+06 | -73.970516 | 4001.530351 |
| 1 | 1.0 | 1.251662 | 40.740873 | 7.024082e+06 | -73.954589 | 3233.940490 |
| 2 | 1.5 | 2.271318 | 40.748909 | 7.026701e+06 | -73.965980 | 4257.066667 |
bybaths[['bathrooms','price']] # print just num baths, avg price
| bathrooms | price | |
|---|---|---|
| 0 | 0.0 | 4001.530351 |
| 1 | 1.0 | 3233.940490 |
| 2 | 1.5 | 4257.066667 |
| 3 | 2.0 | 5501.066971 |
| 4 | 2.5 | 9028.920578 |
| 5 | 3.0 | 10391.060403 |
| 6 | 3.5 | 14534.785714 |
| 7 | 4.0 | 16239.213836 |
| 8 | 4.5 | 20868.000000 |
| 9 | 5.0 | 27214.700000 |
| 10 | 5.5 | 26300.000000 |
| 11 | 6.0 | 50250.000000 |
| 12 | 6.5 | 14500.000000 |
| 13 | 7.0 | 60000.000000 |
| 14 | 10.0 | 3600.000000 |
列 VS 子集
# get column
df['bedrooms'].head(5)
'''
0 3
1 2
2 1
3 1
4 4
Name: bedrooms, dtype: int64
'''
df[['bedrooms','bathrooms']].head(3)
| bedrooms | bathrooms | |
|---|---|---|
| 0 | 3 | 1.5 |
| 1 | 2 | 1.0 |
| 2 | 1 | 1.0 |
获取行
# get index 3 row
df.iloc[3] # same as df.iloc[3,:]
'''
bathrooms 1
bedrooms 1
building_id 28d9ad350afeaab8027513a3e52ac8d5
created 2016-04-18 02:22:02
description Building Amenities - Garage - Garden - fitness...
display_address East 49th Street
features ['Hardwood Floors', 'No Fee']
interest_level low
latitude 40.7539
listing_id 6888711
longitude -73.9677
manager_id 1067e078446a7897d2da493d2f741316
photos ['https://photos.renthop.com/2/6888711_6e660ce...
price 3275
street_address 333 East 49th Street
Name: 3, dtype: object
'''
df.iloc[0:2] # first two rows
| bathrooms | bedrooms | building_id | created | description | display_address | features | interest_level | latitude | listing_id | longitude | manager_id | photos | price | street_address | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1.5 | 3 | 53a5b119ba8f7b61d4e010512e0dfc85 | 2016-06-24 07:54:24 | A Brand New 3 Bedroom 1.5 bath ApartmentEnjoy ... | Metropolitan Avenue | [] | medium | 40.7145 | 7211212 | -73.9425 | 5ba989232d0489da1b5f2c45f6688adc | ['https://photos.renthop.com/2/7211212_1ed4542... | 3000 | 792 Metropolitan Avenue |
| 1 | 1.0 | 2 | c5c8a357cba207596b04d1afd1e4f130 | 2016-06-12 12:19:27 | Columbus Avenue | ['Doorman', 'Elevator', 'Fitness Center', 'Cat... | low | 40.7947 | 7150865 | -73.9667 | 7533621a882f71e25173b27e3139d83d | ['https://photos.renthop.com/2/7150865_be3306c... | 5465 | 808 Columbus Avenue |
df.iloc[0:2][['created','features']] # first two rows, show 2 columns
| created | features | |
|---|---|---|
| 0 | 2016-06-24 07:54:24 | [] |
| 1 | 2016-06-12 12:19:27 | ['Doorman', 'Elevator', 'Fitness Center', 'Cat... |
索引,通过索引的键获取行
df = df.set_index('listing_id')
df.head(3)
| bathrooms | bedrooms | building_id | created | description | display_address | features | interest_level | latitude | longitude | manager_id | photos | price | street_address | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| listing_id | ||||||||||||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 7211212 | 1.5 | 3 | 53a5b119ba8f7b61d4e010512e0dfc85 | 2016-06-24 07:54:24 | A Brand New 3 Bedroom 1.5 bath ApartmentEnjoy ... | Metropolitan Avenue | [] | medium | 40.7145 | -73.9425 | 5ba989232d0489da1b5f2c45f6688adc | ['https://photos.renthop.com/2/7211212_1ed4542... | 3000 | 792 Metropolitan Avenue |
| 7150865 | 1.0 | 2 | c5c8a357cba207596b04d1afd1e4f130 | 2016-06-12 12:19:27 | Columbus Avenue | ['Doorman', 'Elevator', 'Fitness Center', 'Cat... | low | 40.7947 | -73.9667 | 7533621a882f71e25173b27e3139d83d | ['https://photos.renthop.com/2/7150865_be3306c... | 5465 | 808 Columbus Avenue | |
| 6887163 | 1.0 | 1 | c3ba40552e2120b0acfc3cb5730bb2aa | 2016-04-17 03:26:41 | Top Top West Village location, beautiful Pre-w... | W 13 Street | ['Laundry In Building', 'Dishwasher', 'Hardwoo... | high | 40.7388 | -74.0018 | d9039c43983f6e564b1482b273bd7b01 | ['https://photos.renthop.com/2/6887163_de85c42... | 2850 | 241 W 13 Street |
df.loc[7150865]
'''
bathrooms 1
bedrooms 2
building_id c5c8a357cba207596b04d1afd1e4f130
created 2016-06-12 12:19:27
description
display_address Columbus Avenue
features ['Doorman', 'Elevator', 'Fitness Center', 'Cat...
interest_level low
latitude 40.7947
longitude -73.9667
manager_id 7533621a882f71e25173b27e3139d83d
photos ['https://photos.renthop.com/2/7150865_be3306c...
price 5465
street_address 808 Columbus Avenue
Name: 7150865, dtype: object
'''
df = df.reset_index()
df.head(3)
| listing_id | bathrooms | bedrooms | building_id | created | description | display_address | features | interest_level | latitude | longitude | manager_id | photos | price | street_address | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 7211212 | 1.5 | 3 | 53a5b119ba8f7b61d4e010512e0dfc85 | 2016-06-24 07:54:24 | A Brand New 3 Bedroom 1.5 bath ApartmentEnjoy ... | Metropolitan Avenue | [] | medium | 40.7145 | -73.9425 | 5ba989232d0489da1b5f2c45f6688adc | ['https://photos.renthop.com/2/7211212_1ed4542... | 3000 | 792 Metropolitan Avenue |
| 1 | 7150865 | 1.0 | 2 | c5c8a357cba207596b04d1afd1e4f130 | 2016-06-12 12:19:27 | Columbus Avenue | ['Doorman', 'Elevator', 'Fitness Center', 'Cat... | low | 40.7947 | -73.9667 | 7533621a882f71e25173b27e3139d83d | ['https://photos.renthop.com/2/7150865_be3306c... | 5465 | 808 Columbus Avenue | |
| 2 | 6887163 | 1.0 | 1 | c3ba40552e2120b0acfc3cb5730bb2aa | 2016-04-17 03:26:41 | Top Top West Village location, beautiful Pre-w... | W 13 Street | ['Laundry In Building', 'Dishwasher', 'Hardwoo... | high | 40.7388 | -74.0018 | d9039c43983f6e564b1482b273bd7b01 | ['https://photos.renthop.com/2/6887163_de85c42... | 2850 | 241 W 13 Street |
| Allows non-unique indexes |
df_beds = df.set_index('bedrooms')
df_beds.loc[3].head(3)
| listing_id | bathrooms | building_id | created | description | display_address | features | interest_level | latitude | longitude | manager_id | photos | price | street_address | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| bedrooms | ||||||||||||||
| 3 | 7211212 | 1.5 | 53a5b119ba8f7b61d4e010512e0dfc85 | 2016-06-24 07:54:24 | A Brand New 3 Bedroom 1.5 bath ApartmentEnjoy ... | Metropolitan Avenue | [] | medium | 40.7145 | -73.9425 | 5ba989232d0489da1b5f2c45f6688adc | ['https://photos.renthop.com/2/7211212_1ed4542... | 3000 | 792 Metropolitan Avenue |
| 3 | 6858062 | 1.0 | 205f95d4a78f1f3befda48b89edc9669 | 2016-04-12 02:39:45 | BEAUTIFUL 2 BEDROOM POSSIBLE CONVERSION INTO T... | Madison Avenue | ['Doorman', 'Elevator', 'Dishwasher', 'Hardwoo... | low | 40.7454 | -73.9845 | 3793e58c60343a3fd6846ca2d2ef3c7f | ['https://photos.renthop.com/2/6858062_5cfb9d9... | 4395 | 121 Madison Avenue |
| 3 | 6890563 | 1.0 | be6b7c3fdf3f63a2756306f4af7788a6 | 2016-04-18 04:46:30 | These pictures are from a similarlisting. | Thompson St | ['Washer/Dryer'] | low | 40.7231 | -74.0044 | 64249f81378907ae7cf65e8ccb4bd8dc | ['https://photos.renthop.com/2/6890563_1b98fae... | 3733 | 25 Thompson St |
检查缺失值
df.isnull().head(5)
| listing_id | bathrooms | bedrooms | building_id | created | description | display_address | features | interest_level | latitude | longitude | manager_id | photos | price | street_address | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False |
| 1 | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False |
| 2 | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False |
| 3 | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False |
| 4 | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False |
df.isnull().any()
'''
listing_id False
bathrooms False
bedrooms False
building_id False
created False
description True
display_address True
features False
interest_level False
latitude False
longitude False
manager_id False
photos False
price False
street_address True
dtype: bool
'''
查找数据帧中缺少描述的所有行:
df.description.isnull().head(5)
'''
0 False
1 False
2 False
3 False
4 False
Name: description, dtype: bool
'''
df[df.description.isnull()].head(5)
| listing_id | bathrooms | bedrooms | building_id | created | description | display_address | features | interest_level | latitude | longitude | manager_id | photos | price | street_address | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 57 | 6923973 | 1.0 | 0 | ca9c594aba4534beae6ce71def209994 | 2016-04-26 01:39:25 | NaN | East 74th Street | [] | low | 40.7699 | -73.9565 | b7de4cb395920136663132057fa89d84 | [] | 2700 | 315 East 74th Street |
| 67 | 6891766 | 1.0 | 1 | 5a922b81c28dd44b2ffadf603c90c0d5 | 2016-04-19 01:42:01 | NaN | West 50th Street | ['Doorman', 'Fitness Center', 'Pool', 'Elevato... | low | 40.7619 | -73.9854 | 62b685cc0d876c3a1a51d63a0d6a8082 | [] | 3697 | 250 West 50TH Street |
| 73 | 6890772 | 1.0 | 1 | bde4018a1c4f290eb89af695a7a1d644 | 2016-04-18 04:59:44 | NaN | East 85th Street | ['Elevator', 'Cats Allowed', 'Dogs Allowed', '... | low | 40.7777 | -73.9532 | d1737922fe92ccb0dc37ba85589e6415 | [] | 2945 | 225 East 85th Street |
| 174 | 6853469 | 1.0 | 1 | 0 | 2016-04-10 05:13:03 | NaN | 83rd Avenue | ['Doorman', 'Elevator', 'Cats Allowed', 'Dogs ... | low | 40.7111 | -73.8272 | e6472c7237327dd3903b3d6f6a94515a | ['https://photos.renthop.com/2/6853469_75548f7... | 1675 | 123-30 83rd Avenue |
| 210 | 6846567 | 1.0 | 1 | 6289dd7229f0d3b87254860764be70ab | 2016-04-09 01:39:12 | NaN | West 28th Street | ['Doorman', 'Fitness Center', 'Elevator', 'Cat... | low | 40.7512 | -74.0026 | 62b685cc0d876c3a1a51d63a0d6a8082 | [] | 4030 | 525 West 28th Street |
另一个查询,获得价格高于 1M 美金的所有apt行
(df.price>1000000).head(5)
'''
0 False
1 False
2 False
3 False
4 False
Name: price, dtype: bool
'''
df[df.price>1000000]
| listing_id | bathrooms | bedrooms | building_id | created | description | display_address | features | interest_level | latitude | longitude | manager_id | photos | price | street_address | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 9590 | 7208794 | 1.0 | 2 | 5d3525a5085445e7fcd64a53aac3cb0a | 2016-06-24 05:02:58 | NaN | West 116th Street | ['Doorman', 'Elevator', 'Cats Allowed', 'Dogs ... | low | 40.8011 | -73.9480 | d1737922fe92ccb0dc37ba85589e6415 | [] | 1150000 | 40 West 116th Street |
| 19558 | 7208764 | 1.0 | 2 | cd25bbea2af848ebe9821da820b725da | 2016-06-24 05:02:11 | NaN | Hudson Street | ['Doorman', 'Elevator', 'Cats Allowed', 'Dogs ... | low | 40.7299 | -74.0071 | d1737922fe92ccb0dc37ba85589e6415 | [] | 4490000 | 421 Hudson Street |
| 29665 | 7013217 | 1.0 | 1 | 37385c8a58176b529964083315c28e32 | 2016-05-14 05:21:28 | West 57th Street | ['Doorman', 'Cats Allowed', 'Dogs Allowed'] | low | 40.7676 | -73.9844 | 8f5a9c893f6d602f4953fcc0b8e6e9b4 | [] | 1070000 | 333 West 57th Street | |
| 30689 | 7036279 | 1.0 | 1 | 37385c8a58176b529964083315c28e32 | 2016-05-19 02:37:06 | This 1 Bedroom apartment is located on a prime... | West 57th Street | ['Doorman', 'Elevator', 'Pre-War', 'Dogs Allow... | low | 40.7676 | -73.9844 | 18133bc914e6faf6f8cc1bf29d66fc0d | ['https://photos.renthop.com/2/7036279_924b52f... | 1070000 | 333 West 57th Street |
df[(df.price>1000) & (df.price<10_000)].head(3) # parentheses are required in query!!!!
| listing_id | bathrooms | bedrooms | building_id | created | description | display_address | features | interest_level | latitude | longitude | manager_id | photos | price | street_address | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 7211212 | 1.5 | 3 | 53a5b119ba8f7b61d4e010512e0dfc85 | 2016-06-24 07:54:24 | A Brand New 3 Bedroom 1.5 bath ApartmentEnjoy ... | Metropolitan Avenue | [] | medium | 40.7145 | -73.9425 | 5ba989232d0489da1b5f2c45f6688adc | ['https://photos.renthop.com/2/7211212_1ed4542... | 3000 | 792 Metropolitan Avenue |
| 1 | 7150865 | 1.0 | 2 | c5c8a357cba207596b04d1afd1e4f130 | 2016-06-12 12:19:27 | Columbus Avenue | ['Doorman', 'Elevator', 'Fitness Center', 'Cat... | low | 40.7947 | -73.9667 | 7533621a882f71e25173b27e3139d83d | ['https://photos.renthop.com/2/7150865_be3306c... | 5465 | 808 Columbus Avenue | |
| 2 | 6887163 | 1.0 | 1 | c3ba40552e2120b0acfc3cb5730bb2aa | 2016-04-17 03:26:41 | Top Top West Village location, beautiful Pre-w... | W 13 Street | ['Laundry In Building', 'Dishwasher', 'Hardwoo... | high | 40.7388 | -74.0018 | d9039c43983f6e564b1482b273bd7b01 | ['https://photos.renthop.com/2/6887163_de85c42... | 2850 | 241 W 13 Street |
直方图变量
df.bathrooms.value_counts()
'''
1.0 39422
2.0 7660
3.0 745
1.5 645
0.0 313
2.5 277
4.0 159
3.5 70
4.5 29
5.0 20
5.5 5
6.0 4
6.5 1
10.0 1
7.0 1
Name: bathrooms, dtype: int64
'''
import matplotlib.pyplot as plt
plt.xlabel('Num Bathrooms')
plt.ylabel('Num Apts')
plt.hist(df.bathrooms, bins=20)
plt.show()
# <Figure size 640x480 with 1 Axes>
plt.xlabel('Num Bedrooms')
plt.ylabel('Num Apts')
plt.hist(df.bedrooms, bins=20)
plt.show()

plt.xlabel('Price')
plt.ylabel('Num Apts at that price')
plt.hist(df.price, bins=45) # not useful since loooong right tail
plt.show()

import numpy as np
df_log = df.copy()
df_log["price"] = np.log( df["price"] )
#OR:
#df_log["price"] = df["price"].apply(np.log)
df_log.price.head(3)
'''
0 8.006368
1 8.606119
2 7.955074
Name: price, dtype: float64
'''
plt.xlabel('Price')
plt.ylabel('Num Apts at that price')
plt.hist(df_log.price, bins=45) # not useful since loooong right tail
plt.show()

变量间的变化
bybaths.plot.line('bathrooms','price', style='-o')
plt.show()

# OR, can do directly
plt.plot(bybaths.bathrooms, bybaths.price, marker='o') # note slightly different arguments
plt.show()

测试你的知识
从df获取浴室一列
迭代列的列表并将其打印出来
从df获取第 6 行
将df索引设置为卧室,然后使用索引获得所有带 3 间卧室的公寓
获取每月价格> 100_000的所有行
从df中删除列building_id
获取价格介于 1000 和 2000 之间的所有行。
将np.log()函数应用于价格并存储在名为log_price的新列中
将纬度和经度列放入到自己的数据帧中
答案
df.bathrooms
for colname in df:
print(colname)
df.loc[5]
df = df.set_index('bedrooms')
df.loc[3]
df[df.price > 100_000]
#del df.building_id
df = df.drop('building_id', axis=1)
df[ (df.price>1000) & (df.price<2000) ]
df['log_price'] = np.log( df.price )
df[ ['longitude','latitude'] ]
清理
价格
df_clean = df[(df.price>1_000) & (df.price<10_000)]
plt.xlabel('Price')
plt.ylabel('Num Apts at that price')
plt.hist(df_clean.price, bins=45)
plt.show()

plt.scatter(df_clean.bedrooms, df_clean.price, alpha=0.1)
plt.xlabel("Bedrooms", fontsize=12)
plt.ylabel("Rent price", fontsize=12)
plt.show()

位置
df_missing = df_clean[(df_clean.longitude==0) |
(df_clean.latitude==0)]
len(df_missing)
# 11
# only 11 filter out
df_clean = df_clean[(df_clean.longitude!=0) |
(df_clean.latitude!=0)]
Use & to get "and" whereas | is "or".
Using GPS checker gives a rough outline for New York City of latitude, longitude 40.55, -74.1 on the lower left and 40.94, -73.67 on the upper right. Let's filter others out.
df_clean = df_clean[(df_clean['latitude']>40.55) &
(df_clean['latitude']<40.94) &
(df_clean['longitude']>-74.1) &
(df_clean['longitude']<-73.67)]
print(len(df_clean), len(df))
# 48300 49352
热力图
from matplotlib import colors
fig = plt.figure(figsize=(8,8))
ax = fig.add_subplot(111)
plt.scatter(df_clean.longitude,
df_clean.latitude,
alpha=0.8,
s=1.5, # pixel size
vmin=1000, vmax=4000, # limit price range so 4000 is red
c=df_clean['price'],
cmap='rainbow', # color map
marker='.')
plt.colorbar()
plt.show()

训练模型
仅仅获取数值字段
df_train = df_clean[['bathrooms', 'bedrooms', 'longitude', 'latitude', 'price']]
df_train.head(5)
| listing_id | bathrooms | bedrooms | building_id | created | description | display_address | features | interest_level | latitude | longitude | manager_id | photos | price | street_address | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 7211212 | 1.5 | 3 | 53a5b119ba8f7b61d4e010512e0dfc85 | 2016-06-24 07:54:24 | A Brand New 3 Bedroom 1.5 bath ApartmentEnjoy ... | Metropolitan Avenue | [] | medium | 40.7145 | -73.9425 | 5ba989232d0489da1b5f2c45f6688adc | ['https://photos.renthop.com/2/7211212_1ed4542... | 3000 | 792 Metropolitan Avenue |
| 1 | 7150865 | 1.0 | 2 | c5c8a357cba207596b04d1afd1e4f130 | 2016-06-12 12:19:27 | Columbus Avenue | ['Doorman', 'Elevator', 'Fitness Center', 'Cat... | low | 40.7947 | -73.9667 | 7533621a882f71e25173b27e3139d83d | ['https://photos.renthop.com/2/7150865_be3306c... | 5465 | 808 Columbus Avenue | |
| 2 | 6887163 | 1.0 | 1 | c3ba40552e2120b0acfc3cb5730bb2aa | 2016-04-17 03:26:41 | Top Top West Village location, beautiful Pre-w... | W 13 Street | ['Laundry In Building', 'Dishwasher', 'Hardwoo... | high | 40.7388 | -74.0018 | d9039c43983f6e564b1482b273bd7b01 | ['https://photos.renthop.com/2/6887163_de85c42... | 2850 | 241 W 13 Street |
X_train = df_train[['bedrooms','bathrooms','latitude','longitude']]
y_train = df_train['price']
from sklearn.ensemble import RandomForestRegressor
rf = RandomForestRegressor(n_estimators=100, n_jobs=-1, oob_score=True)
rf.fit(X_train, y_train)
print(f"OOB R^2 score is {rf.oob_score_:.3f} (range is -infinity to 1.0; 1.0 is perfect)")
# OOB R^2 score is 0.867 (range is -infinity to 1.0; 1.0 is perfect)
模型告诉我们特征的什么信息?
# pip install rfpimp
from rfpimp import *
I = oob_importances(rf, X_train, y_train)
I.plot(kind='barh', legend=False)
plt.show()

合成特征
df['one'] = 1
df.head(3)
| listing_id | bathrooms | bedrooms | building_id | created | description | display_address | features | interest_level | latitude | longitude | manager_id | photos | price | street_address | log_price | one | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 7211212 | 1.5 | 3 | 53a5b119ba8f7b61d4e010512e0dfc85 | 2016-06-24 07:54:24 | A Brand New 3 Bedroom 1.5 bath ApartmentEnjoy ... | Metropolitan Avenue | [] | medium | 40.7145 | -73.9425 | 5ba989232d0489da1b5f2c45f6688adc | ['https://photos.renthop.com/2/7211212_1ed4542... | 3000 | 792 Metropolitan Avenue | 8.006368 | 1 |
| 1 | 7150865 | 1.0 | 2 | c5c8a357cba207596b04d1afd1e4f130 | 2016-06-12 12:19:27 | Columbus Avenue | ['Doorman', 'Elevator', 'Fitness Center', 'Cat... | low | 40.7947 | -73.9667 | 7533621a882f71e25173b27e3139d83d | ['https://photos.renthop.com/2/7150865_be3306c... | 5465 | 808 Columbus Avenue | 8.606119 | 1 | |
| 2 | 6887163 | 1.0 | 1 | c3ba40552e2120b0acfc3cb5730bb2aa | 2016-04-17 03:26:41 | Top Top West Village location, beautiful Pre-w... | W 13 Street | ['Laundry In Building', 'Dishwasher', 'Hardwoo... | high | 40.7388 | -74.0018 | d9039c43983f6e564b1482b273bd7b01 | ['https://photos.renthop.com/2/6887163_de85c42... | 2850 | 241 W 13 Street | 7.955074 | 1 |
添加适当长度的随机列
df2 = df.copy()
df2['random'] = np.random.random(size=len(df))
df2.head(2).T
| 0 | 1 | |
|---|---|---|
| listing_id | 7211212 | 7150865 |
| bathrooms | 1.5 | 1 |
| bedrooms | 3 | 2 |
| building_id | 53a5b119ba8f7b61d4e010512e0dfc85 | c5c8a357cba207596b04d1afd1e4f130 |
| created | 2016-06-24 07:54:24 | 2016-06-12 12:19:27 |
| description | A Brand New 3 Bedroom 1.5 bath ApartmentEnjoy ... | |
| display_address | Metropolitan Avenue | Columbus Avenue |
| features | [] | ['Doorman', 'Elevator', 'Fitness Center', 'Cat... |
| interest_level | medium | low |
| latitude | 40.7145 | 40.7947 |
| longitude | -73.9425 | -73.9667 |
| manager_id | 5ba989232d0489da1b5f2c45f6688adc | 7533621a882f71e25173b27e3139d83d |
| photos | ['https://photos.renthop.com/2/7211212_1ed4542... | ['https://photos.renthop.com/2/7150865_be3306c... |
| price | 3000 | 5465 |
| street_address | 792 Metropolitan Avenue | 808 Columbus Avenue |
| log_price | 8.00637 | 8.60612 |
| one | 1 | 1 |
| random | 0.630259 | 0.124522 |
df2['i'] = [i for i in range(len(df))]
df2.head(2).T
| 0 | 1 | |
|---|---|---|
| listing_id | 7211212 | 7150865 |
| bathrooms | 1.5 | 1 |
| bedrooms | 3 | 2 |
| building_id | 53a5b119ba8f7b61d4e010512e0dfc85 | c5c8a357cba207596b04d1afd1e4f130 |
| created | 2016-06-24 07:54:24 | 2016-06-12 12:19:27 |
| description | A Brand New 3 Bedroom 1.5 bath ApartmentEnjoy ... | |
| display_address | Metropolitan Avenue | Columbus Avenue |
| features | [] | ['Doorman', 'Elevator', 'Fitness Center', 'Cat... |
| interest_level | medium | low |
| latitude | 40.7145 | 40.7947 |
| longitude | -73.9425 | -73.9667 |
| manager_id | 5ba989232d0489da1b5f2c45f6688adc | 7533621a882f71e25173b27e3139d83d |
| photos | ['https://photos.renthop.com/2/7211212_1ed4542... | ['https://photos.renthop.com/2/7150865_be3306c... |
| price | 3000 | 5465 |
| street_address | 792 Metropolitan Avenue | 808 Columbus Avenue |
| log_price | 8.00637 | 8.60612 |
| one | 1 | 1 |
| random | 0.630259 | 0.124522 |
| i | 0 | 1 |
数据集具有features属性(类型为字符串),其中包含公寓的特性列表。
df.features.head(5)
'''
0 []
1 ['Doorman', 'Elevator', 'Fitness Center', 'Cat...
2 ['Laundry In Building', 'Dishwasher', 'Hardwoo...
3 ['Hardwood Floors', 'No Fee']
4 ['Pre-War']
Name: features, dtype: object
'''
让我们创建三个新的布尔列,指示公寓是否有门卫,停车或洗衣房。 首先制作数据帧的副本,因为我们将对其进行修改(否则我们将收到错误“正在尝试在DataFrame的切片副本上设置值”):
df_aug = df[['bedrooms','bathrooms','latitude','longitude',
'features','price']].copy()
然后我们规范化特性列,以便缺少的特性值变为空白,并且我们将所有字符串小写。
# rewrite features column
df_aug['features'] = df_aug['features'].fillna('') # fill missing w/blanks
df_aug['features'] = df_aug['features'].str.lower() # normalize to lower case
通过检查features列中是否存在字符串来创建三个布尔列。
df_aug['doorman'] = df_aug['features'].str.contains("doorman")
df_aug['parking'] = df_aug['features'].str.contains("parking|garage")
df_aug['laundry'] = df_aug['features'].str.contains("laundry")
del df_aug['features'] # don't need this anymore
df_aug.head(3)
| bedrooms | bathrooms | latitude | longitude | price | doorman | parking | laundry | |
|---|---|---|---|---|---|---|---|---|
| 0 | 3 | 1.5 | 40.7145 | -73.9425 | 3000 | False | False | False |
| 1 | 2 | 1.0 | 40.7947 | -73.9667 | 5465 | True | False | False |
| 2 | 1 | 1.0 | 40.7388 | -74.0018 | 2850 | False | False | True |
删除del以外的列的另一种方法是使用drop()函数:
df2 = df.drop('description',axis=1) # drop doesn't affect df in place, returns new one
df2.head(2).T # kill this column, return new df without that column
| 0 | 1 | |
|---|---|---|
| listing_id | 7211212 | 7150865 |
| bathrooms | 1.5 | 1 |
| bedrooms | 3 | 2 |
| building_id | 53a5b119ba8f7b61d4e010512e0dfc85 | c5c8a357cba207596b04d1afd1e4f130 |
| created | 2016-06-24 07:54:24 | 2016-06-12 12:19:27 |
| display_address | Metropolitan Avenue | Columbus Avenue |
| features | [] | ['Doorman', 'Elevator', 'Fitness Center', 'Cat... |
| interest_level | medium | low |
| latitude | 40.7145 | 40.7947 |
| longitude | -73.9425 | -73.9667 |
| manager_id | 5ba989232d0489da1b5f2c45f6688adc | 7533621a882f71e25173b27e3139d83d |
| photos | ['https://photos.renthop.com/2/7211212_1ed4542... | ['https://photos.renthop.com/2/7150865_be3306c... |
| price | 3000 | 5465 |
| street_address | 792 Metropolitan Avenue | 808 Columbus Avenue |
让我们对数值特征做一些事情。
df_aug["beds_to_baths"] = df_aug["bedrooms"]/(df_aug["bathrooms"]+1)
df_aug.head(3)
| bedrooms | bathrooms | latitude | longitude | price | doorman | parking | laundry | beds_to_baths | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 3 | 1.5 | 40.7145 | -73.9425 | 3000 | False | False | False | 1.2 |
| 1 | 2 | 1.0 | 40.7947 | -73.9667 | 5465 | True | False | False | 1.0 |
| 2 | 1 | 1.0 | 40.7388 | -74.0018 | 2850 | False | False | True | 0.5 |
超出我们的范围,但让我们重新训练模型,看看它是否提高了 OOB 得分。
df_clean = df_aug[(df.price>1_000) & (df.price<10_000)]
df_train = df_clean[['bathrooms', 'bedrooms', 'longitude', 'latitude',
'doorman', 'parking', 'laundry', 'beds_to_baths', 'price']]
X_train = df_train[['bedrooms','bathrooms','latitude','longitude',
'doorman', 'parking', 'laundry', 'beds_to_baths']]
y_train = df_train['price']
rf = RandomForestRegressor(n_estimators=100, n_jobs=-1, oob_score=True)
rf.fit(X_train, y_train)
print(f"OOB R^2 score is {rf.oob_score_:.3f} (range is -infinity to 1.0; 1.0 is perfect)")
# OOB R^2 score is 0.870 (range is -infinity to 1.0; 1.0 is perfect)
I = pd.DataFrame(data={'Feature':X_train.columns, 'Importance':rf.feature_importances_})
I.sort_values('Importance',ascending=False)
| Feature | Importance | |
|---|---|---|
| 1 | bathrooms | 0.426254 |
| 3 | longitude | 0.234839 |
| 2 | latitude | 0.168338 |
| 0 | bedrooms | 0.067082 |
| 7 | beds_to_baths | 0.062770 |
| 4 | doorman | 0.026538 |
| 6 | laundry | 0.010321 |
| 5 | parking | 0.003859 |
这个分数稍微好一些,但不是很多。
将类别转换为数值数据
这不是通用的,但适用于小型(有序)类别集:
df['interest_level'] = df['interest_level'].map({'low':1,'medium':2,'high':3})
df[['interest_level']].head(5)
| interest_level | |
|---|---|
| 0 | 2 |
| 1 | 1 |
| 2 | 3 |
| 3 | 1 |
| 4 | 1 |
转换类型
df['some_boolean'] = True
df['some_boolean'] = df['some_boolean'].astype('int8')
df.some_boolean.head()
'''
bedrooms
3 1
2 1
1 1
1 1
4 1
Name: some_boolean, dtype: int8
'''
转换日期
df['dayofweek'] = df['created'].dt.dayofweek # add dow column
df['day'] = df['created'].dt.day
df['month'] = df['created'].dt.month
df.head(1).T
| 0 | |
|---|---|
| listing_id | 7211212 |
| bathrooms | 1.5 |
| bedrooms | 3 |
| building_id | 53a5b119ba8f7b61d4e010512e0dfc85 |
| created | 2016-06-24 07:54:24 |
| description | A Brand New 3 Bedroom 1.5 bath ApartmentEnjoy ... |
| display_address | Metropolitan Avenue |
| features | [] |
| interest_level | 2 |
| latitude | 40.7145 |
| longitude | -73.9425 |
| manager_id | 5ba989232d0489da1b5f2c45f6688adc |
| photos | ['https://photos.renthop.com/2/7211212_1ed4542... |
| price | 3000 |
| street_address | 792 Metropolitan Avenue |
| log_price | 8.00637 |
| one | 1 |
| some_boolean | 1 |
| dayofweek | 4 |
| day | 24 |
| month | 6 |
Feather 格式
df.to_feather("/tmp/rent.feather")
% time df = pd.read_feather("/tmp/rent.feather")
'''
CPU times: user 70.6 ms, sys: 43.9 ms, total: 114 ms
Wall time: 113 ms
'''
与加载 CSV 相比,似乎慢 5 倍:
% time df = pd.read_csv("data/rent.csv")
'''
CPU times: user 597 ms, sys: 73.5 ms, total: 671 ms
Wall time: 670 ms
'''
测试你的知识
过滤出超过 5 个浴室的所有行,并重新放入相同的数据帧
显示浴室与价格的散点图
显示热力图,其颜色是卧室数量的函数。
插入一个名为avg_price的列,它是所有价格的平均值
使用字典,将所有原始interest_level值转换为10, 20, 30,用于低,中和高类别
将manager_id列转换为类别而不是字符串类型
从created字段创建一个名为day_of_year的新列,带有每年的日期 1-365
3.3 操纵和可视化数据
原文:Manipulating and Visualizing Data
译者:飞龙
我们已经学习了加载文件的基础知识,现在是时候将加载的数据,从 NumPy 和 Pandas 重新组织为常用的数据结构了。 为了产生各种数据结构,我们将把它们提供给 matplotlib 进行可视化。 然后,本实验将全面介绍我们的通用数据科学规划模板中的第 2,3 和 5 步:
- 获取数据,这意味着找到合适的文件,或从 Web 收集数据并存储在文件中
- 从磁盘加载数据并放入组织成数据结构的内存
- 规范化,清理或以其他方式准备数据
- 处理数据,这可能意味着训练机器学习模型,计算摘要统计量或优化成本函数
- 输出结果,可以是简单地将答案保存到磁盘上,也可以生成奇特的可视化
您将在机器学习,时间序列分析等课程中了解第 4 步的更多信息。
让我们开始导入我们需要的所有软件包,并设置一些参数,使这个 Jupyter 笔记本看起来更好:
import pandas
import numpy as np
import matplotlib.pyplot as plt
pandas.options.display.max_rows = 7 # Don't display too much data (Pandas)
np.set_printoptions(threshold=4) # Don't display too much data (NumPy)
你的新 BFF
分析程序倾向于使用大量的一维和二维数组。2D 数组是矩阵和数据表。1D 数组是向量,例如欧几里德空间中的点。表的列或行也是一维数组。 Python 有列表和列表的列表,可以满足 1D 和 2D 数组,但 Pandas 和 NumPy 定义了类似但功能更强的数据结构。
让我们从 Pandas 数据帧开始,它们是非常像 Excel 表的强大表格。 我还认为 Pandas 的read_csv()是加载组织成行和列的大多数数据的最简单方法。 这是一个示例数据文件,包含一段时间内的价格列表,每行一个数据点且没有标题行:
! wc data/prices.txt
! head data/prices.txt
'''
345 345 2067 data/prices.txt
0.605
0.600
0.594
0.592
0.600
0.616
0.623
0.628
0.630
0.629
'''
(wc和head是 bash 命令,你可能会发现它们将来很有用。)
以下是使用 Pandas 加载该文件的方法:
prices = pandas.read_csv('data/prices.txt', header=None)
prices # jupyter notebooks know how to display this nicely
| 0 | |
|---|---|
| 0 | 0.605 |
| 1 | 0.600 |
| 2 | 0.594 |
| ... | ... |
| 342 | 1.939 |
| 343 | 1.898 |
| 344 | 1.891 |
345 rows × 1 columns
左栏中的数字只是索引,是 Pandas 会显示给您的信息;它们不作为数据结构的一部分存储在内存中。 我们来看看这个数据结构的类型和形状:
print "type is", type(prices)
print "shape is", prices.shape
'''
type is <class 'pandas.core.frame.DataFrame'>
shape is (345, 1)
'''
该输出表明,数据存储在DataFrame对象中,并且有 344 行和 1 列。
虽然 Pandas 非常适合加载数据,而我们将在下面看到其他一些内容,但我更喜欢使用 NumPy 数组;实际的类型称为ndarray。让我们将价格列表从数据帧转换为 NumPy 数组:
m = prices.as_matrix() # Convert data frame to numpy array
print "type is", type(m)
print "shape is", m.shape
print m
'''
type is <type 'numpy.ndarray'>
shape is (345, 1)
[[ 0.605]
[ 0.6 ]
[ 0.594]
...,
[ 1.939]
[ 1.898]
[ 1.891]]
'''
打印的数组看起来像列表的列表,但它是不同的数据类型。 仅仅因为两个数据结构以相同的方式打印出来,并不意味着它们是相同类型的对象。
我们可以使用数组索引表示法array[row, column]访问 2D NumPy 数组:
print m[0] # Access the first row
print m[0,0] # Access the first column of the first row
print m[1] # Access the 2nd row
print m[1,0] # Access the first column of the 2nd row
'''
[ 0.605]
0.605
[ 0.6]
0.6
'''
虽然这有点奇怪。 我们将其视为一维数组或仅仅是一个列表,而不是具有单列的二维数组(形状为 345 x 1)。 为了让 NumPy 将其视为 1D,我们使用数组的shape属性:
m.shape = (345,) # len(m)==345
m
# array([ 0.605, 0.6 , 0.594, ..., 1.939, 1.898, 1.891])
现在,我们可以像我们期望的那样使用单个索引访问元素:
print m[0]
print m[1]
print m[2]
'''
0.605
0.6
0.594
'''
第二个参数为空的形状表示 1D 数组,这是 NumPy 将常规 Python 列表转换为数组的方式:
sizes = [28, 32, 34, 36, 38, 39, 40, 41] # Plain old Python list
a = np.array(sizes) # Convert to NumPy array
print "shape is", a.shape
a
'''
shape is (8,)
array([28, 32, 34, ..., 39, 40, 41])
'''
虽然我们在这里,但这里是将列表的列表转换为 2D NumPy 数组的方式:
stuff = [
[ 18, 8, 307, 3504],
[ 15, 8, 350, 3693],
[ 18, 8, 318, 3436]
]
m = np.array(stuff)
print "shape is", m.shape
m
'''
shape is (3, 4)
array([[ 18, 8, 307, 3504],
[ 15, 8, 350, 3693],
[ 18, 8, 318, 3436]])
'''
现在多个索引是有意义的。 例如,要访问包含值 3436 的元素,我们使用m[2,3](第 3 行,第 4 列)。
类型问题
TODO:展示x + y可以是字符串,int,float,list或 numpy 数组。 重载运算符。例如,如果x是字符串且y是int,则x * y可以是字符串
import numpy as np
def f(x):
"Scalar or vector math!"
return np.cos(3 * np.pi * x) / x
print f(3.4)
X = np.array([1.2,3.0]) # a numpy array is more flexible than list of numbers
print f(X) # returns array due to vector math in f()!
print [f(x) for x in X] # manually apply f() to X
'''
0.237946174816
[ 0.25751416 -0.33333333]
[0.25751416197912252, -0.33333333333333331]
'''
绘制时间序列数据
我们的价格列表表示时间序列数据,例如股票价格,温度或人口波动。Matplotlib 是一个很好的数据可视化库,在本节中我们将使用它的plot(),将价格显示为时间序列。 该函数接受单独的数组X和Y坐标。
我发现 Matplotlib 有点神秘,但我学会了一遍又一遍地使用的模式,比如这个时间序列的绘图。
m = prices.as_matrix() # Let's convert pandas data frame to numpy array
time = np.arange(0, len(m), 1) # Time axis goes from 0 to len(m) by 1
#plt.figure(figsize=(5, 2)) # Prepare a plot 5x2 inches
plt.plot(time, m) # Plot time vs the prices data
plt.xlabel("Time") # Always set the axes labels
plt.ylabel("Price (dollars)")
plt.show() # Show the actual plot

绘制函数
有时我们有一个平滑的函数,比如我们想绘制的余弦。为此,我们需要定期采样函数,来收集Y坐标列表(如之前的价格)。让我们首先定义将X坐标映射到Y值的函数,然后以 0.1 到 1.1 之间,步长 0.01 的固定间隔获取X值的样本:
def f(x):
return np.cos(3 * np.pi * x) / x
X = np.arange(.1, 1.1, 0.01) # from .1 to 1.1 by step 0.01
有三种方法可以在X中包含的坐标处,采样函数f(),我在这里已经描述过了。 所有这三种方法都使用我们的映射模式:
# Get f(x) values for all x in three different ways
# Option 1: (non-Pythonic)
Y = []
for x in X:
Y.append(f(x))
# Option 2: Pythonic way (cool kids do this)
Y = [f(x) for x in X]
# Option 3: Data science way (the most popular kids do this)
Y = f(X) # a so-called broadcast; implied map
print X
print Y
'''
[ 0.1 0.11 0.12 ..., 1.07 1.08 1.09]
[ 5.87785252 4.62764923 3.54816076 ..., -0.73846263 -0.67497095
-0.60670813]
'''
给定X和Y坐标,我们可以绘制函数:
plt.figure(figsize=(5, 2))
plt.plot(X, Y)
plt.xlabel("x")
plt.ylabel("cos(3 * pi * x) / x")
plt.show()

可视化变量之间的关系
让我们现在超越一维数组到二维数组并绘制一列与另一列。以下是一些样本汽车数据(带标题行),带有每加仑英里数,汽缸数,发动机马力和重量(磅)的列:
! head data/cars.csv
'''
MPG,CYL,ENG,WGT
18,8,307,3504
15,8,350,3693
18,8,318,3436
16,8,304,3433
17,8,302,3449
15,8,429,4341
14,8,454,4354
14,8,440,4312
14,8,455,4425
'''
我们可以再次使用 Pandas 将数据加载到数据帧中,然后转换为 NumPy 2D 数组(矩阵):
cars = pandas.read_csv('data/cars.csv')
print "shape is", cars.shape
cars
# shape is (392, 4)
| MPG | CYL | ENG | WGT | |
|---|---|---|---|---|
| 0 | 18.0 | 8 | 307.0 | 3504 |
| 1 | 15.0 | 8 | 350.0 | 3693 |
| 2 | 18.0 | 8 | 318.0 | 3436 |
| ... | ... | ... | ... | ... |
| 389 | 32.0 | 4 | 135.0 | 2295 |
| 390 | 28.0 | 4 | 120.0 | 2625 |
| 391 | 31.0 | 4 | 119.0 | 2720 |
392 rows × 4 columns
m = cars.as_matrix()
print "shape is", m.shape
m
'''
shape is (392, 4)
array([[ 18., 8., 307., 3504.],
[ 15., 8., 350., 3693.],
[ 18., 8., 318., 3436.],
...,
[ 32., 4., 135., 2295.],
[ 28., 4., 120., 2625.],
[ 31., 4., 119., 2720.]])
'''
假设我们对汽车重量和燃油效率之间的关系感兴趣。 我们可以通过使用散点图绘制重量与效率来直观地检查这种关系。
这就引出了如何从 numpy 数组中提取列的问题,其中每列代表与所有汽车的一个属性关联的数据。 想法是固定列号,但使用通配符(冒号字符)表示我们想要所有行:
# can do this:
print cars.MPG
print cars['MPG']
# but I like as numpy matrix
mpg = m[:,0]
wgt = m[:,3]
print "shape is", mpg.shape
mpg
'''
0 18.0
1 15.0
2 18.0
...
389 32.0
390 28.0
391 31.0
Name: MPG, dtype: float64
0 18.0
1 15.0
2 18.0
...
389 32.0
390 28.0
391 31.0
Name: MPG, dtype: float64
shape is (392,)
array([ 18., 15., 18., ..., 32., 28., 31.])
'''
一旦我们有了两列,我们就可以使用 matplotlib 的scatter()函数:
plt.figure(figsize=(5, 2))
plt.scatter(wgt, mpg, alpha=0.5) # looks cooler with alpha (opacity) at 50%
plt.xlabel('Weight (pounds)')
plt.ylabel('Miles per gallon')
plt.show()

很棒!这显示了重量和效率之间的明确关系:汽车越重,效率越低。
了解发动机汽缸的数量与重量和效率有何关系也很有趣。 我们可以转到三维图形甚至多个图形,但在这种情况下最好将另一个属性添加到单个图形中。我们可以根据气缸数改变颜色,但更好的可视化会改变所绘制点的大小。
我们可以像以前一样用m[:,1]获取气缸的数量,但我们现在需要单独绘制每个点,因为我们必须指定不同的尺寸。 这意味着我们需要一个循环在scatter()周围,来传递单独的X和Y坐标而不是列表。 scatter()的s参数实际上与我们想要的圆的面积成正比(参见 pyplot 散点图的标记大小)。 为了突出引擎尺寸之间的差异,我按比例 .7 缩放。以下是用于说明三个变量之间关系的代码:
plt.figure(figsize=(5, 2))
hp = m[:,2]
plt.scatter(wgt, mpg, s=hp*.5, alpha=0.1)
plt.xlabel('Weight (pounds)')
plt.ylabel('Miles per gallon')
plt.show()

在探索数据时,了解唯一值的集合通常很有用。例如,如果想知道气缸数的集合,我们可以使用set(mylist):
m = cars.as_matrix()
cyl = m[:,1]
print set(cyl)
有趣。 我不知道有 3 个汽缸的汽车。
练习:使用映射模式将cyl转换为整数集合(不是浮点值)。 提示:int(3.0)为 3。
直方图
我们可以计算每个唯一值中有多少出现在数据集中,而不仅仅是属性值的唯一集合。 Python 的Counter对象,一种dict,知道如何计算元素。 例如,以下是我们如何获得类似字典的对象,将气缸数映射到具有多个气缸的汽车数量:
from collections import Counter
m = cars.as_matrix()
cyl = m[:,1]
Counter(cyl)
# Counter({3.0: 4, 4.0: 199, 5.0: 3, 6.0: 83, 8.0: 103})
这对于类别变量非常有用,类别变量的种类数是这样,但是“计数器”不适合数值。 此外,观察气缸计数,很难快速了解相关的总体。 用直方图可视化数据是一个更好的想法,也适用于数值。 以下是为气缸属性制作直方图的代码:
plt.figure(figsize=(5, 2))
plt.hist(cyl, alpha=0.5)
plt.xlabel('Number of cylinders')
plt.ylabel('Number of cars')
plt.show()

相同的模式也为我们提供了数值数据的直方图:
m = cars.as_matrix()
hp = m[:,2]
plt.figure(figsize=(5, 2))
plt.hist(hp, alpha=0.5)
plt.xlabel('Horsepower')
plt.ylabel('Number of cars')
plt.show()

直方图实际上是变量密度函数的粗略估计,因此将直方图标准化来使区域积分(总和)为 1,通常很有用。要获得标准化的直方图,请使用参数normed=True:
plt.figure(figsize=(5, 2))
n, bins, patches = plt.hist(hp, normed=True, alpha=0.5)
plt.xlabel('Horsepower')
plt.ylabel('Probability')
plt.show()

请注意,桶的高度之和不等于 1;高度乘以宽度之和等于 1。
切片和切块
到目前为止,我们加载的所有数据都是数字的,但以字符串的形式加载分类或文本变量是很常见的。在这种情况下,Pandas 数据帧非常有用。 让我们加载一些示例销售数据,包含三个字符串列:
sales = pandas.read_csv('data/sales-small.csv')
sales
| Date | Quantity | Unit Price | Shipping | Customer Name | Product Category | Product Name | |
|---|---|---|---|---|---|---|---|
| 0 | 10/13/10 | 6 | 38.94 | 35.00 | Muhammed MacIntyre | Office Supplies | Eldon Base for stackable storage shelf, platinum |
| 1 | 10/1/12 | 49 | 208.16 | 68.02 | Barry French | Office Supplies | 1.7 Cubic Foot Compact "Cube" Office Refrigera... |
| 2 | 10/1/12 | 27 | 8.69 | 2.99 | Barry French | Office Supplies | Cardinal Slant-D Ring Binder, Heavy Gauge Vinyl |
| ... | ... | ... | ... | ... | ... | ... | ... |
| 28 | 11/8/10 | 28 | 13.48 | 4.51 | Carlos Soltero | Office Supplies | Tenex Personal Project File with Scoop Front D... |
| 29 | 10/21/12 | 49 | 6.08 | 1.17 | Grant Carroll | Office Supplies | Col-Erase Pencils with Erasers |
| 30 | 1/1/11 | 10 | 5.98 | 4.38 | Don Miller | Technology | Imation 3.5" DS/HD IBM Formatted Diskettes, 10... |
31 rows × 7 columns
数据帧的好处是我们可以使用table.attribute或数组索引表示法table[attribute ]来按名称访问列。
sales.Date
'''
0 10/13/10
1 10/1/12
2 10/1/12
...
28 11/8/10
29 10/21/12
30 1/1/11
Name: Date, dtype: object
'''
sales['Date']
'''
0 10/13/10
1 10/1/12
2 10/1/12
...
28 11/8/10
29 10/21/12
30 1/1/11
Name: Date, dtype: object
'''
sales['Customer Name']
'''
0 Muhammed MacIntyre
1 Barry French
2 Barry French
...
28 Carlos Soltero
29 Grant Carroll
30 Don Miller
Name: Customer Name, dtype: object
'''
通过sales [0]访问行不起作用,因为 Pandas 希望使用数组索引表示法来获取列。 相反,我们必须使用稍微麻烦的表示法:
sales.iloc[0] # get first row of data
'''
Date 10/13/10
Quantity 6
Unit Price 38.94
Shipping 35
Customer Name Muhammed MacIntyre
Product Category Office Supplies
Product Name Eldon Base for stackable storage shelf, platinum
Name: 0, dtype: object
'''
为了获得单个元素,我们可以在loc之后使用常规的 Python 表示法,用于列表的列表:
print sales.iloc[0][0], sales.iloc[0][1], sales.iloc[0][2]
# 10/13/10 6 38.94
在软件的构建和调试过程中,我经常喜欢显式打印列名,如上所示。 另一方面,如果我们需要将元素转换为普通的 Python 列表,我们可以使用list()来实现:
row = list(sales.iloc[0])
print row
# ['10/13/10', 6, 38.939999999999998, 35.0, 'Muhammed MacIntyre', 'Office Supplies', 'Eldon Base for stackable storage shelf, platinum']
练习:将sales的所有行转换为列表的列表。 提示:使用映射模式和list()。
这项工作的任务很普遍,Pandas 直接提供了一种转换机制:
m = sales.as_matrix()
print "Type is", type(m)
print m[0] # get first row
'''
Type is <type 'numpy.ndarray'>
['10/13/10' 6 38.94 ..., 'Muhammed MacIntyre' 'Office Supplies'
'Eldon Base for stackable storage shelf, platinum']
'''
我们仍然可以使用之前看到的切片表示,来单独获取列:
m[:,0] # get first column
'''
array(['10/13/10', '10/1/12', '10/1/12', ..., '11/8/10', '10/21/12',
'1/1/11'], dtype=object)
'''
m[:,4] # get fifth column
'''
array(['Muhammed MacIntyre', 'Barry French', 'Barry French', ...,
'Carlos Soltero', 'Grant Carroll', 'Don Miller'], dtype=object)
'''
单独获取数据帧
对于机器学习,我们经常希望将其中一列作为因变量分开,将其他列保持为一组独立变量。 我们通常使用的符号是X - > Y,意味着X中的观察集将结果预测或分类为Y.
例如,假设我们想要根据效率,汽缸数量和整车重量来预测发动机尺寸。我们需要将引擎大小分离为Y并将其他列组合成X。使用 Pandas,我们可以轻松地分离变量并保留变量名称:
cars = pandas.read_csv('data/cars.csv')
Y = cars['ENG']
X = cars[['MPG','CYL','WGT']]
print X
print
print Y
'''
MPG CYL WGT
0 18.0 8 3504
1 15.0 8 3693
2 18.0 8 3436
.. ... ... ...
389 32.0 4 2295
390 28.0 4 2625
391 31.0 4 2720
[392 rows x 3 columns]
0 307.0
1 350.0
2 318.0
...
389 135.0
390 120.0
391 119.0
Name: ENG, dtype: float64
'''
转换为 NumPy 数组会删除列名称,但让我们将其视为矩阵,这在很多情况下都很方便(例如,矩阵加法)。从 NumPy 数组中分离列有点麻烦,但是:
m = cars.as_matrix()
Y = m[:,2]
X = np.column_stack((m[:,0],m[:,1], m[:,3])) # note extra parens; it's a tuple of columns
print X
print
print Y
'''
[[ 18. 8. 3504.]
[ 15. 8. 3693.]
[ 18. 8. 3436.]
...,
[ 32. 4. 2295.]
[ 28. 4. 2625.]
[ 31. 4. 2720.]]
[ 307. 350. 318. ..., 135. 120. 119.]
'''
虽然 NumPy 数组在拆分表时更麻烦,但不用loc访问元素通常更方便:
print cars.iloc[0][1]
print m[0,1]
'''
8.0
8.0
'''
混合和缺失的数据
使用来自 Jeremy Howard 的提示,有关真实世界的数据清理。
加载和解析日期
import pandas
df = pandas.read_csv("data/mixed.csv", parse_dates=['Date'])
df
| Date | Description | Size | Price | Topic | |
|---|---|---|---|---|---|
| 0 | 2017-06-20 | NaN | 92.0 | 1.50 | News |
| 1 | 2017-06-21 | run forest | 42.0 | 2.34 | Sports |
| 2 | 2017-06-21 | not your droids | 19.0 | 0.88 | Sports |
| 3 | 2017-06-22 | hi mom | NaN | 9.30 | Politics |
| 4 | 2017-06-23 | foo&bar | 1.0 | 10.00 | NaN |
| 5 | 2017-06-24 | get off my lawn | 99.0 | 8.90 | Sci |
如果您需要将日期转换为已用时间,则可以将日期时间戳转换为 UNIX 时间,即自 1970 年以来的秒数:
d = df['Date']
delta = d - pandas.datetime(1970,1,1)
delta
'''
0 17337 days
1 17338 days
2 17338 days
3 17339 days
4 17340 days
5 17341 days
Name: Date, dtype: timedelta64[ns]
'''
df3 = df.copy()
df3['Date'] = delta.dt.total_seconds()
df3
| Date | Description | Size | Price | Topic | Size_na | |
|---|---|---|---|---|---|---|
| 0 | 1.497917e+09 | NaN | 92.0 | 1.50 | 2 | False |
| 1 | 1.498003e+09 | run forest | 42.0 | 2.34 | 5 | False |
| 2 | 1.498003e+09 | not your droids | 19.0 | 0.88 | 5 | False |
| 3 | 1.498090e+09 | hi mom | 42.0 | 9.30 | 3 | True |
| 4 | 1.498176e+09 | foo&bar | 1.0 | 10.00 | 1 | False |
| 5 | 1.498262e+09 | get off my lawn | 99.0 | 8.90 | 4 | False |
或者,您可以将时间戳转换为自 1970 年以来的天数:
delta.dt.days
'''
0 17337
1 17338
2 17338
3 17339
4 17340
5 17341
Name: Date, dtype: int64
'''
字符串到类别变量
以下是我们将列转换为类别变量的方法:
df['Topic'] = df['Topic'].astype('category')
df
| Date | Description | Size | Price | Topic | |
|---|---|---|---|---|---|
| 0 | 2017-06-20 | NaN | 92.0 | 1.50 | News |
| 1 | 2017-06-21 | run forest | 42.0 | 2.34 | Sports |
| 2 | 2017-06-21 | not your droids | 19.0 | 0.88 | Sports |
| 3 | 2017-06-22 | hi mom | NaN | 9.30 | Politics |
| 4 | 2017-06-23 | foo&bar | 1.0 | 10.00 | NaN |
| 5 | 2017-06-24 | get off my lawn | 99.0 | 8.90 | Sci |
print df['Topic'].cat.categories # .cat field gives us access to categories stuff
# Index([u'News', u'Politics', u'Sci', u'Sports'], dtype='object')
print df['Topic'].cat.codes
'''
0 0
1 3
2 3
3 1
4 -1
5 2
dtype: int8
'''
print df['Topic'].cat.as_ordered()
'''
0 News
1 Sports
2 Sports
3 Politics
4 NaN
5 Sci
dtype: category
Categories (4, object): [News < Politics < Sci < Sports]
'''
字符串到序数
如果我们愿意,我们可以将该类别转换为整数:
# make sure you convert to categorical first
df['Topic'] = df['Topic'].astype('category')
df['Topic'] = df['Topic'].cat.codes+1 # add one so NA (-1) becomes 0
df
| Date | Description | Size | Price | Topic | Size_na | |
|---|---|---|---|---|---|---|
| 0 | 2017-06-20 | NaN | 92.0 | 1.50 | 2 | False |
| 1 | 2017-06-21 | run forest | 42.0 | 2.34 | 5 | False |
| 2 | 2017-06-21 | not your droids | 19.0 | 0.88 | 5 | False |
| 3 | 2017-06-22 | hi mom | 42.0 | 9.30 | 3 | True |
| 4 | 2017-06-23 | foo&bar | 1.0 | 10.00 | 1 | False |
| 5 | 2017-06-24 | get off my lawn | 99.0 | 8.90 | 4 | False |
缺失数据
我们的数据缺少描述,我们可以忽略,但也缺少大小(数值)和主题(类别)条目。
*如果元素是数字,我们用列中位数替换缺失值,并添加一列 0 或 1 来表示是否缺少值。
*如果元素是分类的,当我们在get_dummies()上使用参数dummy_na = True时,Pandas 可以自动处理缺失值(参见下一节)。
让我们转换丢失的数值数据:
pandas.isnull(df['Size'])
'''
0 False
1 False
2 False
3 True
4 False
5 False
Name: Size, dtype: bool
'''
df['Size_na'] = pandas.isnull(df['Size'])
df
| Date | Description | Size | Price | Topic | Size_na | |
|---|---|---|---|---|---|---|
| 0 | 2017-06-20 | NaN | 92.0 | 1.50 | 1 | False |
| 1 | 2017-06-21 | run forest | 42.0 | 2.34 | 4 | False |
| 2 | 2017-06-21 | not your droids | 19.0 | 0.88 | 4 | False |
| 3 | 2017-06-22 | hi mom | NaN | 9.30 | 2 | True |
| 4 | 2017-06-23 | foo&bar | 1.0 | 10.00 | 0 | False |
| 5 | 2017-06-24 | get off my lawn | 99.0 | 8.90 | 3 | False |
szcol = df['Size']
df['Size'] = szcol.fillna(szcol.median())
df
| Date | Description | Size | Price | Topic | Size_na | |
|---|---|---|---|---|---|---|
| 0 | 2017-06-20 | NaN | 92.0 | 1.50 | 1 | False |
| 1 | 2017-06-21 | run forest | 42.0 | 2.34 | 4 | False |
| 2 | 2017-06-21 | not your droids | 19.0 | 0.88 | 4 | False |
| 3 | 2017-06-22 | hi mom | 42.0 | 9.30 | 2 | True |
| 4 | 2017-06-23 | foo&bar | 1.0 | 10.00 | 0 | False |
| 5 | 2017-06-24 | get off my lawn | 99.0 | 8.90 | 3 | False |
虚拟变量
相反,我们可以将类别变量转换为虚拟变量,也称为“独热编码”。
如果我们懒惰,我们可以将所有内容转换为虚拟对象,但是Description字段不是我们需要转换的内容,因为它主要是我们所携带的信息。
pandas.get_dummies(df) # convert all categorical to dummies
| Date | Size | Price | Topic | Size_na | Description_foo&bar | Description_get off my lawn | Description_hi mom | Description_not your droids | Description_run forest | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2017-06-20 | 92.0 | 1.50 | 1 | False | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 1 | 2017-06-21 | 42.0 | 2.34 | 4 | False | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 |
| 2 | 2017-06-21 | 19.0 | 0.88 | 4 | False | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 |
| 3 | 2017-06-22 | 42.0 | 9.30 | 2 | True | 0.0 | 0.0 | 1.0 | 0.0 | 0.0 |
| 4 | 2017-06-23 | 1.0 | 10.00 | 0 | False | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 5 | 2017-06-24 | 99.0 | 8.90 | 3 | False | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 |
pandas.get_dummies(df['Topic']) # One column's dummies
| 0 | 1 | 2 | 3 | 4 | |
|---|---|---|---|---|---|
| 0 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 |
| 1 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 |
| 2 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 |
| 3 | 0.0 | 0.0 | 1.0 | 0.0 | 0.0 |
| 4 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 5 | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 |
pandas.get_dummies(df['Topic'], dummy_na=True) # Add an "na" column
| 0.0 | 1.0 | 2.0 | 3.0 | 4.0 | nan | |
|---|---|---|---|---|---|---|
| 0 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 1 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 |
| 2 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 |
| 3 | 0.0 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 |
| 4 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 5 | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 | 0.0 |
我们可以手动将这些新列打包到旧数据帧中并删除旧列:
df2 = pandas.concat([df,pandas.get_dummies(df['Topic'], dummy_na=True)], axis=1)
df2.drop('Topic', axis=1, inplace=True) # Considered better than del df2['Topic'] I think
df2
| Date | Description | Size | Price | Size_na | 1.0 | 2.0 | 3.0 | 4.0 | 5.0 | nan | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2017-06-20 | NaN | 92.0 | 1.50 | False | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 1 | 2017-06-21 | run forest | 42.0 | 2.34 | False | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 |
| 2 | 2017-06-21 | not your droids | 19.0 | 0.88 | False | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 |
| 3 | 2017-06-22 | hi mom | 42.0 | 9.30 | True | 0.0 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 |
| 4 | 2017-06-23 | foo&bar | 1.0 | 10.00 | False | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 5 | 2017-06-24 | get off my lawn | 99.0 | 8.90 | False | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 | 0.0 |
或者,我们可以通过指定要转换的列来轻松实现:
pandas.get_dummies(df, columns=['Topic'], dummy_na=True) # The easy way
| Date | Description | Size | Price | Size_na | Topic_0.0 | Topic_1.0 | Topic_2.0 | Topic_3.0 | Topic_4.0 | Topic_nan | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2017-06-20 | NaN | 92.0 | 1.50 | False | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 1 | 2017-06-21 | run forest | 42.0 | 2.34 | False | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 |
| 2 | 2017-06-21 | not your droids | 19.0 | 0.88 | False | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 |
| 3 | 2017-06-22 | hi mom | 42.0 | 9.30 | True | 0.0 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 |
| 4 | 2017-06-23 | foo&bar | 1.0 | 10.00 | False | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 5 | 2017-06-24 | get off my lawn | 99.0 | 8.90 | False | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 | 0.0 |
如果您需要遍历数据帧中的列,则可以使用for循环执行此操作:
# walk columns; col is the actual series
for name,col in df.iteritems():
print name
'''
Date
Description
Size
Price
Topic
Size_na
'''
df.describe() # useful stats about columns
| Size | Price | Topic | |
|---|---|---|---|
| count | 6.000000 | 6.000000 | 6.000000 |
| mean | 49.166667 | 5.486667 | 3.333333 |
| std | 39.117345 | 4.326175 | 1.632993 |
| ... | ... | ... | ... |
| 50% | 42.000000 | 5.620000 | 3.500000 |
| 75% | 79.500000 | 9.200000 | 4.750000 |
| max | 99.000000 | 10.000000 | 5.000000 |
8 rows × 3 columns
总结
在本讲座中,您已经学习了加载和操作数据的基础知识:
- 使用
read_csv()将数据加载到 Pandas 数据帧中 - 使用
as_matrix()将数据帧转换为 NumPy 数组 - 使用
dataframe.columnname或matrix[:, columnindex]提取列 - 使用
dataframe.iloc[rowindex][columnindex]或matrix[rowindex, columnindex]访问元素 - 使用
set(mylist)获取唯一元素
而且,您已经学会了如何可视化:
- 时间序列数据
- 超出给定范围的函数
- 使用散点图绘制变量之间的关系
- 近似密度函数的直方图
三、关键编程模式
4.1 生成均匀的随机数
原文:Generating Uniform Random Numbers
译者:飞龙
Q:如何生成随机字符串?
A:把一个新学生放在 vi 编辑器前面,让他/她退出。
-- Emiliano Lourbet (@taitooz)
要执行基于计算机的模拟,我们需要能够生成随机数。 生成均匀分布的随机数是最容易的,并且是标准编程语言的“给我一个随机数”函数。 这是一个 Python 会话示例:
import random
print(random.random())
print(random.random())
print(random.random())
'''
0.3562460099444431
0.8189486725857756
0.8483609628240635
'''
均匀随机变量非常重要,因为它们是我们生成其他随机变量的基础,如二项,正态,指数等。
讨论
我们可以通过访问以太网网络设备上的噪声来生成实际随机数,但噪声可能不是均匀分布的。 我们通常会生成伪随机数,这些伪随机数不是随机的,而是看起来像。 从 Ross' Simulation 一书中,我们看到一个非常简单的递归机制(递归关系),使用 模/余数操作生成[0,1)中的值:
x{i+1} = a xi modulo m
这是递归的(或迭代的而不是闭式),因为xi是先前值的函数:
x1 = a x0 modulo m
x2 = a x1 modulo m
x3 = a x2 modulo m
x4 = a x3 modulo m
...
Ross 表示xi在[0, m-1]中,但设置任何xi = 0会使所有后续xi = 0,所以我们应该避免这种情况。 实际上,然后,此方法返回(0,1)中的值。
要从xi获得[0,1)中的随机数,我们计算xi / m。
我们必须为a和m选择一个使xi看似随机的值。 Ross 建议为m选择一个大的素数,它符合我们的整数的字大小,例如m = 2 ^ 31 - 1,a = 7 ^ 5 = 16807。
最初我们为x0设置了一个值,称为随机种子(它不是第一个随机数)。 每个种子都会产生不同的伪随机数序列。 (在 Python 中,您可以使用random.seed(x)来设置标准库的种子。)
Python 实现
我们的目标是采用这个简单的递归公式并使用它来生成统一的随机数。 函数runif01()为每个调用返回一个新的随机值。 使用m = 2 ^ 31 - 1,a = 7 ^ 5 = 16807,初始种子x0 = 666。
a = 16807
m = pow(2,31)-1
DFLT_SEED = 666
x_i = DFLT_SEED # this is our x_i that changes each runif01() call
def runif01():
"Return a random value in U(0,1)"
global x_i
x_i = a * x_i % m
# display(callsviz(varnames=['a','m','x_i']))
return x_i / float(m)
请注意,xi位于全局空间而不是runif()空间。
from lolviz import callsviz
runif01()
# 0.005212361926777457
让我们试试它:
[runif01() for i in range(4)]
'''
[0.21940766983637944,
0.5847069400291457,
0.1695410698510432,
0.4767609864830789]
'''
练习
定义一个新函数runif(a, b),它生成[a,b)中的随机数而不是[0,1]。 提示:我们需要缩放和移动[0,1)中的均匀随机值。 注意:您不能在本实验中使用random.random()或任何其他内置随机数生成器。
def runif(a,b):
"Return a random value in U(a,b)"
...
def runif(a,b):
"Return a random value in U(a,b)"
if b<a: # swap
t = a
a = b
b = t
return runif01()*(b-a) + a
print([runif(0,10) for i in range(3)])
print([runif(5,6) for i in range(3)])
'''
[5.931631259588352, 2.926579901448721, 7.028403648654187]
[5.638012293091981, 5.072609996922599, 5.356218278108266]
'''
练习
定义一个新函数setseed(x),它更新seed全局变量。
def setseed(s):
"Update the seed global variable but ensure seed > 0"
...
测试序列:
setseed(501)
print runif01()
print runif01()
print runif01()
应该生成:
0.00392101099897
0.900431859726
0.558266419712
def setseed(s):
"Update the seed global variable but ensure seed > 0"
global x_i
if s <= 0: s = 666
x_i = s
setseed(501)
print([runif01() for i in range(3)])
print([runif(5,6) for i in range(3)])
'''
[0.003921010998972231, 0.9004318597262874, 0.5582664197116468]
[5.783716093648093, 5.916385943496779, 5.698552350373265]
'''
随机数密度函数估计
稍微向前走一步,我们可以将数据操纵和可视化中的直方图绘制示例,用作密度估计的粗略形式,来验证随机值的分布是近似均匀的:
import matplotlib.pyplot as plt
# jupyter notebook command (ignore)
%matplotlib inline
sample = [runif01() for i in range(5000)] # Get 5000 random variables
plt.figure(figsize=(4, 1.5))
plt.hist(sample, bins=10, density=True, alpha=0.3)
plt.xlabel('Random value from U(0,1)')
plt.ylabel('Probability')
plt.show()

在生成伪随机数的情况下,我们感兴趣的是由递归关系生成的值序列。 现在我们将注意力转向迭代方法,它循环到递归关系的值收敛为止。
4.2 近似平方根
译者:飞龙
有许多有用的函数没有闭式解,这意味着我们不能只进行计算并返回值。 相反,我们需要使用迭代方法来近似函数值。 我们可以用它来近似正弦(用泰勒级数展开),近似平方根(我们将在本课程中做),或优化成本或误差函数(下一讲的梯度下降)。
与之前的统一随机变量讲座一样,我们必须将递归关系转换为 Python。 我们将寻找收敛序列,而不是返回递推序列中的单个值。 换句话说,如果我们将序列计算得足够远,x_{i + 1}将接近xi,使xi成为平方根的非常准确的近似值。 这将教会我们迭代计算的基础知识,并为更复杂的函数优化材料做好准备。
巴比伦方法
为了近似平方根,我们的想法是选择一个初始估计值x0,然后使用巴比伦方法来更好地的估计和迭代值xi。递归关系:
x{i+1} = 1/2 (xi + n/xi)
网上有很多东西你可以阅读,来更多了解为什么这个过程有效,但它依赖于xi和n / xi的平均值(中点),让我们更接近n的平方根。很酷的是迭代很快收敛。
我们的目标是编写一个函数,它接受一个数字并将其返回平方根。 在开始编码之前我们对这个函数了解多少? 好吧,根据我们的函数工作计划,我们有一个明确的问题描述,我们也有我们想要的函数签名:
def sqrt(n):
因为我们正在实现递归关系,所以我们知道我们将有一个循环,从xi计算x{i + 1}。
收敛
循环的终止条件是当我们达到收敛或接近收敛。 收敛只意味着x{i + 1}非常接近xi。 因为我们永远无法比较实数,所以我们必须检查差异是否小于某些精度,如 0.00000001。
迭代方法的大纲
正如我们有一个分析程序的大纲,迭代方法都共享相同的基本大纲。 (我在这里假设x{i + 1}仅取决于一个先前的值,并且i在循环转换时隐式递增。)
- 将
x0设为初始值 - 重复:
function-giving-next-value(xi)
- 直到
abs(x{i+1} - xi) < precision - 返回
x{i+1}
因为 Python 没有do-while循环,所以我们用一个包含条件的无限循环来伪造它,这会在收敛时中断:
- 将
x0设为初始值 while True:function-giving-next-value(xi)- 如果
abs(x{i+1} - xi) < precision- 返回
x{i+1}
- 返回
这是一个相当简单的递归关系实现,但你会注意到,我们实际上不需要保留所有先前的xi,除了新值和前一个值。 这是一个 Python 实现,它只跟踪两个值并遵循无限循环模式:
def sqrt(n):
"compute square root of n"
PRECISION = 0.00000001 # stop iterating when we converge with this delta
x_0 = 1.0 # pick any old initial value
x_prev = x_0
while True: # Python doesn't have repeat-until loop so fake it
#print(x_prev)
x_new = 0.5 * (x_prev + n/x_prev)
if abs(x_new - x_prev) < PRECISION:
return x_new
x_prev = x_new # x_i+1 becomes x_i (previous value)
'''
1.0
50.5
26.24009900990099
15.025530119986813
10.840434673026925
10.032578510960604
10.000052895642693
10.000000000139897
'''
sqrt(100)
# 10.0
为了测试我们的平方根近似,我们可以将它与math.sqrt()进行比较,并使用 numpy 的isclose来进行比较。
import numpy as np
def check(n):
assert np.isclose(sqrt(n), np.sqrt(n))
def test_sqrt():
check(125348)
check(89.2342)
check(100)
check(1)
check(0)
test_sqrt()
如您所见,我们可以在函数中定义函数。 除了test_sqrt()之外的代码看不到函数check()之外,它没有任何特殊之处。 另一方面,check()可以看到test_sqrt()之外的符号,比如我们的sqrt()。
练习
输入(不要剪切/粘贴)sqrt(n)函数并测试,例如,sqrt(125348.0)。 确保得到正确的答案(354.045195),然后添加print语句,以便您可以看到xi值的序列。 我得到了:
1.0
62674.5
31338.249992
15671.1249162
7839.56178812
3927.77547356
1979.84435152
1021.5781996
572.139273508
395.612894667
356.228988269
354.051888518
354.045194918
354.045194855
注意它收敛得多快!
def sqrt_with_trace(n):
"compute square root of n"
PRECISION = 0.00000001 # stop iterating when we converge with this delta
x_0 = 1.0 # pick any old initial value
x_prev = x_0
while True: # Python doesn't have repeat-until loop so fake it
print(x_prev)
x_new = 0.5 * (x_prev + n/x_prev)
if abs(x_new - x_prev) < PRECISION:
return x_new
x_prev = x_new
sqrt_with_trace(125348.000000)
'''
1.0
62674.5
31338.249992022273
15671.12491623693
7839.561788117747
3927.7754735639414
1979.844351517673
1021.5781996033152
572.1392735077299
395.612894666841
356.2289882689982
354.0518885182295
354.04519491839494
354.04519485512014
354.04519485512014
'''
现在我们知道了如何实现收敛的递归关系,让我们来看看函数优化。 乍一看,它看起来完全不同,但使用了迭代方法的相同抽象。
4.3 单变量梯度下降
原文:Iterative Optimization Via Gradient Descent
译者:飞龙
本讲座/实验的目标是解决一个重要的迭代计算问题:梯度下降函数最小化。 我希望你会看到梯度下降与平方根近似相同,只是具有不同的递归关系。 本课程的完整源代码在这里。
讨论
找到最小化或“优化”函数f(x)(通常在某个范围内)的x是一项非常重要的操作,因为我们可以使用它来最小化风险,并且对于机器学习,可以学习我们的分类器或预测器的参数。 例如,深度学习使用成本函数优化来训练神经网络。 通常x将是一个向量,但我们假设x是一个标量来学习基础知识。 如果我们知道函数像二次多项式一样是凸的,那么有一个独特的解决方案,我们可以简单地将导数设置为零并求解x:
f'(x) = 0
这基本上是在问“这个函数在哪里变平?” 例如,函数f(x) = (x-2)^2 + 1的导数为f'(x) = 2x - 4,其零点为x = 2。
import matplotlib.pyplot as plt
import numpy as np
def f(x): return (x - 2)**2 + 1
graphx = np.arange(0, 4, 0.01)
graphy = f(graphx) # apply f to all of graphx (BROADCAST)
plt.plot(graphx, graphy)
plt.show()

def foo(x):
print(type(x))
return (x - 2)**2 + 1
foo(2), foo(10)
'''
<class 'int'>
<class 'int'>
(1, 65)
'''
X = np.array([1,2,3])
foo(X)
'''
<class 'numpy.ndarray'>
array([2, 1, 2])
'''
我们更喜欢找到f(x)的全局最小值,但通常必须满足局部最小值,我们希望它接近全局最小值。 找到全局最小值的一个不错的方法是通过随机初始化x0的位置,找到一些局部最小值,然后选择所发现的f(x)最小的最小值的x。 例如,函数f(x) = cos(3\pi x) / x在[0,1.3]中有两个最小值,其中一个是明显的全局最小值:
def f(x): return np.cos(3 * np.pi * x) / x
graphx = np.arange(.1, 1.1, 0.01)
graphy = f(graphx)
plt.plot(graphx, graphy)
plt.axis([0, 1.1, -4, 6])
plt.show()

如果函数有很多最小值/最大值或非常复杂,则可能没有简单的解析解。
有许多方法可以迭代地(即非解析地)找到函数最小值,但我们将使用一种众所周知的技术,称为梯度下降或最速下降法。
梯度下降
梯度下降需要一个起始位置,x0,优化函数,f(x)及其导数f'(x)。 回想一下,导数只是特定点处函数的斜率。 换句话说,当x从特定位置移动时,f(x)是上涨还是下跌,以及涨多少? 例如,x ^ 2的导数是2x,当x > 0时为正斜率,当x < 0时为负斜率。 梯度下降使用导数,迭代地选择x的新值,使我们越来越接近f(x)的最小值。 斜率的符号告诉我们最接近的最小值的方向(以x为单位)。 例如,这里再次是余弦图,这次显示了代表特定点的导数的多个向量。
注意,导数在最小值处为零,即平坦(对于最大值也是如此)。 请注意,导数(红色向量)指向最接近的最小值的相反方向。 如果我们想要朝最近的最小值方向移动,我们应该通过加上导数的相反数来调整x; 也就是说,我们应该从xi中减去导数,得到x_{i + 1}。 更新我们对x的估计来最小化f(x)的递归关系就是:
x_{i+1} = xi - η f'(xi)
其中η被称为学习率,我们将在下面讨论。 η f'(xi)一项表示我们采取的最小步骤的大小。
基本的算法为:
- 选取初始值
x0,让x = x0 - 让
x_{i+1} = xi - η f'(x_i)直到f'(x_i)=0
该算法非常简单,但在处理计算机的有限精度时,知道何时停止算法是个问题。 具体来说,没有两个浮点数真的相等。所以f'(x) = 0总是假的。通常我们会这样做:
abs(x_{i+1} - xi) < precision
精度是一些非常小的数字,如 0.0000001。 我们也可以测试一下
abs(f(x_{i+1}) - f(xi)) < precision
看看函数是否变平或者是否有一个非常小的垂直变化,和f(x_{i+1})是否正在回升。第三种方法是测试导数是否接近于零。 我尝试了所有这三个,并发现只检查x_ {i + 1}是否收敛是最好的(例如,它允许我具有非常高的学习率)。 第三种方法是有问题的,因为我们使用有限差分,这是对真实导数的不良近似。
我们采取的步骤按学习率η缩放。 Yaser S. Abu-Mostafa 有一些很棒的幻灯片和你应该看的视频。以下是他在幻灯片 21 中对学习率如何影响收敛的描述:
x的范围也会影响学习率。 这是一个非常复杂的挑剔的任务,那些在该领域经验丰富的人告诉我,这是一个挑选学习率,起始位置,精确度等等的艺术。 您可以从较低的学习率开始,然后将其调高,来确定您是否仍在收敛而不会在最小值附近摆动。 可以在数值秘籍中找到梯度下降和其他最小化技术的优秀描述。
使用有限差分来近似导数
有时,导数很难,昂贵,或者无法通过解析(符号)找到。 例如,某些函数本身就是迭代的,甚至是必须优化的模拟。f(x)可能没有闭式。 为了解决这个问题并减少输入要求,我们可以估算出特定x值附近的导数。 这样我们可以优化任何合理的表现良好的函数(左右连续性很好)。 我们的最小化器只需要一个起始位置和f(x),而不是f'(x),这使我们的用户的生活更加简单,我们的最小化器更加灵活。
为了近似导数,我们可以采取几种方法。 最简单的是比较。由于我们真的只需要一个方向,我们所要做的就是将当前的f(xi)与两个方向上的小步(h)进行比较:f(xi - h)和f(xi + h)。 如果f(xi - h) < f(xi),我们应该将x{i+1}移到xi的左边。 如果f(xi + h) < f(xi),我们应该向右移动x{i+1}。 这些被称为后向和前向差异,但也存在中心差异。 优秀的文章随机梯度下降技巧有很多计算梯度的实用信息......
使用斜率的方向是有效的,但不会很快收敛。 我们真正想要的是使用斜率的模,来使算法在陡的地方变快,浅的地方变慢,,因为它将接近最小值。 因此,我们应该使用变化的模或速率,而不仅仅使用有限差分的符号。 在递推关系中,用我们的(前向)有限差分替换导数,我们得到一个类似的公式:
x{i+1} = xi - η (f(xi+h) - f(xi)) / h , where f'(x) ~ (f(xi+h) - f(xi)) / h
为了简化操作,我们可以将步长h折叠到学习率η常数中,因为我们无论如何都要选择它。
x{i+1} = xi - η (f(xi+h) - f(xi))
当斜率越大时,步长越大,当接近最小值时,步长变小(因为该区域更平坦)。 Abu-Mostafa 在他的幻灯片中指出,η应该随坡度增加而增加,但我们保持固定并允许有限差分增加步长。 我们没有像他那样将导数/差分归一化为单位向量(参见他的幻灯片)。
一个实现
我们的目标是使用梯度下降来最小化f(x) = cos(3 pi x) / x。 为了增加找到全局最小值的几率,我们可以使用标准 python 的random.uniform(),在[0.1,1.3]范围内选择一些随机起始位置,并对所有这些位置执行梯度下降。为了观察我们的最小化器,我们最终将绘制x的轨迹,表明我们的梯度下降所采取的步骤。 下面是两个示例下降,其中显示了x和f(x)值以及最小值:
回想一下我们的平方根讲义,我们有一个迭代方法的基本大纲:
x_prev = initial value
while True:
x_next = function-giving-next-value(x_prev)
if abs(x_next - x_prev) < precision:
return x_next
为了实现我们的梯度下降,我们只需用xi - η (f(xi+h) - f(xi))替换function-giving-next-value:
def minimize(f, x0, eta, h, precision):
x = x0
while True:
prev = x
finite_diff = f(x + h) - f(x) # division by h rolls into learning rate
x = x - eta * finite_diff # decelerates x step as it flattens out
if abs(x - prev) < precision:
return x # x is coordinate where f(x) is a minimum
选择一个合适的步长值h,对于通过有限差分获得良好的导数近似非常重要,但这足以避免因精度不足而导致的错误结果(在计算机中减去两个浮点数会使精度低于原始数字)。 您希望该数字足够小,以便您的算法不会在最小值附近振荡。 如果η太大,则递归关系将计算有限差分,使得x{i + 1}跳过函数的另一个最小值。 我们还必须选择学习率η,这样我们可以走得尽可能快,但不要太快,以至于x{i + 1}来回越过最小值。 当我把学习率提高太多时,我看到算法在振荡:
...
f(2.470932790352) = -0.109488272790 , delta = -0.39601471581233022023
f(2.099265953523) = 0.282655040497 , delta = 0.39214331328753893047
f(2.474216657712) = -0.097250083113 , delta = -0.37990512360965078553
f(2.100529851383) = 0.277900324077 , delta = 0.37515040718930170449
f(2.478172114663) = -0.082429682649 , delta = -0.36033000672594917013
...
这里有一些不错的参数以及我们如何调用最小化函数:
import random
ETA = 10
h = 0.0001
PRECISION = 0.0000001 # can't be too small as f(x)-f(xprev) prec is low
def f(x): return np.cos(3*np.pi*x) / x
x0 = random.uniform(.1, 1.2)
minx = minimize(f, x0, ETA, h, PRECISION)
print(f"f({minx}) = {f(minx)}")
# f(0.9886073570106972) = -1.0056986067028884
为了帮助您了解程序正在执行的操作,您可以打印x,f(x)以及您认为有助于查看程序如何探索曲线的任何其他值。在循环中:
print(f"f({x:.12f}) = {f(x):.12f} delta = {delta:.20f}")
显示最小化轨迹
如果我们可视化它,而不是打印出来,那么理解最小化函数所采用的路径就非常简单了。 我们需要的第一件事是该函数的一个版本,它保留了所有x值的轨迹:
def minimize_trace(f, x0, eta, h, precision):
tracex = [x0]
x = x0
while True:
prev = x
finite_diff = f(x + h) - f(x) # /h rolls into learning rate
x = x - eta * finite_diff # decelerates x step as it flattens out
tracex.append(x)
if abs(x - prev) < precision:
return tracex
接下来,我们导入 matplotlib 库以进行绘图,然后定义以下展示函数。
import matplotlib.pyplot as plt
def viz_trace(x0, minimizer):
# Plot the damped sine curve
graphx = np.arange(.1, 1.3, 0.01)
graphy = [f(x) for x in graphx] # or just f(graphx)!
plt.plot(graphx, graphy)
# Minimize and get trace of x locations
tracex = minimizer(f, x0, ETA, h, PRECISION)
# Plot the trace
tracey = [f(x) for x in tracex]
plt.scatter(tracex, tracey, color="darkred", marker='o', s=5)
# Add some text describing trace
plt.text(0.3, 4.5, f"f(x={tracex[-1]:.5f}) = {f(tracex[-1]):.5f}",
fontsize=14)
plt.text(0.3, 3.7, f"steps = {len(tracex)}", fontsize=14)
plt.tight_layout()
plt.savefig("img/cos-trace-2minima-another.svg")
plt.show()
为了测试该例程,我们传递一些随机的起始点并让它显示轨迹:
# every time you run this, it'll start at new initial location and find nearest minimum
import time
random.seed(int(round(time.time() * 1000))) # get new pseudo-random sequence each time
x0 = random.uniform(.1, 1.3)
viz_trace(x0, minimize_trace)

使用符号导数
比起有限差分的解,使用符号导数,我们可以做得更好。我们可以使用 Wolfram Alpha 或其他工具对任何函数做这件事。 理论上有限差分很好,但它们对于有限精度浮点数(即在计算机上)不起作用。 减法会破坏精度,尤其是当操作数非常相似时。
d/dx(cos(3pi x)/x) = (-3pi x sin(3pi x) + cos(3pi x))/x^2
def minimize_dx(f, x0, eta, h, precision):
tracex = [x0]
x = x0
while True:
prev = x
dx = - (3 * np.pi * x * np.sin(3*np.pi*x) + np.cos(3 * np.pi * x)) / x**2
x = x - eta * dx # decelerates x step as it flattens out
# if len(tracex)<300:
# print(f"x={x:.3f}, dx={dx:.3f}")
tracex.append(x)
if abs(x - prev) < precision:
return tracex
ETA = 0.001
random.seed(int(round(time.time() * 1000))) # get new pseudo-random sequence each time
x0 = random.uniform(.1, 1.3)
viz_trace(x0, minimize_dx)

总结
许多数学计算问题需要迭代解。 其中最重要的是最小化误差函数来训练机器学习模型。 好消息是,这里描述的梯度下降方法,遵循我们在近似平方根时看到的简单迭代方法。 下一步是将梯度下降扩展为两个变量,您将作为一个项目来解决回归(直线拟合)问题。
四、用于计算和优化的迭代式方法
5.1 使用 bash 走向胜利
译者:飞龙
目标
- 启动 bash shell
- 设置工作目录
- 列出该目录中的文件
- 从 shell 启动 Python
- 打印 Python 程序的命令行参数
这是一个有趣的来自麻省理工学院的终端游戏。
描述
UNIX shell(在我们的例子中是bash)是一种交互式领域特定语言,用于控制和监视 UNIX 操作系统,包括进程,设备,ram,cpu,磁盘等。它也是一种编程语言,尽管我们会主要用于移动文件,执行命令等。如果必须使用 Windows 机器,请安装 UNIX shell,使用 VMWare 安装 Linux,或启动 UNIX 盒子和 Amazon Web 服务。
您需要熟悉 UNIX 命令行,因为我们将使用命令行控制所有云计算设施。
当您第一次启动Terminal.app,或使用您拥有的任何 Linux 应用程序启动 shell 时,您将看到简单的美元提示或更复杂的内容,显示当前目录或当前机器等:
$
shell 只是一个像 Python 一样的交互式解释器:
>>> print "hello"
hello
>>>
在 bash 中做同样的事情:
$ echo "hello"
hello
$
shell 有许多状态变量,其中一个是当前工作目录。 我们执行的大多数命令都是相对于此工作目录的。 您可以使用以下命令打印出当前工作目录:
$ pwd
/Users/parrt/github/msan501/notes
$
通过输入可能带参数的命令来执行命令并按回车后,您会收到提示。它为另一个命令准备好了。
任何以/开头的路径表示它是从磁盘目录体系的根开始的绝对路径。 (您可以在维基百科深入了解路径)。 所以其他任何东西都被认为相对于当前的工作目录。 例如,如果您当前的工作目录是/Users/parrt/github/msan501,那么ls notes将为您提供/Users/parrt/github/msan501下面的notes目录的目录列表。 这是一个有用的图表,显示了名为view.py的文件的完全限定路径名的成分:
最常见的事情之一是使用cd更改当前工作目录:
$ cd /Users/parrt/github/msan501
$ pwd
/Users/parrt/github/msan501
$
您可以请求该目录中的文件列表:
$ ls
LICENSE README.md data/ labs/ notes/ projects/
请注意,~是一个特殊字符,表示“用户的主目录”。Mac 上对于任何YOURID都是/Users/YOURID的。 对我来说是/Users/parrt。
磁盘上有很多程序。 您只需使用其名称作为命令行中的第一个元素即可启动它们。 例如,以下是我们如何启动 Python shell:
$ python
Python 2.7.12 |Anaconda 4.2.0 (x86_64)| (default, Jul 2 2016, 17:43:17)
[GCC 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2336.11.00)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
Anaconda is brought to you by Continuum Analytics.
Please check out: http://continuum.io/thanks and https://anaconda.org
>>>
那当然给了我们一个新的提示符,>>>,这是 Python 用的。 我们可以通过按下 control-D(这意味着“文件末尾”)退出 Python。 当我们退出 Python 时,它将把我们带回 shell 和$提示符。
命令名称后面的任何内容都被视为该命令的参数。 例如,以下是获取特定目录的目录列表方式,即使我们当前不在该目录中:
$ ls /bin
[* csh* echo* ksh* mkdir* rcp* stty* wait4path*
bash* date* ed* launchctl* mv* rm* sync* zsh*
cat* dd* expr* link* pax* rmdir* tcsh*
chmod* df* hostname* ln* ps* sh* test*
cp* domainname* kill* ls* pwd* sleep* unlink*
语法通常是:
command arg1 arg2 arg3
这类似于 Python 中的以下函数调用:
command(arg1, arg2, arg3)
另一个例子,这是我们执行特定的 Python 脚本而不是进入 Python 交互式模式的方式:
$ python myscript.py
... any output from the script ...
当然,我们经常希望将参数传递给 Python 脚本本身。 这些参数紧跟着提供给python命令的脚本名称。 例如,这是一个简单的脚本,它打印脚本的任何参数(让我们把它放到args.py):
import sys
print "args:", sys.argv[1]
我们这样运行脚本:
$ python args.py hi mom
args: ['args.py', 'hi', 'mom']
$
5.2 使用 git 版本控制工具
原文:Introduction to git and revision control
译者:飞龙
动机。 每个商业开发人员都在工作时使用版本控制。 您将遇到的每家公司都使用它。 仅仅因为这个原因,您需要学习版本控制来在商业环境中发挥作用。 在本课程中,您还将使用版本控制(一种名为git的系统)来提交您的作品。
对于我们在 MSAN 中的目的,我们将忽略程序员常规使用的大多数非平凡功能,例如分支和合并。 Git 非常复杂,如果没有优秀的github.com,它将不是我的首选。
在本实验中,目标是基本了解版本控制和 git / github 的工作原理。 我们将借此机会,通过开始使用图像项目仓库来学习主要的 git 命令。
这是一个互动式 git “游戏”,来帮助您学习。 网上有很多课程资料。
版本控制入门
让我绕个弯子,你应该备份你的笔记本电脑。想象一下你的笔记本电脑丢了。 你的所有工作将在哪里?不见了。 我推荐 Carbonite 或 Mozy。 就个人而言,我的计算机旁边有一个本地 Timemachine OS X 备份硬盘,每小时拍摄一次快照。 它跟踪与前一个快照的差异,而不是再次复制整个硬盘驱动器(缓慢/浪费)。 然后,我使用 Carbonite 进行异地云备份,当我晚上睡觉时备份。
使用这种多层备份策略,是思考程序员如何使用版本控制的好方法。 git 有点像 Time Machine,一个本地备份,github.com 有点像 Carbonite 异地云备份。 版本控制系统和备份系统之间的区别在于,我们指示版本控制系统什么时候生成快照。 每个快照应该是对文件所做的逻辑工作块。备份系统自动拍摄快照。
我们不仅告诉版本控制系统何时拍摄快照,我们还告诉它快照是什么。 您处理的每个项目都在一个目录中,与该项目关联的所有文件都位于该子树中的某个位置。 文件集称为仓库,在任何给定时间,我的计算机都有很多这样的存储库。
git存储库实例只是磁盘上的一个目录,但它也有一个.git(隐藏)目录。 该目录包含一个完整的数据库,其中包含存储库中发生的所有事情,因为它是使用git init创建的(或者因为你从 github 克隆了它)。 如果要丢弃存储库,只需从磁盘中删除整个子树。没有通知中央服务器。 每个存储库实例都是一个完整的副本,因此您可以拥有 10 个存储库版本,这些存储库位于不同目录中,从同一磁盘上的原始存储库克隆。
跟踪变化
与 Time Machine 备份一样,git 跟踪快照与上次请求快照时的差异。 每个快照都被称为提交(并且程序员将这些提交视为事务。)您应该请求提交来锁定逻辑工作块,例如功能添加或错误修复。 拥有完整的更改列表非常有用。例如,这是一个块,取自 ANTLR 存储库中我的提交,由 SourceTree git GUI 所示:

您可以返回并查看对存储库的任何提交所做的更改。 无论是使用 PyCharm 还是 git,我发现回顾最近的提交来查看哪些更改引入了错误非常重要。 有时我决定放弃一小部分正在做的事情并将文件翻转回旧版本。 如果你走错了路并希望恢复所有这些变化,git 可以很容易地做到这一点。 它甚至可以将存储库重置为某些早期提交的状态。
github.com 上的镜像仓库
继续这个类比,github.com 就像我使用的基于云的异地备份。 我们通常会在 github.com 上保留我们本地存储库的完整副本。与提交更改一样,我们还必须将本地存储库推送到 github。每次推送都可以确保完整的文件集和 git 更改数据库(在.git子目录中)备份到了 Github 上。 当然,作为这样的副作用,您现在拥有了您的工作的备份。 丢失你的笔记本电脑并不是灾难性的,至少就你的工作而言。
使用 github 的原因是我也可以访问你的存储库,而我无法访问你的笔记本电脑硬盘。 为了给项目评分,我将将您的存储库克隆到我的硬盘上。 如果你进行了更改,我可以拉取那些。我也可以编辑你的代码来发表评论,然后将我的存储库副本推回 github,然后你可以将这些更改拉回你的硬盘。 一切都是同步的,它不会错误地覆盖更改。 这是多个程序员之间的沟通方式,并且有点超出了这个介绍性的课程,但你至少应该在抽象中意识到这一点。
还有一种情况是,您可以使用 github.com 在两台不同的计算机上处理相同的软件,例如笔记本电脑和台式机。 拥有两台具有相同软件的计算机意味着它们具有副本。 这会使您可能覆盖软件的正确/最新版本。 或者,您会忘记您已经对笔记本电脑进行了更改,但现在已经在桌面上进行了一系列更改。 您在两台不同的计算机上进行了更改 解决问题可能很棘手且容易出错,所以我们push/pull到/从 github 来保持同步。
图像项目入门
您将收到一个指向 github 的链接,该链接是为图像项目创建存储库的邀请。 在您接受之后,它将为您提供指向私有存储库的链接(只有您和我可以看到存储库的内容)。 我的 github id 是parrt,所以我的 URL 是:
https://github.com/USF-MSAN501/images-parrt
我教的每个课程都将在 github 上拥有自己的组织,在这种情况下:https://github.com/USF-MSAN501。每个人在 github 的目录下都有一个存储库,每个项目一个。
我们的第一步是从 github 克隆空存储库到我们的本地磁盘上。从您的 github 存储库页面,复制 HTTPS URL,如下所示:
如果选择 SSH 版本,则需要我们为身份验证设置 SSH 密钥。这是你最后想要做的,但就目前而言,不要担心,只需使用 HTTPS。
克隆到你的笔记本
现在,打开终端或其他 bash shell 程序并创建一个目录,该目录将以有序的方式容纳所有 MSAN 项目。 我强烈建议您创建一个整体目录,在该目录下为每个课程创建一个目录。 从命令行看起来像这样:
$ pwd # print working directory
/Users/parrt
$ mkdir classes # make directory called classes
$ cd classes # change current working directory to classes
$ mkdir msan501 # create directory msan501 under classes
$ cd msan501 # jump into msan501
不要在您创建的任何文件名或目录中使用空格。 许多开源项目是在 UNIX 下开发的,UNIX 讨厌文件名中的空格。 如果你使用空格,东西会神秘地无效。
根据您设置 shell 的方式,您可能会看到$提示符左侧的当前工作目录。 这是我的提示符:
beast:~/classes/msan501 $
我的机器名称是beast,~是/Users/parrt的简写或者你的用户 ID。
现在我们有了一个合适的结构,是时候在/Users/parrt/classes/msan501下克隆你的存储库了:
$ git clone https://github.com/USF-MSAN501/images-parrt.git
Cloning into 'images-parrt'...
warning: You appear to have cloned an empty repository.
$ cd images-parrt/
$ ls
克隆后,在msan501下有一个名为images-YOURID的空目录。 您可以在此目录下完成所有工作。此目录称为存储库。从技术上讲,它不是空的,如果你在ls命令中使用-a选项,请求当前目录中的所有文件,你将看到 git 用来存储快照的子目录:
$ ls -a
./ ../ .git/
由 git 管理的文件
在 github 上创建存储库并在本地克隆它之后,你可以在 git 管理的目录下创建各种文件,但是 git 忽略它们直到你“添加”它们。 add命令基本上通知存储库它应该关心该文件。 你可以放置你想要的任何其他文件。 除非你“添加”它们,否则 Git 会忽略它们。
从 github 下载images-starterkit.ipynb到您的images-YOURID目录,并将其命名为images.ipynb。 大牛在一个命令中下载和重命名,不需要浏览器:
$ curl -o images.ipynb https://github.com/parrt/msan501/raw/master/projects/images-starterkit.ipynb
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 41311 0 41311 0 0 87895 0 --:--:-- --:--:-- --:--:-- 87895
$ ls
images.ipynb
如果没有安装curl,请使用brew进行安装。
制作快照(提交)
我们在目录中有一个文件,但是 git 不知道它应该跟踪它。 我们必须明确地将它添加到版本控制:
$ git add images.ipynb
$ git status
On branch master
Initial commit
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: images.ipynb
...
此时,我们可以自由添加更多文件或更改该文件。 当我们想制作快照时,我们提交更改:
$ git commit -a -m 'initial add'
[master (root-commit) b40aca4] initial add
1 file changed, 9 insertions(+)
create mode 100644 images.ipynb
-m是在命令行后面指定提交消息的选项。 您可以放心地忽略-a的含义,但请确保始终在提交命令中使用它。
在提交之前,文件更改位于所谓的暂存区域中。 这有点像把所有东西放在一个盒子里(添加),然后邮寄盒子(提交)。
推回到 github
每次执行提交时,都会创建文件当前状态的快照。 Git 跟踪每次提交的更改集和提交消息,我们可以轻松检索:
$ git log
commit b40aca41ed4b94731a2ec87e9c11a60a1a4ef234
Author: parrt <parrt@cs.usfca.edu>
Date: Fri Jun 30 12:50:42 2017 -0700
initial add
在您明确推回到 github 之前,github 不知道您在本地磁盘上对存储库进行了更改。 Git 知道原始存储库的来源,但它不会在提交时自动将任何内容推送到 github。 Commit 用于本地存储库,push 用于与远程存储库同步。远程存储库称为来源(origin)。 使用以下命令询问 git 它认为来源是什么:
$ git remote -v
origin https://github.com/USF-MSAN501/images-parrt.git (fetch)
origin https://github.com/USF-MSAN501/images-parrt.git (push)
好吧,让我们将我们的更改(我们添加一个文件)推回给 github:
$ git push origin master
Counting objects: 3, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 408 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To github.com:USF-MSAN501/images-parrt.git
* [new branch] master -> master
该消息中有很多内容,但如果没有错误,那么你很高兴。 master就是我们所说的分支,你将始终在master分支中工作。
在你对 git 和 github 非常熟悉之前,你应该总是检查 github 上的远程存储库来验证你的推送是否成功。 刷新你的https://github.com/USF-MSAN501/images-parrt URL 的版本,它应该显示文件images.ipynb(以及你推送的任何其他文件)。 一旦下载,解压缩,“添加”图像项目 zip,并推回到 github,您的 github 页面应如下所示:
确保您要为项目提交的所有文件都在本地正确提交,然后再推回到 github!
做出更改
您可以进行更改并使用其他提交将其锁定。(确保在git commit上使用-a选项。)
...change existing file images.ipynb from jupyter lab...
$ git commit -a -m 'what I did to images notebook'
删除文件
删除由 git 跟踪的文件也被视为更改,但您也可以显式使用git rm文件名,这就是我所做的。
$ git rm foo.py
$ git commit -a -m 'I do not need foo anymore'
检查仓库的差异
如果您进行了更改并想知道它与当前存储库版本的不同之处,请使用diff:
$ ... tweak images.ipynb ...
$ git diff images.ipynb # kinda sucks with notebooks
...
还原
如果你搞砸了并且想丢弃自上次提交以来所有东西,请进行硬重置(确保使用--hard选项):
$ ... tweak whatever you want ...
$ git reset --hard HEAD
这会丢弃自上次提交以来的所有更改。 如果您只想将未提交的更改还原为单个文件,则可以运行以下命令:
$ git checkout -- filename
我认为他们把这种有趣的破折号选项称为“稀疏模式”。(怎么叫都随便。)
5.3 在 Amazon Web Services 上启动虚拟机
原文:Launching a Virtual Machine at Amazon Web Services
译者:飞龙
本实验的目的是教您在 Amazon Web Services 上创建 Linux 机器,登录并将一些数据复制到该机器。
以下是我为 MSAN692 制作的视频,其中显示了如何启动 AWS 实例并启动基于 Python 的 Web 服务器。
讨论
登录 AWS 并转到 AWS 控制台并单击“EC2”链接。
单击“启动实例”,这将启动过程,在云中创建虚拟机。 实例只是一个虚拟机。
选择“Amazon Linux 2 AMI”条目,该条目应该是第一个。 这是一个常用的“映像”,它使得 Linux 机器包含许多有用的东西,从该列表中可以看到,例如 Python 和 MySQL。 在有人在 Linux 机器上正确安装软件之后,映像只是磁盘的快照。 这意味着我们不必在每次创建新机器时都安装软件。
选择实例类型t2.micro,它应该是列出的第一个机器类型。 这台机器功率非常低,但足以让你玩玩。 点击“查看并启动”。
这将打开一个屏幕,描述我们正在启动的实例的详细信息。 暂时忽略所有这些,只需点击右下方的“查看并启动”即可。 这会打开一个摘要页面,我们可以根据需要进行检查,但只需点击右下角的“启动”即可。
这将打开一个对话框来选择密钥对。 密钥对允许您安全地访问服务器并防止未经授权的访问。 第一次,您需要创建一个新的密钥对。 将其命名为您的用户 ID,然后单击“下载密钥对”。 它将下载userid.pem文件,这是您进入机器的安全凭据。将该文件保存在安全位置。如果丢失了,您将无法进入您创建的机器。 从现在开始,您可以重用该预先存在的密钥。
下载后,单击“启动实例”。 你应该看到类似的东西:
单击i-...链接转到显示实例的 EC2 控制台。
单击您的实例,您应该会在底部看到一个描述框。 查找“公共IP”地址,在这种情况下为54.196.174.210:
我们必须等到状态停止显示“正在初始化”,这可能需要几分钟。
连接到远程服务器
单击页面顶部的“连接”按钮,将弹出一个对话框,告诉您如何连接到服务器。 您想要使用“独立 SSH 客户端”链接来连接(Java 现在是浏览器中的安全风险,因此我们无法使用该选项。)在内部您将看到连接到您的计算机所需的ssh命令。 如果您有 Windows,则会显示一个链接,向您展示如何使用名为 PuTTY 的 SSH 客户端。
在我们连接之前,我们必须确保计算机上的每个人都看不到安全文件(其他用户)。 否则 ssh 将不允许我们连接,因为安全文件不安全:
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: UNPROTECTED PRIVATE KEY FILE! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Permissions 0644 for '/Users/parrt/Dropbox/licenses/parrt.pem' are too open.
It is required that your private key files are NOT accessible by others.
This private key will be ignored.
bad permissions: ignore key: /Users/parrt/Dropbox/licenses/parrt.pem
Permission denied (publickey).
哇!执行这个:
$ cd ~/Dropbox/licenses
$ ls -l parrt.pem
-rw-r--r--@ 1 parrt parrt 1696 Aug 4 15:15 /Users/parrt/Dropbox/licences/parrt.pem
要修复权限,我们可以使用您的操作系统具有的 GUI 来“显示文件信息”,或者从命令行执行以下操作:
$ cd ~/Dropbox/licenses
$ chmod 400 parrt.pem
它像这样改变了权限:
$ ls -l parrt.pem
-r--------@ 1 parrt 501 1696 Aug 1 12:12 /Users/parrt/Dropbox/licenses/parrt.pem
如果您不明白究竟发生了什么,请不要担心。 它基本上是说,该文件仅仅对我,当前用户,来说可读可写,任何其他人没有权限。
对于 mac 和 linux 用户,我们将直接使用命令行中ssh命令。它将是这样的:
ssh -i parrt.pem ec2-user@54.196.174.210
当然,您必须提供userid.pem文件的完整路径名。 所以我需要输入:
ssh -i ~/Dropbox/licenses/parrt.pem ec2-user@54.196.174.210
尝试再次连接,它现在会警告您之前从未连接过该机器。 同样,这是一项安全措施。 你可以在这里简单地说“是”。
$ ssh -i parrt.pem ec2-user@54.196.174.210
The authenticity of host '54.196.174.210 (54.196.174.210)' can't be established.
ECDSA key fingerprint is SHA256:BYGiV2qRthhw52HQni5vnoRtiT16cplmdbXAuXqQdqc.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '54.196.174.210' (ECDSA) to the list of known hosts.
Last login: Thu Aug 4 20:04:07 2016 from sentinel.cs.usfca.edu
__| __|_ )
_| ( / Amazon Linux AMI
___|\___|___|
https://aws.amazon.com/amazon-linux-ami/2016.03-release-notes/
8 package(s) needed for security, out of 17 available
Run "sudo yum update" to apply all updates.
[ec2-user@ip-172-30-0-97 ~]$
$是你的提示符,就像你在本地机器上使用终端 /shell 一样,但是你给远程服务器而不是本地机器发出命令。
多个 shell
你可以从Terminal.app点击command-T并获得多个标签,一个用于远程主机,一个用于本地/笔记本电脑:
或者在Terminal.app中使用command-N获取两个窗口。
通过在两台计算机上执行命令ls /,来验证远程计算机不是您的笔记本电脑:
上传数据或代码
要将数据提供给服务器,如果文件很小,可以剪切和粘贴。 例如,将以下数据剪切并粘贴到该远程计算机上主目录中名为coffee的文件中。 首先从这些笔记中复制这些数据:
3 parrt
2 jcoker
8 tombu
然后在远程计算机上启动nano编辑器:
[ec2-user@ip-172-30-0-97 ~]$ nano coffee
将数据粘贴到该文件中并键入control-X,然后单击“是”以保存文件。 退出后应该返回到提示符。
[ec2-user@ip-172-30-0-97 ~]$ cat coffee # print it back out
3 parrt
2 jcoker
8 tombu
$
对于较大的文件,我们需要使用安全 copy scp命令,与安全 shell ssh具有相同参数结构。在笔记本电脑上运行另一个 shell。 下载数据文件cheese_deaths.csv,并存储在笔记本上,您当前正在处理笔记本命令行的目录中。现在,在笔记本电脑上的 shell 中,使用以下命令(类似于ssh)将其复制到远程计算机:
$ scp -i ~/Dropbox/licenses/parrt.pem cheese_deaths.csv ec2-user@54.196.174.210:~ec2-user
cheese_deaths.csv 100% 160 6.9KB/s 00:00
$
不要忘记该行末尾的~ec2-user。 该命令的一般形式是:
scp -i your-pem-file file-to-copy user@machine-name:directory-on-remote-machine
从连接到远程服务器的 shell,请求目录列表,您将看到新文件:
[ec2-user@ip-172-30-0-135 ~]$ ls
cheese_deaths.csv
[ec2-user@ip-172-30-0-135 ~]$ head cheese_deaths.csv
years,cheese,deaths
2000,29.8,327
2001,30.1,456
2002,30.5,509
2003,30.6,497
2004,31.3,596
2005,31.7,573
2006,32.6,661
2007,32.7,809
2008,32.8,717
要退出远程服务器,请在$提示符下键入exit或使用^ D。 机器仍在运行,但您不再通过笔记本电脑连接它。
玩转你的实例,然后当你完成时终止你的实例,否则将继续向你收取使用该机器的费用。在 AWS 控制台上右键单击实例,然后选择“实例状态”,然后选择子菜单“终止”。 它会警告您所有本地存储都将消失。 点击“是,终止”按钮。 完成后应该如下所示:
如果你点击“停止”,它会停止机器,但仍然会向你收费。 另一方面,这很有用,因为您可以重新启动该计算机而无需执行整个过程。 您的所有数据都将完整无缺。 如果您点击“终止”,它将把机器丢弃,您将不得不再次执行此过程来获得新机器。
五、常见编程工具
6.1 链表
原文:Linked lists
译者:飞龙
我们已经研究了 Python 内置的数组/列表,但它们并不总是最好的列表。 有时,我们会在列表的头部或中间插入和删除内容。 如果我们使用实现为连续数组(由内存中的连续单元组成)的列表来执行此操作,我们必须移动大量单元以为新元素腾出空间或填充删除所产生的空隙。
链接列表抽象列表的实现允许我们有效地插入和删除任何我们想要的东西。 这种灵活性是以更多内存为代价的。
研究链表的另一个原因是链表节点的概念很容易扩展,以创建分层的父子数据结构,称为树,然后是图(跟踪任何节点->节点的关系)。
TODO:使用 graphviz 或其他东西来可视化结构。 将树的讨论移动到单独的页面。 这个讲座进行得很快,让一些学生感到困惑,花了大约 1.5 个小时都没有讲到树。 让学生将几个笔记连在一起并将其可视化。 从元组开始然后执行 graphviz。
链表与数组的隐喻
想象一下,我想在课堂上点名。 由于每个人都坐在彼此旁边,即连续,我可以通过向左或向右看,从一个人指向下一个人。这就是列表的工作方式,就像连续的块一样。
链表要求每个人不仅要记住他们的名字,还要记住他们的右边(下一个指针)。 只要我记得列表中的第一个人(头部),我可以稍后给那个人打电话并询问他们的名字。 然后我可以让他们引用下一个排队的人。 即使人们分布在整个大陆或随意重新分布他们所在的位置,这也是有效的。 这些元素不要求是连续的,因为列表中的每个节点都具有到达下一个人所需的信息。
链表实现将下一个指针与每个列表值相关联。 我们称这些东西节点,通常是:[value, next]或(value, next)。 我们还维护指向列表的头部的指针,有时是列表的尾部。
最简单的列表有一个元素,next指针/引用不指向任何东西。
users = ("parrt", None)
这是一个有两个元素的链表:
users = ("parrt", ("tombu", None))
三个元素的:
users = ("parrt", ("tombu", ("afedosov", None)))
在实践中,我们将使用列表而不是元组,因为元组是不可变的。 我们希望能够更改节点的next指针:
a = ["parrt", None]
b = ["tombu", None]
c = ["afedosov", None]
users = a # points to first node of list
a[1] = b # first node's next points to 2nd element
b[1] = c
链表的最基本实现只是一个头部指针(这里我使用users作为特定链表)。 创建一个空链表只需要users = None。
当然,我们可以存储我们想要的任何类型的对象,而不仅仅是字符串。 我们可以存储数字甚至其他列表!
支持代码
让我们添加一些支持代码,以便更轻松地构建和操作链表。 它使用 Python 中的一些面向对象的编程语法,如果需要,可以忽略它。 基本上,我正在定义一个名为Node的对象(一个数据聚合),它将有两个字段value和next。对于某些元组/列表p,访问字段名称要比p[0]和p[1]容易得多。
class Node:
def __str__(self):
return "(%s,%s)" % (self.value, str(self.next))
def __repr__(self):
return str(self)
def __init__(self, value, next=None):
self.value = value
self.next = next
有了这个定义,我们可以更自然地创建列表:
a = Node("parrt")
b = Node("tombu")
c = Node("dmose")
print(a, b, c)
# Now link them up
users = a
a.next = b
b.next = c
print(users)
'''
(parrt,None) (tombu,None) (dmose,None)
(parrt,(tombu,(dmose,None)))
'''
在头部插入节点
好的,我们假设我们用head = None创建一个空链表。 要在链表的头部插入一些东西,比如hi,有两种情况:空列表和非空列表。 空链表是head == None的情况,即链表的初始条件。 一个非空的链表当然会让head指向一些元组。 这两种情况都可以用同样的方式处理:
head = Node('hi', head)
这使得一个新节点持有新值hi和一个next指针,指向旧的头元组(即使是None)。 最后,它设置head指针来指向新元组。
在中间插入更复杂。 我们需要找到一个节点,在它之后我们要插入一些东西。 然后我们挂载新的节点。例如,要在tombu和dmose之间插入一些东西,我们创建一个新的节点,其next指针指向dmose的节点(tombu指向的东西)。然后 我们设置tombu的next指针来指向包含mary的新节点:
b.next = Node("mary",b.next)
users
# (parrt,(tombu,(mary,(mary,(dmose,None)))))
遍历链表
假设我们构建了一个链表,我们如何遍历这个列表呢? 使用数组,我们只需访问第 i 个值并沿列表移动 i。 要遍历链表,我们必须定义一个光标(通常称为p或q),我们可以将其视为一个手指,我们在链表中的节点之间移动。 以下是遍历链表并打印出值的代码模式:
p = users
while p is not None:
print(p.value)
p = p.next
'''
parrt
tombu
mary
mary
dmose
'''
递归版本
还有另一种遍历数据结构的方法,我们称之为递归。 我们熟悉递归关系的概念。 在这种情况下,请注意,从链表中的任何节点开始,链表的其余部分看起来像链表。 它就像一个分形,无论你放大多少,它仍然看起来像一个分形。
递归函数调用是调用它周围函数的调用。 最明显的一个是:
def f():
f()
但是,这不是很有用,因为它是一个无限循环。 函数f()做的第一件事是调用自己,从而产生循环。 我们通常有一个终止条件,告诉它何时停止递归。
以下是我们递归遍历列表的方式:
def walk(p):
if p is None: return
print(p.value)
walk(p.next)
walk(users)
'''
parrt
tombu
mary
mary
dmose
'''
练习
创建一个名为tostr的函数,给定一个链表head作为参数,它返回一个括号包围的字符串,如['parrt', 'tombu', 'dmose']。 提示:将此问题转化为我们知道如何解决的问题。 只需将元素添加到常规 Python 列表([]),就像在链表中找到它们一样; 然后使用str()返回该列表的字符串表示形式。
def tostr(head):
p = head
values = []
while p is not None:
values.append(p.value)
p = p.next
return str(values)
tostr(users)
# "['parrt', 'tombu', 'mary', 'mary', 'dmose']"
Exercise
Implement method len(head) that counts and returns the number of elements in the linked list referred to by head. Return zero if the list is empty. Hint: This is just adding a counter-accumulator pattern to the list walking pattern.
def len(head):
p = head
n = 0
while p is not None:
n = n + 1
p = p.next
return n
len(users)
# 5
我们还可以制作该函数的递归版本:
def rlen(head):
if head is None: return 0
return 1 + rlen(head.next)
rlen(users)
# 5
练习
实现名为getitem(head, i)的方法,返回列表head中的第i个节点,从零开始。 提示:将一个计数器累加器循环与一个搜索循环(查找等于i的计数器)和列表遍历模式相结合。 考虑这一点的另一种方式是,它在前一个长度练习中添加搜索模式。
def getitem(head,j):
"Return ith node in the list starting from 0 or return None if invalid index"
i = 0
p = head
while p is not None:
if i == j:
return p
i = i + 1
p = p.next
return None
print(getitem(users, 0).value)
print(getitem(users, 1).value)
print(getitem(users, 2).value)
print(getitem(users, -1))
print(getitem(users, 999))
'''
parrt
tombu
mary
None
None
'''
删除第一个元素
要删除列表的第一个节点,我们所要做的就是将head指向第一个节点的next,注意检查空列表条件:
if head is not None:
head = head.next
树的一次尝试
一旦我们熟悉了从一个节点到另一个节点的指针/引用的概念,我们就可以扩展节点来拥有两个指针,而不仅仅是一个next指针。二叉树由具有left和right子指针的节点组成;这些指针中的一个或两个可以是None。 如果每个树节点最多只有一个子节点,则树会简化为链表。
树是分析中非常常见的数据结构,尤其是机器学习。 例如,有一种称为层次聚类的聚类形式,它构造相关元素组的树。 然后,最强大的机器学习模型之一被称为随机森林,它由一组决策树组成,它们协同工作来提供非常强大的预测器或分类器。
我们从正常的生物学角度颠倒树木。 树的根是单个节点,并在图的顶部绘制。 树的叶子是没有子节点的子节点。
下面是一些树节点的支持代码,除了树节点(数据聚合)包含value,left和right字段,你可以安全忽略它。
class TNode:
def __str__(self):
return "(%s,%s,%s)" % (self.value, str(self.left), str(self.right))
def __repr__(self):
return str(self)
def __init__(self, value, left=None, right=None):
self.value = value
self.left = left
self.right = right
为了测试它,我们可以像链表节点一样创建节点,然后根据需要将它们连接起来:
myroot = prez = TNode("Paul")
provost = TNode("Don")
som = TNode("Liz")
cas = TNode("Marcelo")
prez.left = provost
provost.left = som
provost.right = cas
prez
# (Paul,(Don,(Liz,None,None),(Marcelo,None,None)),None)
from lolviz import *
treeviz(prez)
树的遍历
虽然最常见的是使用while循环来遍历链表,但最常见的是使用递归来遍历树。 例如,以下是我们如何前序打印树中的所有元素,这意味着在它的子节点之前打印它的值。
def walk(t):
if t is None: return
print(t.value)
walk(t.left)
walk(t.right)
walk(prez)
'''
Paul
Don
Liz
Marcelo
'''

浙公网安备 33010602011771号