掌握 Git:修改提交
原文:https://towardsdatascience.com/mastering-git-amending-commits-f46c2afc9508
如何使用 git amend 更改您之前的提交
照片由 Praveen Thirumurugan 在 Unsplash 拍摄
Git 是所有数据科学家和软件工程师都应该知道如何使用的工具。无论您是独自从事一个项目,还是作为一个大型分布式团队的一部分,了解如何使用 Git 从长远来看都可以为您节省大量时间。git 的关键功能之一是能够撤销、重做或编辑已经做出的更改。当第一次并不总是如预期的那样工作时,这可以用来确保在每次提交中具有清晰定义的变更的干净的提交历史。实现这一点的工具是git --amend
,它使您能够修改您的最后一次提交,而无需在原始提交的基础上进行新的提交。这样做的好处是,您可以通过添加或删除文件和/或修改提交消息来改变上次提交的内容。
更改提交消息
使用--ammend
函数的一个最简单的方法是简单地改变最后一次提交的消息。当您过早提交、临时提交或在撰写消息时忘记遵循某些团队约定时,这可能是有益的。更改提交消息可以确保它准确地描述了以前的提交,以便其他人可以确切地知道该更改包含什么。
要修改消息,当当前没有暂存任何内容时,您只需运行:
git commit --amend
这将打开您链接到 git 的编辑器,允许您修改之前的提交消息。如果您甚至不想打开编辑器,更改整个提交消息,您可以简单地添加-m
选项,这将允许您在同一个命令中指定新消息。这可以通过以下方式实现:
git commit --amend -m "New commit message"
更改提交的文件
在某些情况下,您想要更改的不仅仅是附加到最后一次提交的消息。这可能是因为您忘记存放文件,在最初的提交中出错,或者因为一些额外的小更改应该在最后一次提交而不是新的提交中捆绑。这也可以通过--amend
功能来完成。
为此,你所要做的就是简单地使用git add file_name
将文件暂存起来,然后使用git commit --amend
。典型的工作流程可能采取以下形式:
git add some_amazing_python.py
git commit -m "Some amazing work"#edit the files to remove an unecessary import
#and make it cleanergit add some_amazing_python.py
git commit --amend --no-edit
第二次升级文件时,使用git commit --amend
会将第二次更改与第一次更改捆绑在一起。
您还会注意到,--no-edit
标志被添加到了git commit -amend
调用中。这样做是因为我们不希望在这次更改中更改提交消息,只更改文件。当然,如果你想改变消息,你可以移除这个标志来打开一个编辑器,或者使用-m
选项来改变消息而不打开编辑器。
除了简单地向现有文件添加更改之外,您还可以从文件中删除上次提交的更改。这可以通过git rm
功能而不是git add
来删除文件。
警告!!!
git commit -amend
通过删除以前的提交并创建一个新的提交来工作,如下图所示:
作者图片
因此,使用git commit --amend
会改变存储库的历史。如果资源库已经公开共享,比如在 GitHub 上,并且其他开发人员已经在现有提交的基础上进行构建,这可能会使事情变得困难和混乱。这是因为在公开共享和使用时更改提交的历史将会产生合并冲突,这在将来很难解决。作为开发人员,您不想处理的一件事是合并冲突。所以如果改动已经公开,就不要用--amend
。
唯一的例外是,如果你正在做你自己的分支,别人还没有接触过。这应该可以避免合并冲突的问题,但是在推送远程存储库时仍然会给自己带来困难。这是因为本地存储库和远程存储库之间的历史将会不同,这意味着必须通过强制提交到远程存储库来解决这个问题。这可以使用git push -f
来完成,其中-f
强制提交并用本地提交历史覆盖公共提交历史。只要确保这是在正确的分支上完成的!
结论
git --amend
是一个非常有用的命令知道吗!如果遗漏了什么,它允许您更改提交消息,或者调整前一次提交中的更改。这可以为您和其他开发人员确保一个清晰的提交历史,并且在犯小错误的时候可以派上用场。只要确保你没有在已经公开的提交中使用它,或者当你在之前的提交中改变代码的行为时(这应该是一个新的!).
来源
[1]https://www.atlassian.com/git/tutorials/rewriting-history
[2]https://stack overflow . com/questions/179123/how-to-modify-existing-un pushed-commit-messages
如果你喜欢你所读的,并且还不是 medium 会员,请使用下面我的推荐链接注册 Medium,来支持我和这个平台上其他了不起的作家!提前感谢。
https://philip-wilkinson.medium.com/membership
或者随意查看我在 Medium 上的其他文章:
掌握 Git: Git 精选
原文:https://towardsdatascience.com/mastering-git-git-cherry-pick-fbeb23eea04d
如何将单个提交精选到您当前的分支
由 Praveen Thirumurugan 在 Unsplash 上拍摄
Git 是所有数据科学家和软件工程师都应该知道如何使用的工具。无论您是独自从事一个项目,还是作为一个大型分布式团队的一部分,了解如何使用 Git 从长远来看都可以为您节省大量时间。git 的一个关键功能是能够从任何地方选择任何提交并复制到当前分支的头部。这可以通过命令git cherry-pick
来完成,该命令可用于撤销更改、引入新功能、解决 bug 或确保提交附加到正确的分支。这当然是有用的,但你必须小心,不要在合并或重定基础可能更有益的情况下过度使用它。
樱桃采摘
git cherry-pick
是一个有用的工具,它允许您从存储库中的任何地方复制一个提交,并将其附加到当前分支的头部。这使得它成为一个非常有用的工具,可以以多种方式使用,但避免过度使用通常是最好的做法。这是因为该命令会创建重复的提交,这会造成混乱的历史记录,并且在大多数情况下,合并或重置会更有好处。那么什么时候应该使用它:
- 作为同事在不同的分支从事类似的工作。在这种情况下,您的同事开发的类或函数可以帮助您当前的进展或将来的系统集成。为了将这种功能引入到您当前的分支中,并避免代码的重复,您可以简单地挑选出那些允许您继续工作的特定功能。这将集成功能,特别是如果它们是前端和后端系统,同时也减少了将来可能出现的合并冲突的数量。
- 减少 bug。在某些情况下,当开发一个新特性时,你可能会发现一个来自另一个分支甚至主线的 bug(希望不是!).在这一点上,最好是尽快为客户获得一个 bug 补丁,这可能意味着在您当前的分支上开发补丁。在这一点上,您可以将您的变更精选到主分支或另一个分支中,以尽快解决 bug。
- 为了不失去功能。在某些情况下,新功能可能不会在您希望的时候合并到 main 中。在这些情况下,为了获得您想要的特性,最好使用精选来挑选该功能。这样就保证了这个特性不会丢失!
使用 git 精选
使用git cherry-pick
相对简单,因为您需要做的就是找到您感兴趣的提交引用,然后当您检查出您想要将功能引入的分支时,使用git cherry-pick commit_reference
。例如:
作者图片
如果在提交G
中有一些功能,在当前的特性分支中会很有用。您可以使用:
git cherry-pick G
这将改变提交历史,使得G
现在也将出现在当前头部的尖端,如下所示:
为了能够识别您想要挑选的相关提交及其相关的提交引用,您可以使用命令git log
或git reflog
,它们将向您显示提交历史。
附加功能
为了改进git cherry-pick
的工作,我们还可以添加标志来改变功能,使它对我们想要的更有用:
- 选项将导致 git 在应用精选操作之前提示提交消息。这将允许您更改提交消息,如果您想反映它在该位置正在做什么。
--no-commit
flag 将执行精选,但不是进行新的提交并将其添加到当前文件头,而是简单地移动文件内容,而不进行实际的提交。当您只想检查更改或查看更改如何与您的功能集成而不提交时,这可能很有用。-x
将在原始提交消息中添加一行“cherry picked from commit …”,用于无冲突提交。这在从公开可用的分支中挑选樱桃时会很有用。
您还可以通过指定git cherry-pick branch_name
来指定想要将最新提交移动到哪个分支。这将从当前分支获取最新提交,并将其放在指定分支的末端。
此外,如果您想同时挑选多个提交,您可以添加它们的提交引用,用空格分隔。尽管不建议这样做,因为理想情况下,您应该一次只复制一个提交。
结论
git cherry-pick
是一个强大的命令,如果正确使用,它会非常有用。它允许您将提交复制到当前分支的头部,当您想要共享功能但不想执行合并时,这对于解决在其他地方发现的错误或提交到错误的分支时非常有用。当合并或重定基础是更好的实践时,例如当多个提交可能被移动时,不应该使用它。您可以使用列出先前提交的git log
命令找到您想要移动的提交的提交标识。
来源
[1]https://www.atlassian.com/git/tutorials/cherry-pick
[2]https://www.gitkraken.com/learn/git/cherry-pick
如果你喜欢你所读的,并且还不是 medium 会员,请使用下面我的推荐链接注册 Medium,来支持我和这个平台上其他了不起的作家!提前感谢。
https://philip-wilkinson.medium.com/membership
或者随意查看我在 Medium 上的其他文章:
掌握 Git:“Git stash”
原文:https://towardsdatascience.com/mastering-git-git-stash-cf4042dca068
如何使用 git stash 来存储您不准备提交的更改
由 Praveen Thirumurugan 在 Unsplash 上拍摄
Git 是所有数据科学家和软件工程师都应该知道如何使用的工具。无论您是独自从事一个项目,还是作为一个大型分布式团队的一部分,了解如何使用 Git 从长远来看都可以为您节省大量时间。虽然 git 对于清楚地查看您对存储库所做的更改的历史很有用,但是在某些情况下,您还没有准备好提交您对历史所做的更改。当 git 告诉您需要提交或者您将丢失您的更改时,这可能是一个挑战。当您想要更改分支或者从远程存储库中提取更改时,Git 通常会这样做。那么你能做什么呢?git stash
来救援了!stash
命令允许您“隐藏”您还没有准备好提交的更改,这样您就可以保存更改,而不必将它们添加到历史中。这将允许您轻松地更改分支或从远程存储库中提取,而不必担心丢失您的更改。那么它是如何工作的呢?
Git Stash
git 的正常工作流程是编辑文件,准备变更,然后提交。这个工作流将创建一个清晰的提交历史,其中的工作明显地建立在彼此的基础上。它允许你进入你正在开发的特性的流程,做出清晰的改变,并且在一个编码会话中做出你通常的惊人的代码贡献。当然,这是理想的工作流程,但是在现实中,作为团队的一部分在工作环境中进行开发意味着您必须经常在一天中切换任务或职责。当您正在进行代码更改时,这可能会令人沮丧,因为您还没有准备好提交,但同时又不想丢失您的工作。您可能会被要求更改您正在处理的内容,因为出现了一个需要尽快解决的 bug,另一个开发人员要求您查看另一个分支,对您正在处理的分支进行了新的提交,您只想休息一下。此时,在您可以做任何其他事情之前,git 可能会告诉您对您所做的更改做一些事情。
如果您还不想创建新的提交(尽管您可以随时返回并编辑提交或消息),您可以使用git stash
保存未提交的更改。这个过程允许您处理存储库中的其他任务,而不会丢失到目前为止所做的更改。然后,当您想要恢复这些更改时,可以简单地重新应用这些更改,而不会影响提交历史。厉害!它是如何工作的?
使用 Git Stash
默认情况下,当您调用git stash
时,您对跟踪文件所做的所有更改都将被转移到一个存储中。此时,您当前的工作目录将阻止返回到上次提交后的阶段。这意味着在那之后你所做的所有更改都将被保存在一个存储库中,你可以在以后访问它。
你藏起来的东西会包含:
- 对暂存文件所做的更改
- 对跟踪文件所做的更改(但未转移)
但是您需要知道,它不会自动包含:
- 工作目录中尚未暂存的新文件
- 有意忽略的文件
这是您需要注意的事情,尤其是当您处理涉及新文件的变更时。为了包括这一点,您可以使用标志-u
,它将包括未被跟踪的文件,以及-a
,它也将包括被忽略的文件。
一旦你创建了隐藏,做了你需要在其他地方做的改变,然后回到你工作的最初的分支,你会想知道你如何能把那些隐藏的改变拿回来?有两种方法可以做到:
git stash apply
—将获取您存储在存储库中的变更,将它们应用到当前签出分支的工作目录中,并且将保持存储库的完整性。当您将更改拉至不同于最初开发的分支时,或者您正在编辑更改,但您可能希望保留最初的更改以备不时之需时,这很有用。重要的是,在它被清理之前,这个藏匿点在未来仍然存在,这意味着如果出现问题,你可以转换回原来的藏匿点。git stash pop
—还会将存储在存储中的更改应用到当前工作目录,但这将在应用更改后删除存储。这可以在你不在乎以后保留存储时使用,或者如果你计划对存储进行更改,并且不想在任何时候恢复到原始存储时使用。
这非常有用,尤其是对于存储您不准备与团队其他成员共享的变更。
然而,必须注意的是,这并不能代替将您的变更提交到存储库中。储藏只能适度进行。这是因为跟踪变更会变得很困难,尤其是在有多个 stashes 的情况下,因为它们的历史和它们包含的内容不像提交那样清楚。这意味着应该谨慎使用它们,只在需要的时候使用,否则更改应该提交!
附加功能
除了使用起来相对简单之外,git stash 是一个非常强大的工具,可以通过多种方式进行修改。
git stash --patch
(或使用-p
标志)将打开一个编辑器,允许你交互地选择你想要保存的变更和你想要保存在你的工作目录中的变更。当您不想隐藏所有的更改时,这很有用,因为您可能想要提交一些更改。git stash save "message"
将允许你添加一条消息到你的秘密物品中。当您有多个隐藏并且想要清楚地告诉变更包含什么或者它们为什么被隐藏时,这是特别有用的。如果你经常使用 stash,这是一个很好的实践,当你回去应用 stash 的变化时,它会使你的生活变得容易得多。git stash show
将显示您最近收藏的内容(文件更改的数量、添加的数量和删除的数量)。这有助于大致了解最新的存储内容,但是如果你想了解文件中的所有变化,你可以通过-p
,它将显示与git diff
相同的输出- 如果你已经藏了不止一次,
git stash list
会显示所有已藏物品的列表。这将显示隐藏来自哪个分支和他们的隐藏索引(你如何能在隐藏之间选择)。默认情况下,在分支和创建存储的提交之上,所有存储都被标识为“WIP”(正在进行的工作)。 git stash show stash@<index>
可用于显示特定存储的变化,例如git stash show stash@{2}
将显示第三个存储的变化。这可以扩展到前面的命令,比如git stash pop stash@{2}
,它将弹出第三个存储,而不是默认的最后一个存储更改。git stash drop
可用于从隐藏条目列表中删除单个隐藏条目。当您知道您在将来的任何时候都不会使用这些更改,并且保持您的 git 存储库没有任何未使用的更改时,这是非常有用的。git stash clear
将其扩展到删除所有隐藏的更改,但要谨慎使用,因为在此之后很难恢复任何隐藏!- 最后
git stash branch <branch_name>
可以用来从最新的存储中创建一个分支,并删除最新的存储。当您将存储应用到您的分支的最新版本之后出现冲突时,这可能是有用的(这可能发生,并且可能非常混乱!)
结论
如果 git 警告你不能做某件事,因为你没有提交修改git stash
可以帮助你!它的工作原理是将更改存储到您不准备创建提交的文件中,确保当您更改分支、发出拉请求或只想提交您已经完成的部分工作时,工作不会丢失。这有时会很有用,可以存放多份,但要确保不要过度使用!与提交不同,隐藏可能很难跟踪,而且它们会被全面地记录,所以当您准备好发布您的更改时,就提交吧!
来源
[1]https://www . atlassian . com/git/tutorials/saving-changes/git-stash
[2]https://git-scm.com/docs/git-stash
如果你喜欢你所读的,并且还不是 medium 会员,请使用下面我的推荐链接注册 Medium,来支持我和这个平台上其他了不起的作家!提前感谢。
https://philip-wilkinson.medium.com/membership
或者随意查看我在 Medium 上的其他文章:
掌握 Git:合并和重建基础
原文:https://towardsdatascience.com/mastering-git-merge-and-rebase-f2a7c5c348a9
它们是什么,如何使用它们?
照片由 Praveen Thirumurugan 在 Unsplash 上拍摄
Git 是所有数据科学家和软件工程师都应该知道如何使用的工具。无论您是单独从事一个项目,还是作为大型分布式团队的一部分,从长远来看,了解如何使用 Git 都可以为您节省大量时间。这是因为 git,以及更广泛的版本控制,允许您跟踪对代码库所做的更改,并与其他人有效地协作。你想逆转你所做的改变,用 git 很容易做到,你想从现有的代码中分离出一个新的特性,git,你想在你做改变的同时运行一个有效的产品,git,你想和其他人在同一段代码上工作,git 和 github!这真的很神奇。
学习其中一个重要的关键功能是合并和重组,将分支工作引入主线。阅读本文的大多数人都会在某个地方遇到过合并或重组(当然,有些比其他的更容易、更简洁)。这可能是因为你自己正在开发一个特性,你把它引入了你的主线,或者是其他人把他们已经完成的工作引入了一个共享的主线分支。这意味着理解 merge 和 rebase 是如何工作的,它们的区别是什么,以及如何使用它们来为自己服务,以便将来使用 git 时更加有效,这是非常重要的。
合并和重定基础之间的关键区别在于它如何处理代码历史,以及您是否需要新的提交。在合并的情况下,维护代码的历史,包括分支最初从主线分叉的点,并且需要新的提交来将分支合并在一起。相反,rebase 只是将新分支的提交放在主线的顶部,而不管分支的来源。这保持了主线中的线性,可以很容易地导航,但这是以保留提交的直接历史为代价的。您使用哪一个通常是个人偏好或取决于项目负责人,但为了以防万一,了解如何使用这两者是值得的!
安装
如果你已经安装了 git 和 gitbash,那太好了!不用担心设置,你应该准备好了。如果没有,请随意查看下面的帖子,其中涵盖了如何安装 git 和开始使用 git 命令。
合并
一旦设置好 git 和 gitbash,我们就可以继续创建一个测试文件夹,我们将使用它来查看 git merge 会发生什么。我们将简单地称这个文件夹为git_test_merge
,但是你想怎么叫都可以!一旦创建了文件夹,您就需要cd
进入并创建一个新的空存储库。
为此,您将需要使用命令git init
来创建新的存储库。然后你应该看到文件夹中创建的.git
文件,并且main
或master
(如果你使用的是旧版本的 git)应该出现在你所在的文件夹目录之后。为此,您应该已经运行了这三个命令:
mkdir git_test_mergecd git_test_mergegit init
这将在文件夹git_test_merge
中创建一个 git 存储库,并为您提供该存储库的所有功能。出于这个测试的目的,我们将首先创建一个文件,添加一些行并创建两个提交,然后再创建一个分支,稍后我们将合并到这个分支中。这只是为了展示提交的结构以及分支如何处理这些提交。
要创建一个文件并向其中添加一行,我们可以使用下面的命令echo "This is a test file" >> test_file.txt
。这将创建文件test_file.txt
,并附加第一行“这是一个测试文件”。如果你想检查这是不是真的,你可以使用命令nano test_file.txt
来检查它是否工作。
一旦您创建了文件,我们希望使用git add test_file.txt
提交该文件,然后我们可以使用git commit -m "Create new test_file"
提交我们所做的更改。这将在我们的存储库中创建第一个提交。
这应该采用以下命令的形式:
echo "This is a test file" >> test_file.txtgit add test_file.txtgit commit -m "Create new test_file"
我们可以通过使用gitk
使用内置的 git GUI 来检查这种情况。在这种情况下,将出现以下内容:
这是一个 GUI,向您显示以前的提交以及它们是如何交互的。到目前为止,我们只在主分支上创建了一个提交,如下所示。它还向您展示了是谁在何时提交的,以及与此提交相关的文件中的更改。虽然这是非常有用的功能,但我们将只关注之前提交的可视化表示,这样我们就可以看到git merge
和git rebase
都发生了什么。
在我们继续之前,我们可以创建第二个提交,这样在我们创建一个新的分支来合并之前,我们可以在我们的master
分支上看到一个清晰的历史。这可以通过以下方式简单实现:
echo "This is the second line of the test_file" >> test_file.txtgit add test_file.txtgit commit -m "Add second line to the test_file"
此时,我们应该在master
分支上看到两个提交,这创建了一个清晰的历史。然后我们想要创建一个分支,稍后我们想要将它合并回master
分支。出于我们的目的,这可以称为merge_branch
,但是通常以您试图创建的特征命名。我们可以使用以下命令创建这个分支:
git checkout -b merge_branch
这将创建分支(由于-b
命令)并由于checkout
命令检查分支。
如果您再次使用gitk
,您应该会看到如下内容:
其中我们现在有一个master
和一个merge_branch
,它们目前都处于“向 test_file 添加第二行”提交状态。我们还可以看到,我们已经检出了merge_branch
,因为它是粗体的(不是很清楚,但它在那里)。
为了检查 merge 是如何工作的,我们可以为这个分支创建两个新的提交。这将像以前一样通过创建一个新文件并向其中添加两行来完成。您可以自己尝试一下,看看是否可以复制我们在上面完成的过程,如果不能,那么您可以遵循下面的命令:
echo "This is a new file" >> merge_file.txtgit add merge_file.txtgit commit -m "Create a merge_file"echo "This is the second line of the merge file" >> merge_file.txtgit add merge_file.txtgit commit -m "Add a second line to the merge file"
然后我们可以再次使用gitk
命令来查看merge_branch
应该比主分支提前两次提交,如下所示:
如果我们现在简单地进行合并,我们将不得不使用git checkout master
检查master
分支并运行git merge merge_branch
。结果看起来会像这样:
这将为您提供一个完整的合并,很好,但它并没有真正显示您想要的合并。以这种方式运行它将会把来自merge_branch
的所有提交放到master
分支上。这并没有显示太多,因为在现实中,master
分支可能会有进一步的提交,这会使过程变得复杂。
因此,我们可以通过检查master
分支并创建一个新的提交来复制它。这可以通过以下方式实现:
git checkout masterecho "This is the third line" >> test_file.txtgit add test_file.txtgit commit -m "Add third line to the test_file"
然后我们需要创建合并,将merge_branch
引入到master
分支中。我们这样做是因为位于我们想要将提交放入的分支中。在这种情况下,我们已经检查了master
分支,所以我们是在正确的地方。
然后我们执行git merge merge_branch
,指定我们想要将merge_branch
合并到主分支中。然后,它会要求将提交消息添加到合并中,因为合并将被注册为提交。这种情况下,我们可以简单的命名为merge merge_branch into master
。
一旦完成,我们就将merge_branch
合并到master
分支中。我们可以再次使用gitk
将它形象化如下:
在这里,提交的图形保留了提交来自两个分支的位置,这两个分支源自提交“将第二行添加到 test_file”。我们还可以看到它们在那之后如何分化的历史,直到我们将merge_branch
合并回master
。
这样做的好处是它维护了提交历史和顺序。您知道提交的顺序以及它们来自哪里,这意味着跟踪提交来自哪里变得更加容易。然而,问题是,如果你有许多分支,你想合并,那么这可能会变得凌乱和难看。这意味着清楚地理解历史会变得困难,这也是为什么许多开发人员更喜欢使用 rebase 而不是 merge。
重置基础
git 中的 Rebasing 将接受指定的提交,并将它们放在另一个分支中的提交之上。这样做的好处是,一旦您删除了分支,提交流将呈现线性,这将很容易导航和跟踪。然而,这样做的缺点是,它不能像合并那样直接保存工作流的历史。
为了了解这是如何工作的,我们可以基本上重复我们为合并工作流所采取的大部分步骤。我们可以使用mkdir git_test_rebase
、cd
创建一个名为git_test_rebase
的新文件夹,然后使用git_init
初始化 git 存储库。然后,我们可以创建一个包含两行代码和两次提交的新文件:
echo "This is a new file in the rebase folder" >> rebase_file.txtgit add rebase_file.txtgit commit -m "Create a new rebase text file"echo "This is the second line in the rebase file" >> rebase_file.txtgit add rebase_file.txtgit commit -m "Add second line to rebase_file"
由此,我们可以创建一个新的分支,稍后我们将使用以下内容将该分支重置回master
分支:
git checkout -b "rebase_branch"
在这个新分支中,和以前一样,我们可以创建另一个新文件并添加两个提交,这样新分支就有了一些历史:
echo "This is a new file in the rebase branch" >> new_file.txtgit add new_file.txtgit commit -m "Create new file"echo "This is the second line in the new file" >> new_file.txtgit add new_file.txtgit commit -m "Add second line to the new file"
然后我们可以进行checkout master
分支,以便像以前一样,我们可以向rebase_file
添加一个新行,并在主分支上创建第三个提交:
echo "Third line to the rebase_file" >> rebase_file.txtgit add rebase_file.txtgit commit -m "Add third line to the rebase_file"
在这一点上,我们回到了在最后一个存储库中执行merge
时的位置,在master
分支中执行了三次提交,在rebase_branch
中执行了两次提交,在第二次提交后 rebase 分支被分支。
这里的不同之处在于,我们现在必须再次对checkout
和rebase_branch
进行调整,因为是从中您想要合并的分支开始执行的,而不是从您想要合并的分支开始。一旦我们完成了这些,我们可以使用git rebase master
来告诉分支,我们想要将这个分支的所有提交重新放入master
分支。我们可以再次使用gitk
来检查发生了什么:
这里的主要区别是这个rebase
命令只是将来自rebase_branch
的提交放在了master
分支提交之上,而不需要创建新的提交。它也没有将master
分支指针移动到最近提交的位置。为此,我们可以使用以下命令强制master
分支执行rebase_branch
上的最新提交:
git branch -f master rebase_branch
这将显示:
当我们做gitk
时。为此,你必须确保没有检出master
分支,否则你将无法移动指针。
我们可以看到,rebase 已经将rebase_branch
的提交放在了master
分支的顶部,我们必须将master
分支的指针向上移动到这些新的提交。虽然这保留了提交的行结构,并且看起来很清晰,但是它没有按照提交的顺序保留提交的历史,这对于某些人来说是不理想的。
结论
现在,您知道了如何在一个特性分支上执行合并和重置,以合并提交。要记住的一个关键点是,必须从您想要合并的分支执行 rebase,而合并必须从您想要合并的分支执行。您还需要记住,合并将保留提交的历史以及它们来自哪里,但它可能会创建一个混乱的历史,通常很难破译,您需要一个额外的提交。相比之下,rebase 会产生一个很好的线性历史,易于阅读,但它不会保留它们的直接历史,而是将来自签出分支的提交放在指定分支的现有提交之上。您更喜欢哪种类型将是个人偏好之一,取决于他们各自的权衡,但在项目中,通常最好在开始时指定您将使用哪种约定。否则,您可能会以 rebases 和 merges 结束,这会混淆存储库的历史。
如果你喜欢你所读的,并且还不是 medium 会员,请使用下面我的推荐链接注册 Medium,来支持我和这个平台上其他了不起的作家!提前感谢。
https://philip-wilkinson.medium.com/membership
或者随意查看我在 Medium 上的其他文章:
掌握 Git:重置 v 还原
原文:https://towardsdatascience.com/mastering-git-reset-v-revert-12701108a451
你应该使用哪一个,为什么?
照片由 Praveen Thirumurugan 在 Unsplash 上拍摄
Git 是所有数据科学家和软件工程师都应该知道如何使用的工具。无论您是独自从事一个项目,还是作为一个大型分布式团队的一部分,了解如何使用 Git 从长远来看都可以为您节省大量时间。这是因为 git,以及更广泛的版本控制,允许您跟踪对代码库所做的更改,并与其他人有效地协作。你想逆转你所做的改变,用 git 很容易做到,你想从现有的代码中分离出一个新的特性,git,你想在你做改变的同时运行一个有效的产品,git,你想和其他人在同一段代码上工作,git 和 github!这真的很神奇。
Git 是所有数据科学家和软件工程师都应该知道如何使用的工具,所以我整理了一系列有助于掌握 git 的关键工具和功能。这篇文章是这个系列的第二篇文章,将介绍 git 重置和 git 恢复,它们可以用来撤销提交历史中的更改。如果您错过了本系列的第一篇文章,关于 git merge 和 git rebase,可以在下面找到链接:
git 用户会遇到的两个常用工具是git reset
和git revert
的工具。这两个命令的好处是,您可以使用它们来删除或编辑您在之前提交的代码中所做的更改。理解它们是如何工作的将会为你节省大量的时间,让你编写更干净的代码,并且当你这样做的时候对提交有更多的信心。
重置
在提交级别(我们改变整个提交),重置是将分支的当前尖端移动到先前提交的一种方式。这样做是为了从当前分支中删除我们不再需要的提交,或者撤销已经做出的任何更改。一个例子是使用下面的命令将分支new-feature
从当前的HEAD
向后移动两次提交:
git checkout new-feature reset HEAD~2
看起来像这样:
作者图片
朝向new-feature
分支头部的两个提交变得悬空或孤立。这意味着它们将在下次 Git 执行垃圾收集时被删除,实质上是从提交历史中删除它们。
但是,其效果取决于附加到命令的标志:
- 标志意味着尽管我们回复了两次提交,但这些提交中的更改仍然是工作目录的一部分,并被准备提交。如果此时运行
git commit
,那么属于提交C
和B
一部分的所有更改都将被提交,您将处于与重置前相同的阶段。 --mixed
是默认标志,在没有指定其他标志时使用。这样,虽然工作目录没有更改,也就是说没有文件被更改,但这些更改都不会被提交。这意味着如果你运行git status
,你会看到所有被修改的文件都是红色的,等待提交。--hard
标志将更改暂存快照和工作目录,以删除提交B
和C
中的所有更改。这意味着你根本看不到B
或C
的变化,就像它们从未存在过一样。这是最极端的重置,意味着它将把所有东西都变回提交A
后的状态。
git reset
的这种用法是一种简单的方法,可以撤销还没有与其他人共享的更改。这是因为git reset
改变了存储库的历史,所以应该只在改变没有公开时使用,比如当推送到远程存储库时。否则,当其他开发人员在您使用git reset
更改的提交基础上构建时,可能会出现导致合并冲突的问题,这肯定不会使您成为团队中最受欢迎的开发人员!
这意味着当提交主要在您的本地存储库时,可以使用git reset
。用例包括当在当前分支上开发的新特性应该是它们自己的分支的一部分时,当在先前的提交中引入了错误并且您想要移除它们时,或者当您想要改变提交历史时,例如不同提交中的不同文件或者不同提交名称。
归还
虽然git reset
应该主要用于存储库的非公共分支,但是有时您想要撤销对公共存储库所做的更改。这可能是因为所做的更改引入了意外的错误,或者更改本身并不需要。在这种情况下,应该使用git revert
而不是git reset
。
这是因为恢复会通过创建新的提交来撤消提交。这使它成为撤销公共提交历史中的更改的安全方式,因为它不会覆盖任何历史,而是在新的提交中撤销所有这些更改。例如,当您想要撤销提交B
中所做的更改时,您可以使用git revert B
创建一个新的提交来撤销所有这些更改。这样做的时候,git 会找出在B
中所做的更改,如果可能的话撤销所有这些更改,然后将一个新的提交添加到现有的项目中,如下所示:
git revert B
作者图片
如果可以进行更改,并且没有合并冲突,那么将打开一个编辑器,要求您命名将被添加到当前头文件末尾的新提交,就像B*
一样。
这保存了提交历史,并潜在地减少了您的团队可能必须处理的合并冲突的数量。但是在你这么做之前,因为git revert
有可能覆盖文件,你需要提交或者隐藏在恢复操作中可能丢失的更改
结论
git reset
和git revert
都是撤销先前提交中所做更改的有用命令。虽然git reset
通过将分支的当前头移回指定提交来实现这一点,从而改变提交历史,但是git revert
通过创建新提交来实现这一点,该新提交撤销指定提交中的改变,因此不改变历史。这意味着,当您想要在多次提交中撤销更改时,git reset
可能更有用,但通常应该仅在更改尚未公开时使用,而git revert
可以在仅需要撤销特定提交时使用,甚至可以在更改已经公开时使用。知道如何使用这两个命令会让你在提交时更有信心,因为你知道它们是可以撤销的,并且有希望让你和你的团队更有效率!
如果你喜欢你所读的,并且还不是 medium 会员,请使用下面我的推荐链接注册 Medium,来支持我和这个平台上其他了不起的作家!提前感谢。
https://philip-wilkinson.medium.com/membership
或者随意查看我在 Medium 上的其他文章:
在 Matplotlib 中控制插入轴
原文:https://towardsdatascience.com/mastering-inset-axes-in-matplotlib-458d2fdfd0c0
嵌入轴是 Matplotlib 中一个奇妙的(且经常未被充分利用的)工具。它们可用于:
- 放大图的特定部分以更详细地显示它们。
- 用附加信息替换图中的空白区域。
- 给你的数字一点额外的活力!
在这篇文章中,我将介绍使用嵌入轴的基本知识,然后给出如何定制和改进你的绘图的额外细节。本文末尾的 Python 笔记本中包含了用于生成所有图的代码。
基础知识
创建嵌入轴有三个主要组成部分。首先,您需要一个现有的环绕轴来添加插入轴。然后,您需要创建插入轴,并定义它在环绕轴中的位置。最后,您需要将插入数据绘制到插入轴上。一个可选步骤是添加线条来指示插入缩放。这可以通过以下方式实现:
# Code snippet to create inset axes.
# axis is the surrounding axis into which the inset axis will be added# Create an inset axis in the bottom right corner
axin = axis.inset_axes([0.55, 0.02, 0.43, 0.43])# Plot the data on the inset axis and zoom in on the important part
plot_data(axin)
axin.set_xlim(-10, 12)
axin.set_ylim(0.75, 1.4)# Add the lines to indicate where the inset axis is coming from
axis.indicate_inset_zoom(axin)
在上面的例子中,axis.inset_axes
创建了插入轴axin
,并定义了它在原始axis
中的位置(稍后将详细介绍这个定位)。插入轴中显示的区域是通过设置轴限制来控制的,即axin.set_xlim
和axin.set_ylim
——把这想象成裁剪原始图,这样我们可以放大重要的部分。只需使用indicate_inset_zoom
即可添加缩放线和边框。
这将产生类似如下的结果:
使用单个插入轴放大部分绘图的示例。作者创造的形象。
嵌入轴的定位是轴相对于轴的定位。这意味着位置与轴限制无关。嵌入轴的坐标应在0,0和1,1之间。axis.inset_axes
需要四个参数来定义边界:[x0,y0,width,height]。因此,例如,[0.4,0.4,0.2,0.2]给出了一个位于图中心的插入轴,,而不管轴限制是什么:
当外部轴限制改变时,插入轴不会移动,因为(默认情况下)位置是相对于轴的。作者创造的形象。
越来越花哨
我们可以通过使用嵌入轴让更有创意:
- 没有理由为什么我们不能在我们的地块中有多个插入轴。也许我们想放大两个区域?
- 有时,嵌入轴中的内容可能实际上比整体情节更有趣。我们可以交换内容,以便周围的绘图包含放大的版本,并且插入轴显示更宽的视图。
- 插入轴甚至不需要显示与周围绘图相同的数据。放大只是一个用例。显示附加信息也很好。
下面我们用上面的图给出了这些不同用例的例子。看看带插入轴的图比无聊的原始图好看多了!
使用嵌入轴制作更有趣的图的一系列选项。作者创造的形象。
更多提示
创建插入轴时,还有一些需要注意的事项。
纵横比 —考虑插入轴的纵横比与其显示的原始区域的纵横比非常重要。这会导致插入轴中原始数据的拉伸或挤压,这可能会导致数据失真。例如:
使用嵌入轴时要注意不同的纵横比。作者创造的形象。
轴定位 —上面,我提到了轴相对于坐标定位嵌入轴。或者,可以使用相对于坐标的数据。创建插入轴时设置transform=axis.transData
。不过要小心!这使得嵌入轴的位置对轴限制敏感:
轴与数据相对定位。当轴限制改变时,使用数据相对定位时,插入轴会移动。作者创造的形象。
Z 排序— 默认情况下,插入轴应该出现在您已经绘制的任何内容的顶部。然而,对于手动控制,您可以调节zorder
来控制顶部的内容:
更改插入轴的 z 顺序值允许您将它们放置在现有数据的顶部,甚至是彼此之上。作者创造的形象。
下面是我用来生成本文中的情节的 Python 笔记本:
这就是我使用 Matplotlib 嵌入轴的指南。如果您有任何问题,请发表评论或联系我。感谢阅读,密谋愉快!
在 dbt 中掌握数据透视表
原文:https://towardsdatascience.com/mastering-pivot-tables-in-dbt-832560a1a1c5
这个函数将节省您编写 SQL 的时间。
有多少次你不得不把一个过于复杂的解决方案放在一起来解决一个简单的问题?如果你以前曾经编写过代码,你可能至少可以列出一些例子。对我来说,有几次是围绕着数据透视表进行的。它们听起来很简单,但是用 SQL 写起来很复杂。
幸运的是,我最近发现 dbt 在 dbt utils 中有一个函数,可以快速简单地创建数据透视表。不需要复杂的聚合或连接!
什么是数据透视表?
首先,让我们从什么是数据透视表开始。不,这不是 Excel 数据透视表。它是一个函数,将一列中的唯一值转换成它们自己的列,用一个布尔值填充该行的相应单元格。
例如,透视如下所示的数据…
作者图片
…会产生如下所示的数据…
作者图片
数据透视表用例
这是聚合和计算数据中是否存在某些特征的好方法。当我发现这个技巧时,我需要旋转一个充满不同葡萄酒特征的列。我希望每个特征都有一列,并有一个值告诉我该葡萄酒是否存在该特征。
使用数据透视表也是确保表中有唯一主键的理想方法。通常,当您的列包含多个 id 可能存在的不同属性时,您的主键不再是唯一的。由于一个列会产生重复项,因此最终会有多个 id 相同的行。
还不理解?
让我们看看这一款酒。看到除了wine_features
列,每一列的值都是一样的吗?因为这种酒是干的、有机的、生物动态的,所以创建了一个新的行来代表这些特征。
作者图片
这通常是因为您需要将数据库中的表连接在一起。不幸的是,数据透视表是避免这个问题并保持数据模型整洁的唯一方法。
你如何使用 dbt 数据透视表?
dbt 包含一个名为dbt _ utils . pivot的函数,它可以帮助您使用一行简单的代码透视表格。
看起来是这样的:
{{ dbt_utils.pivot(‘<column name>’, dbt_utils.get_column_values(ref(‘<table name>’), ‘<column name’)) }}
您只需指定要透视的列名。确保用单引号括起来。
您还可以指定该列所在的表的名称。同样,确保你用单引号括起来。
不幸的是,这需要是数据库中的一个永久表。你不能引用一个由 CTE 创建的表格,这是一个错误。我必须创建一个永久表,其中包含我需要连接在一起的表,然后在这个 pivot 函数中引用这些表。让它成为自己的中介模型就行了。
在实际查询中使用 pivot 函数时,它应该是这样的:
select
wine_name,
{{ dbt_utils.pivot('wine_features', dbt_utils.get_column_values(ref('wine_features_joined'), 'wine_features')) }}
from {{ ref('wine_features_joined') }}
group by wine_name
您可以看到,该函数替换了 SQL 查询中列名通常所在的位置。只有作为分组依据的列才是应该选择的其他列。
还要注意,我使用了一个{{ ref() }}
函数,因为我从一个永久表中进行选择,该表已经作为数据模型存在于我的 dbt 环境中。不要从 cte 中选择!
如果我们从一张葡萄酒及其不同特征的表格开始,看起来像这样…
作者图片
使用 pivot 函数运行我在上面展示的查询将导致该表如下所示:
作者图片
注意曾经是wine_features
列中的值的特性现在是列标题。这些列中的布尔值显示该功能是否存在于原始列中。
另一件需要记住的事情是…
通常,旋转列的数据类型很难处理。请确保将它们转换为适当的数据类型,并根据需要重命名它们。在这里,您可以看到透视列的数据类型是一个数字。显然,这是错误的。
作者图片
我不得不为我的每一个专栏重新编写和重命名我的专栏:
wine_features."organic" AS is_organic
引号允许您选择雪花中的列,尽管数据类型与预期的不同。
结论
现在,您已经准备好使用 dbt utils pivot 函数来透视表了!这将使你的生活变得更加简单,你的代码也更加整洁。请继续关注更多关于 dbt 在其 utils 包中为您提供的便利函数的文章。
在那之前,通过订阅我的时事通讯来了解更多关于 dbt 和现代数据栈的知识。
通过讲故事掌握分类模型的评估
数据科学家必备的知识
照片由 Unsplash 上的 Aditya Romansa 拍摄
今天,我们可以从世界各地获得源源不断的数据。分类模型是最流行的机器学习工具之一,用于在数据中发现模式并理解它,以便我们可以揭示决策的相关见解。它们是一种监督学习的形式,在这种学习中,我们训练一个模型,根据预先确定的特征对数据点进行分组。反过来,该模型输出数据点属于特定类别的可能性或概率。
用例层出不穷,广泛分布于各个行业— 语音识别、垃圾邮件检测、异常/欺诈检测、客户流失预测、客户细分和信用评估。
因此,作为一名数据科学家,掌握分类模型的艺术至关重要。
在本文中,我们将关注在数据科学中创建模型的最后一个步骤:评估模型性能,或者换句话说,评估分类的好坏。
有什么比一个好故事更能解释这些指标以及如何使用它们呢?
假设你是所在城市医院产前部的负责人。你的目标是为未来的父母提供最积极的体验。在这方面,你们雇佣了最好的医生,并组建了一支由护士和助产士组成的梦之队来支持他。
医生忙得不可开交,没有时间检查所有的病人以确定他们是否怀孕。所以他使用分析和不同的血液标记进行验证。护士的角色是探访病人以确保预见性。这里我们有 4 种可能的情况:
- 医生说病人怀孕了,护士确认
→ 真阳性(TP) - 医生说病人怀孕了,但护士宣布无效
→ 假阳性【FP】 - 医生说病人没有怀孕,护士确认
→ 真阴性(TN) - 医生说病人没有怀孕,但护士宣布无效
→ 假阴性(FN)
作为部门主管,你专注于提供最佳质量的服务,因此你想评估医生在识别早孕方面的能力。为此,您可以使用 5 个关键指标:
1.准确(性)
准确性可能是最常见的度量标准,因为它相对容易理解。它是正确预测数除以预测总数的比值。
(TP+TN) / (TP + FP + TN + FN)
换句话说,准确性会告诉我们医生在给病人分类方面有多好。
50%的准确率意味着这个模型和掷硬币一样好。一般来说,根据应用领域的不同,我们的目标是达到 90%、95%或 99%以上的精度。
记住:有人说,如果我们有高精度,我们就有一个好模型。只有当数据集平衡时才是这样,也就是说,类的大小相对均匀。
假设在患者群中有更多未怀孕的患者(,即怀孕患者占少数)。在这种情况下,我们说样本是不平衡的,精确度不是评估性能的最佳指标。
2.精确
精度是正确预测的正元素数除以预测的正元素总数。所以这是一个准确性和质量的衡量标准——它告诉我们医生在预测怀孕方面有多好。
TP / (TP + FP)
根据模型应用,具有高精度可能是至关重要的。始终评估出错的风险,以决定精度值是否足够好。
如果医生宣布怀孕(但他错了),这可能会影响患者,因为他们可能会就这个重大消息做出改变人生的决定(例如,买新房子或换车)。
3.回忆
回忆——又名敏感度或真阳性率或命中率,是与实际阳性数相比,正确预测的阳性元素数。它告诉我们医生在检测怀孕方面有多好。
TP / (TP + FN)
与 precision 类似,根据模型应用,拥有高召回率可能是至关重要的。有时,我们不能错过一个预测(欺诈,癌症检测)。
假设医生错过了一个病例,没有预测到怀孕。患者在怀孕期间可能会保持一些不健康的习惯,如吸烟或饮酒。
4.特征
特异性—也称为选择性或真阴性率,总结了当结果为阴性时预测阳性类别的频率。可以理解为误报警指示灯。
TN / (TN + FP)
理想情况下,一个模型应该具有高特异性和高召回率,但这是一个权衡。每个模型都需要选择一个阈值。
基于上面提到的原因,我们不想错过一个怀孕案例。同时,如果我们没有可靠的血液标志物来确认怀孕,我们也不想提醒患者。作为部门主管,你需要决定医生的预后不够好的转变点是什么,我们需要护士进行体检。
5。F-measure | F1 得分
F1 分数是精确度和召回率的加权调和平均值。它反映了一个模型的有效性——当错过一个病例或错误宣布怀孕同样有风险时,医生的表现如何。
2 x (Precision x Recall) / (Precision + Recall)
当样本不平衡,准确性变得不合适时,我们可以使用 F1 分数来评估模型的性能。
如果没有怀孕的患者比怀孕的患者多得多,我们认为样本是不平衡的,并将使用 F1 分数来评估医生的表现。
既然我们已经设置了评估分类模型的关键指标,我们可以更仔细地研究如何以一种令人信服的方式可视化它们。
混淆矩阵
混淆矩阵或误差矩阵是一个简单的表格,汇集了来自分类模型的预测结果:*真阳性、真阴性、假阳性&假阴性。*它通过分解每个类别的正确和错误预测的数量,帮助可视化分类器产生的错误类型。
来源:蛋白质折叠过程中早期折叠残基可解释分类模型的应用(Sebastian Bittrich)
混淆矩阵突出显示了模型在进行预测时混淆的地方。
因此,与单独使用准确性相比,这是一种有用的可视化方法,因为它显示了模型的薄弱之处,并提供了改进它的可能性。
ROC 曲线和精确回忆曲线
ROC 曲线是假阳性率(也称为反向特异性)对真阳性率(也称为灵敏度)的曲线图。如果每个类别的患者数量大致相等(即平衡数据集),则应使用精确-召回曲线,而非平衡病例应使用精确-召回曲线(来源)。
一个好的模型用一条从 0 到 1 快速增长的曲线来表示。该公司表示,该模型不会牺牲太多的精确度来获得高召回率。
一个糟糕的模型——又名无技能分类器——不能区分类别,因此,在所有情况下都预测一个随机或恒定的类别。这些模型由从图的左下角到右上角的对角线表示。
来源:作者
所以你现在可能知道曲线的形状给了我们调试模型的宝贵信息:
- 如果曲线更接近左下方的随机线,则表明假阳性较低,真阴性较高。另一方面,如果图的 y 轴上有更大的值,这意味着更高的真阳性和更低的假阴性。
- 如果曲线不平滑,则意味着模型不稳定。
这些曲线也是比较模型和选择最佳模型的重要工具。
曲线下面积(AUC 或 AUROC)通常用于总结模型技巧。它可以取从 0.5(最差模型)到 1(完美模型)的值。
AUROC 值越高,模型越好。
现在,您已经掌握了开始评估您的分类模型所需的所有基本知识,对于您的数据科学家角色来说,同样重要的是,要有一个好的故事来向大量观众传达您的表现。
像往常一样,给我留下你的评论和消息,我会很高兴收到你的来信,让这篇文章变得更好。在 Medium 上关注我,了解更多关于数据科学的内容。
https://agiraud.medium.com/membership
用 Go 掌握 WebSockets
原文:https://towardsdatascience.com/mastering-websockets-with-go-c30d0ac48081
关于如何在 Go 中使用 WebSockets 构建实时 API 的教程
图片由珀西·博尔梅尔提供。Gopher 由拓也·上田提供,原始 Go Gopher 由勒内·弗伦奇提供(CC BY 3.0)
如果我们仔细想想,常规的 HTTP APIs 是愚蠢的,就像,真的愚蠢。我们可以通过发送数据请求来获取数据。如果我们必须在网站上保持数据新鲜,我们将不得不不断地请求数据,即所谓的轮询。
本文中的所有图片均由珀西·博尔梅尔制作。Gopher 由上田拓也制作,Go Gopher 由蕾妮·弗伦奇原创(CC BY 3.0)
这就像让一个孩子坐在后座上问“我们到了吗”,而不是让司机说“我们到了”。这是我们在设计网站时开始使用的方式,很傻是不是?
令人欣慰的是,开发人员已经通过诸如 WebSockets 、 WebRTC 、 gRPC 、 HTTP2 Stream 、 ServerSent Events 以及其他双向通信等技术解决了这个问题。
WebSockets 是最古老的双向通信方式之一,如今被广泛使用。它被大多数浏览器支持,并且相对容易使用。
在本教程中,我们将介绍什么是 WebSockets 以及它们如何工作,如何在 Go 中使用它们在服务器和客户端之间进行通信。我们还将探讨我在 WebSocket APIs 中看到的一些常见缺陷,以及如何解决它们。
在本教程中,我们将建立一个聊天应用程序,您可以进入不同的聊天室。WebSocket 服务器将使用 Go 构建,客户端使用普通 JavaScript 连接。当使用用 Go、Java、React 或任何其他语言编写的 Websocket 客户端进行连接时,我们学习和应用的模式可以很容易地进行调整。
这篇文章也有录音,可以在我的 YouTube 频道上看。
在 YouTube 上掌握 Go 中的 WebSockets
什么是 WebSockets &为什么您应该关注它
如何用简单的术语初始化 WebSocket
WebSocket 标准在 RFC 645 中定义。
WebSockets 使用 HTTP 向服务器发送初始请求。这是一个普通的 HTTP 请求,但是它包含一个特殊的 HTTP 头Connection: Upgrade
。这告诉服务器,客户机正试图将 HTTP 请求 TCP 连接升级到一个长期运行的 WebSocket。如果服务器用一个HTTP 101 Switching Protocols
响应,那么连接将保持活动,使得客户端和服务器能够双向、全双工地发送消息。
一旦这个连接被同意,我们就可以发送和接收来自双方的数据。WebSockets 没有更多的内容,这可能是你开始使用它们所需要了解的。
如果你想更多地了解在安装过程中到底发生了什么,我可以推荐 RFC。
您可能想知道是否需要实时解决方案。所以这里有几个经常使用 WebSockets 的领域。
- 聊天应用 —需要接收消息并转发给其他客户端的应用,这是 WebSockets 的完美匹配。
- 游戏— 如果你开发一款多人游戏,并且是基于网络的,那么 WebSockets 就是真正的天作之合。您可以从客户端推送数据,并将其广播给所有其他玩家。
- 提要— 对于需要数据提要的应用程序,可以使用 WebSockets 轻松地将更新的数据推送到任何客户端。
- 实时数据— 基本上,只要你有实时数据,WebSockets 就是一个很好的解决方案。
开始应用程序的基础
项目设置—一个 Go 后端和一个 JavaSript 客户端
我们将首先设置一个简单的 HTTP 服务器,它也使用文件服务器托管我们的 web 应用程序。我想避免使用 React 等任何 web 框架,所以我们将使用原生 JavaScript。通常,当连接到 WebSocket 时,步骤非常相似,所以将它移植到您使用的任何框架中应该没有问题。
首先初始化一个新模块
go mod init programmingpercy.tech/websockets-go
然后我们创建一个新文件main.go
,这将是我们的应用程序的起点。
我们将首先设置应用程序来服务 API 并托管 HTML/JS 代码。一旦我们完成了这些,我们将开始实际的 WebSocket 实现,这样更容易理解。
让我们用一个简单的代码填充main.go
来托管我们即将构建的网站。我们将只服务于frontend
目录,我们将创建和存储前端代码。
main . go——第一个简单托管前端的版本
现在让我们添加前端,这将是一个简单的原始 HTML/JS/CSS 文件,显示我们惊人的期待聊天应用程序。它由一个表单chatroom-selection
和另一个表单chatroom-message
组成,前者供用户进入某个聊天室,后者用于通过 WebSocket 发送消息。
这只是简单的 HTML 和 JavaScript,但是还没有实现 WebSocket 实现。唯一值得一提的是window["WebSocket"]
,这是一个全局变量,你可以用它来检查客户端的浏览器是否支持 WebSocket。如果没有定义,我们会提醒用户他们的浏览器不受支持。
创建一个名为frontend
的文件夹和一个名为index.html
的文件。然后用下列要点填充 index.html。我不会涉及 HTML 和 JS 部分,我希望你熟悉它们。
frontend/index.html —还没有 WebSockets 的简单网站
如果您在终端中运行带有go run main.go
的应用程序,并访问 localhost:8080 ,您应该会看到一个令人惊叹的网站,它拥有我们开始实现 WebSockets 所需的一切。
localhost:8080 —令人惊叹的聊天应用程序
现在,发送消息和改变聊天室除了打印到控制台之外什么都不做,但这是我们将要实现的。
在客户端和服务器之间连接 WebSocket
连接客户端和服务器
为了开始,我们将添加到前端,以便它尝试连接到我们的 WebSocket API。这在 JavaScript 中很容易,只需一行代码就可以完成。
在 JavaScript 中,有一个内置的 WebSocket 库,无需导入任何东西就可以使用。我们可以用new WebSocket(URL)
创建客户端,但是首先我们需要创建 URL。URL 由协议组成,就像常规的 HTTP URL 一样,后跟路径。将 WebSockets 放在一个/ws
端点上是一个标准。
我们使用 WebSockets 时有两种协议,一种是ws
,另一种是wss
。它的工作原理就像 HTTP 和 HTTPS,额外的 S 代表安全,并将对流量应用 SSL 加密。
强烈建议使用它,但需要证书,我们稍后会应用它。
让我们在 windows.onload 函数中添加一行连接到ws://localhost/ws
的代码。
index.html—添加了到后端的连接
现在,您可以继续运行程序,当您访问网站时,您应该会看到控制台上打印出一个错误,我们无法连接。这仅仅是因为我们的后端还不接受连接。
让我们更新后端代码以接受 WebSocket 连接。
我们将从构建我们的Manager
开始,它用于服务连接并将常规 HTTP 请求升级到 WebSocket 连接,管理器还将负责跟踪所有客户端。
我们将使用 Gorillas WebSocket 库来处理连接,这是通过创建一个Upgrader
来完成的,它接收 HTTP 请求并升级 TCP 连接。我们将为升级程序分配缓冲区大小,该大小将应用于所有新客户端。
管理器将公开一个名为serveWS
的常规 HTTP HandlerFunc,我们将在/ws
端点上托管它。此时,我们将升级连接,然后简单地再次关闭它,但我们可以验证我们可以通过这种方式连接。
创建一个名为manager.go
的文件,将要点中的代码填入其中。
manager.go —管理器起点,可以接受和升级 HTTP 请求
我们还需要将serveWS
添加到/ws
端点,以便前端可以连接。我们将启动一个新的管理器,并在main.go
内的setupAPI
函数中添加 HTTP 处理程序。
main.go —将管理器的服务功能作为端点公开
您可以通过运行以下命令来运行该软件
go run *.go
如果您继续访问该网站,您应该会注意到控制台中不再显示错误,现在连接已被接受。
客户和管理
负责所有客户的经理
我们可以将所有客户端逻辑添加到serveWS
函数中,但是它可能会变得非常大。我建议创建一个用于处理单个连接的客户端结构,该结构负责与客户端相关的所有逻辑,并由管理器管理。
客户端还将负责以同时安全的方式读/写消息。Go 中的 WebSocket 连接只允许一个并发的 writer,所以我们可以通过使用无缓冲通道来处理这个问题。这是大猩猩库的开发者自己推荐的技术。
在我们实现消息之前,让我们确保创建了客户机结构,并赋予管理器添加和删除客户机的能力。
我已经创建了一个名为client.go
的新文件,它现在很小,可以保存任何与我们的客户相关的逻辑。
我将创建一个名为ClientList
的新类型,它只是一个可以用来查找客户端的地图。我还喜欢让每个客户端持有一个对管理器的引用,这允许我们更容易地管理状态,甚至从客户端。
client.go —客户端的第一份草稿
是时候更新manager
来保存新创建的ClientList
了。由于许多人可以并发连接,我们也希望管理器实现sync.RWMutex
,这样我们可以在添加客户端之前锁定它。
我们还将更新NewManager
函数来初始化一个客户端列表。
函数serveWS
将被更新以创建一个带有连接的新客户端,并将其添加到管理器中。
我们还将使用插入客户端的addClient
函数和删除客户端的removeClient
函数来更新管理器。删除将确保优雅地关闭连接。
manager.go —能够添加和删除客户端的管理器
现在,我们已经准备好接受新客户并添加他们。我们还不能正确删除客户端,但我们很快就会这样做。
我们必须实现,以便我们的客户端可以读取和写入消息。
阅读和撰写邮件
以安全的方式同时编写消息
阅读和书写信息看起来似乎是一件简单的事情,事实也的确如此。然而,有一个许多人忽略的小陷阱。WebSocket 连接只允许有一个并发的 writer,我们可以通过让一个无缓冲的通道充当锁来解决这个问题。
我们将更新manager.go
中的serveWS
函数,以便在创建之后为每个客户端启动两个 goroutines。现在,我们将注释掉这段代码,直到完全实现。
manager.go —更新服务程序,为每个客户机启动读/写 goroutine
我们将从添加阅读过程开始,因为它稍微容易一些。
从套接字读取消息是通过使用ReadMessage
完成的,它返回一个messagetype
、有效载荷和一个错误。
消息类型用于解释发送的是什么类型的消息,是 Ping、pong、数据还是二进制消息等。所有类型都可以在 RFC 中读到。
如果出现问题,就会返回错误,一旦连接关闭,也会返回一个错误。因此,我们将希望检查某些关闭消息来打印它们,但对于常规关闭,我们不会记录。
client.go —添加了阅读消息功能
我们可以更新前端代码,并尝试发送一些消息来验证它是否按预期工作。
在index.html
中,我们有一个名为sendMessage
的函数,它现在在控制台中打印出消息。我们可以简单地更新它,将消息推送到 WebSocket 上。用 JavaScript 发送消息就像使用conn.send
函数一样简单。
index.html——传递信息
重新启动程序,在 UI 中输入一条消息,然后按 Send Message 按钮,您应该会看到在 stdout 中发送的消息类型和有效负载。
现在我们只能发送消息,但对消息什么也不做,是时候让我们增加写消息的能力了。
还记得我说过我们只能用一个并发进程来写 WebSocket 吗?这可以通过多种方式解决,Gorilla 自己推荐的一种方式是使用无缓冲通道来阻止并发写入。当任何进程想要在客户端的连接上写入时,它将改为将消息写入无缓冲通道,如果当前有任何其他进程正在写入,该通道将会阻塞。这使我们能够避免任何并发问题。
我们将更新client
结构来保存这个通道,并更新构造函数来初始化它。
client.go —添加充当网关的出口通道
writeMessages
功能与readMessages
非常相似。然而,在这种情况下,我们不会收到一个错误,告诉我们连接被关闭。一旦egress
通道关闭,我们将向前端客户端发送CloseMessage
。
在 go 中,我们可以通过接受两个输出参数来查看通道是否关闭,第二个输出参数是一个布尔值,表示通道关闭。
我们将使用 connections WriteMessage
函数,它接受 messagetype 作为第一个输入参数,接受 payload 作为第二个输入参数。
client.go —处理任何应该发送的消息的函数
如果你熟悉 Go,你可能已经注意到我们在这里使用了一个for select
,它现在是多余的。在本教程的后面,我们将向选择中添加更多的案例。
确保在
serveWS
函数中取消go client.writeMessages
的注释。
现在,在egress
上推送的任何消息都将被发送到客户端。目前没有进程向出口写入消息,但是我们可以快速破解以测试它是否按预期工作。
我们会将在readMessages
中收到的每条消息广播给所有其他客户端。我们将通过简单地将所有输入消息输出到每个客户端的出口来实现这一点。
client.go —在收到的每条消息中添加了一个小广播
我们只在第 29 行添加了 for 循环,稍后我们将删除它。这只是为了测试整个读写过程是否按预期进行。
是时候更新前端来处理传入的消息了。JavaScript 通过触发一些我们可以应用监听器的事件来处理 WebSocket 事件。
这些事件都在文档中进行了解释。我们可以很快覆盖它们。
- 关闭 —当 WebSocket 关闭时触发。
- 错误 —当 WebSocket 由于错误而关闭时触发。
- 消息 —当 WebSocket 收到新消息时触发
- 打开 —当 WebSocket 连接打开时触发。
根据您想要在前端做什么,您可以分配这些事件处理程序。我们对message
事件感兴趣,所以我们将添加一个监听器,暂时只将它们打印到控制台。
一旦连接打开,我们将添加一个简单的函数来打印发送的事件。这个事件对象包含一堆数据,比如发送的时间戳和消息类型。我们将需要包含在data
字段中的有效载荷。
index.html—添加 onmessage 侦听器来处理传入的消息
你现在可以尝试重启软件,访问网站并发送一些信息。您应该在控制台中看到事件正在被发送和接收。
这意味着现在读和写都有效。
使用事件方法进行缩放
构建在 WebSocket 上发送的消息
我们可以连接,我们现在可以发送和接收信息。这一切都很棒,我们有了一个基本的设置。
现在,如果你只想传递一种信息,这可能行得通。我通常发现创建一个基于事件/类型的方法使得扩展 WebSocket 更加容易。
这意味着我们创建了一个发送每条消息的默认格式。在这种格式中,我们有一个特定的字段来解释它是哪种消息类型,然后是一个有效载荷。
怎么,这听起来耳熟吗?
这是应该的,因为这基本上就是 WebSockets 现在正在做的事情。例外情况是,我们将消息作为 JSON 对象发送,我们的应用程序可以使用该对象来路由到要执行的正确操作/功能。
我发现这是一种易于使用、易于扩展并使 WebSocket 在许多用例中得到利用的方法。这是一种 RPC 解决方案。
我们首先在 JavaScript 文件中添加Event
类,这样我们就可以解析传入的消息。然后,我们将这些事件传递给一个routeEvent
函数,该函数检查字段type
的值,并将事件传递给真正的处理程序。
在onmessage
监听器中,我们希望 JSON 格式的数据适合事件类。
我们还将创建一个名为sendEvent
的函数,它将接受一个事件名称和有效载荷。它根据输入创建事件,并将其作为 JSON 发送。
每当用户使用sendMessage
发送消息时,它将调用sendEvent
。
下面的要点展示了处理这个问题的 JavaScript 部分。
index.html—JavaScript 标签被更新以处理事件。
既然网站已经有了接受Events
并发送它们的逻辑,我们还需要让后端处理它们。
首先创建一个名为event.go
的文件,它将包含事件的所有逻辑。
我们希望在后端有一个Event
结构,它应该是 JavaScript 事件类的一个副本。
Event . go—web socket 事件结构
注意,有效载荷的数据类型是一个json.RawMessage
,因为我们希望用户能够发送他们想要的任何有效载荷。由事件处理程序来决定有效负载数据的结构。
当在后端接收到消息时,我们将使用type
字段将其路由到适当的EventHandler
,eventhandler 是一个函数签名。因此,通过创建满足签名模式的新函数来添加新功能是很容易的。
EventHandler 签名将接受消息来自的Event
和Client
。它也会返回一个错误。我们接受客户端,因为一些处理程序可能希望在完成后返回响应或向客户端发送其他事件。
我们还将添加一个SendMessageEvent
,这是事件有效载荷中预期的格式。
event.go —添加了 EventHandler 签名和 SendMessageEvent
我喜欢让Manager
存储EventHandlers
的地图。这允许我很容易地添加东西,在一个真正的应用程序中,管理器可以包含一个数据库存储库,等等。我们将添加它,并添加一个名为setupEventHandlers
的新函数,用于添加所需的函数。
拥有一堆处理程序的一个好方法是将这些EventHandlers
存储在一个映射中,并使用Type
作为键。因此,我们将保留一个包含所有处理程序的映射,而不是使用开关来路由事件。
我们添加了一个routeEvent
函数,它接收传入的事件并从映射中选择正确的处理程序。
如果您有敏锐的眼光,您可能已经注意到 routeEvent 本身是一个 EventHandler,如果需要的话可以这样使用。
manager.go 向管理器添加了事件处理程序
在我们准备好整个事件基础设施之前,最后一件事是更改Client
。客户端的readMessages
应该将传入的 JSON 编组到一个Event
中,然后使用Manager
对其进行路由。
我们还将修改Clients
出口通道,使其不发送原始字节,而是发送Event
。这也意味着我们需要更改writeMessages
来在发送数据之前整理数据。
client.go 添加了事件而不是原始字节的用法
您可以尝试使用go run *.go
重启后端并发送消息。您应该会看到类似于{send_message [34 49 50 51 34]}
的东西正在被打印。由于当前处理程序不解析原始字节,因此有效负载应该打印为字节。
在我们实现它之前,我想确保我们覆盖了一些与 WebSocket 相关的主题。
心跳——乒乓球
通过心跳保持连接
WebSockets 允许服务器和客户端发送一个Ping
帧。Ping 用于检查连接的另一部分是否仍然存在。
但是我们不仅检查我们的其他连接是否还活着,而且我们还保持它活着。一个空闲的 WebSocket 将/可以关闭,因为它已经空闲了太长时间,Ping & Pong 允许我们轻松地保持通道活动,避免低流量的长时间运行的连接意外关闭。
每当发送了一个Ping
时,另一方必须用一个Pong
来响应。如果没有发送响应,可以假设对方已经不在人世。
这是合理的,你不能一直和没有回应的人说话。
为了实现它,我们将从Server
代码开始。这意味着我们的 API 服务器将频繁地向每个客户端发送 ping,并等待 Pong,如果没有,我们将删除该客户端。
让我们从定义要使用的定时器开始。在client.go
中,我们将创建一个pongWait
和一个pingInterval
变量。PongWait 是我们允许的 pong 之间的秒数,它将被来自客户端的每个 pong 重置。如果超过这个时间,我们将关闭连接,假设 10 秒钟的等待是合理的。
是我们向客户发送 pings 的频率。请注意,这必须低于 pongWait。如果我们有一个发送速度比 pongWait 慢的 PingInterval,pongWait 就会取消。
等等,如果我们每 15 秒发送一次 Ping,但只允许服务器在两次 Ping 之间等待 10 秒,那么连接将在 10 秒后关闭。
client.go —添加了计时变量,pingInterval 算法借用了 Gorilla
现在我们需要让服务器向每个客户端发送 ping 消息。这将在客户端的writeMessages
函数中完成。我们将创建一个基于pingInterval
触发的计时器,一旦触发,我们将发送一个类型为PingMessage
的消息,其中包含一个空的有效负载。
我们在同一个函数中这样做的原因是,我们希望记住连接不允许并发写入。我们可以让另一个进程在出口发送一个 Ping,并在事件结构上添加一个 messageType 字段,但是我发现这个解决方案有点复杂。
因为我们在同一个函数中运行它,所以我们防止它并发写入,因为它要么从出口读取,要么从定时器读取,而不是同时从两者读取。
client . go—write messages 现在频繁发出 Pings
我们正在发送 Pings,我们不需要更新前端代码来响应。这是因为 RFC 规范说任何PingMessage
都应该触发一个PongMessage
来响应。支持 WebSocket 的浏览器已经自动内置,以便客户端能够响应 ping 消息。
所以Server
正在向客户端发送 Pings。客户端用 Pong 消息响应,但是现在呢?
我们需要在服务器上配置一个PongHandler
。PongHandler 是一个将在PongMessage
上触发的功能。我们将更新readMessages
来设置一个初始的PongWait
计时器,一旦启动,它将开始倒计时连接保持活动的时间。
gorilla 软件包允许我们使用SetReadDeadLine
功能轻松设置。我们将获取当前时间,添加 PongWait,并将其设置为连接。
我们还将创建一个名为pongHandler
的新函数,每当客户端接收到一个PongMessage
时,该函数将使用SetReadDeadLine
重置计时器。
client.go 添加 PongHandler 以重置 pongs 之间的时间
太好了,现在我们保持连接活跃,使网站能够长期运行而不会断开连接。
尝试重新启动您的软件,并看到服务器上的日志打印 Pong 和 Pong。
我们已经实现了大部分的东西,现在是一些安全的时候了。
限制邮件大小
限制邮件大小很重要
安全的一个规则是总是预期恶意使用。如果人们可以,他们会的。因此,有一件事是一定要做的,那就是限制服务器上允许处理的最大消息大小。
这是为了避免恶意用户向 DDOS 发送大量帧或在您的服务器上做其他坏事。
Gorilla 使用接受所允许的字节数的SetReadLimit
使得在后端配置变得非常容易。如果消息大于限制,它将关闭连接。
您必须知道邮件的大小,这样您就不会限制正确使用应用程序的用户。
在聊天中,我们可以对前端施加字符限制,然后指定与最大消息匹配的最大大小。
我将设置一个限制,每条消息最多可以有 512 个字节。
client.go 设置最大读取限制可防止产生大量帧
如果您重新启动并尝试发送长消息,连接将关闭。
原产地检查
检查始发位置很重要
目前,我们允许来自任何地方的连接连接到我们的 API。这不是很好,除非那是你想要的。
通常,你有一个托管在某个服务器上的前端,并且那个域是唯一允许连接的源。这样做是为了防止跨站点请求伪造。
为了处理来源检查,我们可以构建一个接受 HTTP 请求的函数,并使用一个简单的字符串检查来查看来源是否被允许。
这个函数必须跟在签名func(r *http.Request) bool
之后,因为将常规 HTTP 请求升级为 HTTP 连接的upgrader
有一个字段接受这样的函数。在允许连接升级之前,它将根据请求执行我们的函数来验证来源。
manager.go 向 HTTP 升级程序添加了原点检查
如果您想测试它,您可以将 switch 语句中的端口改为除了8080
之外的端口,并尝试访问 UI。你应该会看到它崩溃了,并显示一条消息request origin not allowed
。
证明
验证 WebSocket 连接
API 的一个重要部分是,我们应该只允许能够进行身份验证的用户。
WebSocket 没有内置任何身份验证工具。不过这不是问题。
在允许用户建立 WebSocket 连接之前,我们将在serveWS
函数中对用户进行身份验证。
有两种常见的方法。它们都有一定的复杂性,但不会破坏交易。很久以前,您可以通过在 Websocket 连接 URL 中添加user:password
来通过常规的基本认证。这已经被否决了一段时间了。
有两种推荐的解决方案
- 进行身份验证的常规 HTTP 请求返回一次性密码(OTP ),该密码可用于连接到 WebSocket 连接。
- 连接 WebSocket,但是不要接受任何消息,直到发送了一个带有凭据的特殊身份验证消息。
我更喜欢第一个选项,主要是因为我们不希望机器人发送垃圾连接。
所以流量会是
- 用户使用常规 HTTP 进行身份验证,OTP 票证返回给用户。
- 用户使用 URL 中的 OTP 连接到 WebSocket。
为了解决这个问题,我们将创建一个简单的 OTP 解决方案。注意,这个解决方案非常简单,使用官方 OTP 包等可以做得更好,这只是为了展示这个想法。
我们将制作一个RetentionMap
,它是一个保存 OTP 的简单地图。任何超过 5 秒的 OTP 都将被丢弃。
我们还必须创建一个新的login
端点,接受常规的 HTTP 请求并验证用户。在我们的例子中,认证将是一个简单的字符串检查。在实际的生产应用程序中,您应该用实际的解决方案来代替身份验证。涵盖认证本身就是一篇完整的文章。
需要更新serveWS
,以便一旦用户调用它,它就验证 OTP,并且我们还需要确保前端沿着连接请求发送 OTP。
让我们从改变前端开始。
我们想创建一个简单的登录表单,并呈现它,以及显示我们是否连接的文本。所以我们将从更新index.html
主体开始。
index.html—在正文中添加了登录表单
接下来,我们将在 document onload 事件中删除 WebSocket 连接。因为我们不想在用户登录之前尝试连接。
我们将创建一个接受 OTP 输入的connectWebsocket
函数,它被附加为 GET 参数。我们不将其作为 HTTP 头或 POST 参数添加的原因是,浏览器中可用的 WebSocket 客户端不支持它。
我们还将更新onload
事件,为 loginform 分配一个处理程序。该处理程序将向/login
发送一个请求,并等待 OTP 返回,一旦返回,它将触发一个 WebSocket 连接。认证失败只会发送一个警报。
使用onopen
和onclose
我们可以向用户打印出正确的连接状态。更新index.html
中的脚本部分,使其具有以下功能。
index.html—在脚本部分添加支持 OTP 的 websocket
您现在可以尝试前端,当您尝试登录时应该会看到一个警告。
在将这些更改应用到前端之后,是时候让后端验证 OTP 了。有许多方法可以创建 OTP,有一些库可以帮助您。为了使本教程简单,我编写了一个非常基本的助手类,它为我们生成 OTP,一旦它们过期就删除它们,并帮助我们验证它们。有更好的方法来处理 OTP。
我创建了一个名为otp.go
的新文件,其中包含以下要点。
otp.go —删除过期密钥的保留映射
我们需要更新管理器来维护一个RetentionMap
,我们可以用它来验证serveWS
中的 OTP,并在用户使用/login
登录时创建新的 OTP。我们将保持期设置为 5 秒,我们还需要接受一个context
,这样我们就可以取消底层的 goroutine。
manager.go —更新了结构以具有 RetentionMap
接下来,我们需要实现在/login
上运行的处理程序,这将是一个简单的处理程序。您应该用一个真正的登录验证系统来替换身份验证部分。我们的处理程序将接受带有username
和password
的 JSON 格式的有效载荷。
如果用户名是percy
而密码是123
,我们将生成一个新的 OTP 并返回,如果不匹配,我们将返回一个未授权的 HTTP 状态。
我们还更新了serveWS
来接受一个otp
GET 参数。
manager.go —更新了 ServeWS 和登录以处理 OTP
最后,我们需要更新main.go
来托管login
端点,并向管理器传递一个上下文。
main.go —向管理器添加登录端点和取消上下文
一旦您完成了所有这些工作,您现在应该能够使用前端了,但是只有在您成功地使用了登录表单之后。
试试看,按Send Message
什么都不会。但是在你登录之后,你可以查看它正在获取消息的 WebSocket。
我们将只把事件打印到控制台,但是我们会到达那里。还有最后一个安全问题。
使用 HTTPS 和 WSS 加密流量
加密流量
让我们明确一件重要的事情,现在我们正在使用明文流量,投入生产的一个非常重要的部分是使用 HTTPS。
为了让 WebSockets 使用 HTTPS,我们可以简单地将协议从ws
升级到wss
。WSS 是 WebSockets Secure 的首字母缩写。
打开index.html
,更换connectWebsocket
中的连接部分,使用 WSS。
index.html 将 wss 协议添加到连接字符串中
如果你现在试用 UI,它将无法连接,因为后端不支持 HTTPS。我们可以通过向后端添加证书和密钥来解决这个问题。
如果您没有自己的证书,请不要担心,我们将自行签署一个证书,以便在本教程中使用。
我已经创建了一个使用 OpenSSL 创建自签名证书的小脚本。你可以在他们的 Github 上看到安装说明。
创建一个名为gencert.bash
的文件,如果你使用的是 Windows,你可以手动运行命令。
gencert.bash —创建自签名证书
执行命令或运行 bash 脚本。
bash gencert.bash
你会看到两个新文件,server.key
和server.crt
。你不应该分享这些文件。把它们存储在一个更好的地方,这样你的开发者就不会不小心把它们推到 GitHub 上(相信我,这种情况会发生,人们有发现这些错误的机器人)
我们创建的证书只能用于开发目的
一旦有了这些,我们将不得不更新main.go
文件来托管 HTTP 服务器,使用证书来加密流量。这是通过使用ListenAndServeTLS
而不是ListenAndServe
来完成的。它的工作原理是一样的,但是也接受一个证书文件和一个密钥文件的路径。
main.go —使用 ListenAndServeTLS 而不是使用 HTTPs
不要忘记更新originChecker
以允许 HTTPS 域。
manager.go —必须更新源以支持 https
使用go run *.go
重启服务器,这一次,改为访问 https 站点。
根据您的浏览器,您可能不得不接受该域是不安全的
您可能会看到如下所示的错误
2022/09/25 16:52:57 http: TLS handshake error from [::1]:51544: remote error: tls: unknown certificate
这是一个远程错误,这意味着它正从客户端发送到服务器。这说明浏览器无法识别您的证书提供商(您),因为它是自签名的。不用担心,因为这是一个自签名证书,只在开发中使用。
如果您使用的是真实的证书,您将不会看到该错误。
恭喜你,你现在使用的是 HTTPS,你的 Websocket 使用的是 WSS。
实现一些事件处理程序
在我们结束本教程之前,我希望我们实现实际的事件处理程序,以使聊天正常工作。
我们只实现了关于 WebSockets 的框架。是时候根据处理程序实现一些业务逻辑了。
我不会涵盖更多的架构原则或关于 WebSockets 的信息,我们只会通过最终确定获得一些实际操作,不会太多。希望您将看到使用这种事件方法向 WebSocket API 添加更多的处理程序和逻辑是多么容易。
让我们从更新manager.go
开始,在setupEventHandlers
中接受一个实函数。
manager.go 改为将 SendMessageHandler 作为输入添加到事件处理程序中。
我们希望实现SendMessageHandler
,它应该在传入事件中接受一个有效载荷,对其进行编组,然后将其输出到所有其他客户端。
在event.go
里面我们可以添加以下内容。
event.go —添加用于广播消息的真实处理程序
这就是我们在后端需要做的一切。我们必须清理前端,以便 javascript 以想要的格式发送有效载荷。所以让我们用 JavaScript 添加相同的类,并在事件中发送它。
在index.html
的脚本部分的顶部,为两种事件类型添加类实例。它们必须匹配event.go
中的结构,以便 JSON 格式是相同的。
index.html——在脚本标签中,我们定义了两个类
然后我们必须更新当有人发送新消息时触发的sendMessage
函数。我们必须让它发送正确的有效载荷类型。
这应该是一个SendMessageEvent
有效载荷,因为这是服务器中的处理程序所期望的。
index.html——sendMessage 现在发送正确的有效载荷
最后,一旦客户端收到消息,我们应该将它打印到文本区域,而不是控制台。让我们更新routeEvent
以期待一个NewMessageEvent
并将其传递给一个函数,该函数将消息追加到文本区域。
index.html—已添加,以便客户端在收到消息后打印该消息
您现在应该能够在客户端之间发送消息了,您可以轻松地尝试一下。打开两个浏览器选项卡上的 UI,登录开始和自己聊天,但是不要熬夜!
我们可以很容易地修复它,这样我们就可以管理不同的聊天室,这样我们就不会把所有的消息都发给每个人。
让我们从在index.html
中添加一个新的ChangeRoomEvent
开始,并更新用户已经切换聊天室的聊天。
index.html—增加了更衣室事件和逻辑
将manager.go
中的新 ChangeEvent 添加到setupEventHandlers
中,以处理这个新事件。
manager.go —添加了 chattroomevent & chattroomhandler
我们可以在客户端结构中添加一个chatroom
字段,这样我们就可以知道用户选择了哪个聊天室。
client.go 添加聊天室字段
在event.go
中,我们将添加ChatRoomHandler
,它将简单地覆盖客户端中新的chatroom
字段。
我们还将确保SendMessageHandler
在发送事件之前检查其他客户端是否在同一房间。
event.go 添加了 ChatRoomHandler
太好了,我们知道这是一个多么优秀的聊天应用程序,它允许用户切换聊天室。
您应该访问 UI 并尝试一下!
结论
在本教程中,我们为 Websocket 服务器构建了一个完整的框架。
我们有一个以安全、可伸缩和可管理的方式接受 WebSockets 的服务器。
我们涵盖了以下几个方面
- 如何连接 WebSockets
- 如何有效地向 WebSockets 读写消息?
- 如何用 WebSockets 构建 go 后端 API
- 如何为易于管理的 WebSocket API 使用基于事件的设计?
- 如何使用名为乒乓的心跳技术保持联系
- 如何通过限制消息大小来避免巨型帧,从而避免用户利用 WebSocket。
- 如何限制 WebSocket 允许的源
- 如何通过实现 OTP 票务系统在使用 WebSockets 时进行身份验证
- 如何将 HTTPS 和 WSS 添加到 WebSockets?
我坚信本教程涵盖了在开始使用 WebSocket API 之前需要学习的所有内容。
如果您有任何问题、想法或反馈,我强烈建议您联系我们。
我希望你喜欢这篇文章,我知道我喜欢。
匹配,加权,还是回归?
原文:https://towardsdatascience.com/matching-weighting-or-regression-99bf5cffa0d9
因果数据科学
理解和比较条件因果推理分析的不同方法
封面图片,由我的好朋友 Chiara 绘制
A/B 检验或随机对照试验是因果推断中的金标准。通过随机将单位暴露于治疗,我们确保平均而言,治疗和未治疗的个体是可比较的,并且我们观察到的任何结果差异都可以单独归因于治疗效果。
然而,通常治疗组和对照组并不完全可比。这可能是由于随机化不完善或不可用的事实。出于伦理或实践的原因,随机处理并不总是可能的。即使我们可以,有时我们没有足够的个人或单位,所以群体之间的差异是可以捕捉的。这种情况经常发生,例如,当随机化不是在个体水平上进行,而是在更高的聚合水平上进行,如邮政编码、县甚至州。
在这些情况下,如果我们有足够的关于个体的信息,我们仍然可以恢复治疗效果的因果估计。在这篇博文中,我们将介绍并比较不同的程序,以评估治疗组和对照组之间存在的完全可观察到的不平衡的因果关系。特别是,我们将分析加权、匹配和回归过程。
例子
假设我们有一个关于统计和因果推理的博客。为了改善用户体验,我们正在考虑发布一个黑暗模式,我们想了解这个新功能是否会增加用户在我们博客上花费的时间。
对照组(左)和治疗组(右)的网站视图,图片由作者提供
我们不是一家成熟的公司,因此我们不进行 A/B 测试,而是简单地释放黑暗模式,我们观察用户是否选择它以及他们在博客上花费的时间。我们知道可能会有选择:偏好黑暗模式的用户可能会有不同的阅读偏好,这可能会使我们的因果分析变得复杂。
我们可以用下面的 有向无环图(DAG) 来表示数据生成过程。
数据生成过程的 DAG,按作者排序的图像
我们使用来自[src.dgp](https://github.com/matteocourthoud/Blog-Posts/blob/main/notebooks/src/dgp.py)
的数据生成过程dgp_darkmode()
生成模拟数据。我还从[src.utils](https://github.com/matteocourthoud/Blog-Posts/blob/main/notebooks/src/utils.py)
引进了一些绘图函数和库。
from src.utils import *
from src.dgp import dgp_darkmodedf = dgp_darkmode().generate_data()
df.head()
数据快照,图片由作者提供
我们有 300 名用户的信息,我们观察他们是否选择了dark_mode
(治疗)、他们每周的read_time
(感兴趣的结果)以及一些特征,如gender
、age
和以前在博客上花费的总hours
。
我们想估计一下新的dark_mode
对用户read_time
的影响。如果我们运行一个 A/B 测试 或随机对照试验,我们可以比较使用和不使用黑暗模式的用户,我们可以将平均阅读时间的差异归因于dark_mode
。让我们看看我们会得到什么数字。
np.mean(df.loc[df.dark_mode==True, 'read_time']) - np.mean(df.loc[df.dark_mode==False, 'read_time'])-0.4446330948042103
选择dark_mode
的人平均每周花在博客上的时间少了 0.44 小时。我们应该断定dark_mode
是一个坏主意吗?这是因果关系吗?
我们没有随机化dark_mode
,因此选择它的用户可能无法与没有选择它的用户直接比较。我们能证实这种担忧吗?部分地。在我们的设置中,我们只能检查我们观察到的特征、gender
、age
和总计hours
。我们无法检查用户是否在我们没有观察到的其他维度上存在差异。
让我们使用优步[causalml](https://causalml.readthedocs.io/)
包中的create_table_one
函数来生成一个协变量平衡表,包含我们在治疗组和对照组中可观察特征的平均值。顾名思义,这应该永远是你在因果推断分析中呈现的第一张表。
from causalml.match import create_table_one
X = ['male', 'age', 'hours']
table1 = create_table_one(df, 'dark_mode', X)
table1
平衡表,作者图片
治疗组(dark_mode
)和对照组之间似乎存在一些差异。特别是,选择dark_mode
的用户更年轻,花在博客上的时间更少,他们更有可能是男性。
另一种同时观察所有差异的方法是使用成对的小提琴手。成对紫线图的优点是它允许我们观察变量的完整分布(通过核密度估计近似)。
变量分布,按作者分类的图像
violinplot 的洞察力非常相似:似乎选择了dark_mode
的用户和没有选择的用户是不同的。
我们为什么关心?
如果我们不控制可观察的特征,我们就无法估计真实的治疗效果。简而言之,我们不能确定结果的差异(T3)可以归因于治疗(T4),而不是其他特征。例如,可能男性阅读较少,也更喜欢dark_mode
,因此我们观察到负相关,即使dark_mode
对read_time
没有影响(甚至是正相关)。
就有向无环图而言,这意味着我们有一个 后门路径 ,我们需要阻塞以便我们的分析是因果。
无条件分析的 DAG,图片由作者提供
我们如何阻止后门路径?通过对这些中间变量进行分析,在我们的例子中是gender
。条件分析允许我们恢复dark_mode
对read_time
的因果关系。进一步调整age
和hours
的分析,可以提高我们估计的精度,但不会影响结果的因果解释。
条件分析的 DAG,按作者排序的图像
我们如何在gender
、age
和hours
上对进行分析?我们有一些选择:
- 匹配
- 倾向评分权重
- 带控制变量的回归
让我们一起来探索和比较一下吧!
条件分析
我们假设对于一组受试者 i = 1,…,n ,我们观察到一个元组 (Dᵢ,Yᵢ,Xᵢ) 包括
- 一个治疗分配 Dᵢ ∈ {0,1} (
dark_mode
) - 一个回应 Yᵢ ∈ ℝ (
read_time
) - 一个特征向量 Xᵢ ∈ ℝⁿ (
gender
、age
和hours
)
假设 1:不可发现性(或可忽略性,或对可观察性的选择)
无根据假设,作者的图像
即以可观察特征 X 为条件,治疗分配 D 几乎是随机的。我们实际假设的是,没有我们没有观察到的其他特征会影响用户是否选择dark_mode
和他们的read_time
。这是一个强假设,我们观察到的个人特征越多,这个假设就越有可能得到满足。
假设 2:重叠(或共同支撑)
重叠假设,作者图片
即没有观察结果被确定地分配给治疗组或对照组。这是一个更技术性的假设,基本上意味着对于任何级别的gender
、age
或hours
,可能存在选择dark_mode
的个体和不选择dark_mode
的个体。与未发现假设相反,重叠假设是可检验的。
相称的
执行条件分析的第一个也是最直观的方法是匹配。
搭配的思路很简单。例如,由于我们不确定男性和女性用户是否可以直接比较,所以我们在性别范围内进行分析。我们不是在整个样本中比较read_time
和dark_mode
,而是针对男性和女性用户分别进行比较。
df_gender = pd.pivot_table(df, values='read_time', index='male', columns='dark_mode', aggfunc=np.mean)
df_gender['diff'] = df_gender[1] - df_gender[0]
df_gender
不同性别、不同作者的平均阅读时间差异
现在dark_mode
的效果似乎颠倒了:对男性用户来说是负的(-0.03),但对女性用户来说是正的且更大(+1.93),这表明了正的聚合效应,1.93 - 0.03 = 1.90(假设性别比例相等)!这种符号颠倒是辛普森悖论的一个非常经典的例子。
对于gender
,这个比较很容易执行,因为它是一个二进制变量。由于多个变量可能是连续的,匹配变得更加困难。一个常见的策略是使用某种最近邻算法将治疗组中的用户与对照组中最相似的用户进行匹配。我不会在这里深入算法细节,但是我们可以用causalml
包中的NearestNeighborMatch
函数来执行匹配。
NearestNeighborMatch
功能生成一个新的数据集,其中治疗组中的用户已与对照组中的用户 1:1 匹配(选项ratio=1
)。
from causalml.match import NearestNeighborMatch
psm = NearestNeighborMatch(replace=True, ratio=1, random_state=1)
df_matched = psm.match(data=df, treatment_col="dark_mode", score_cols=X)
现在两个群体是不是更有可比性了?我们可以生产新版本的平衡表。
table1_matched = create_table_one(df_matched, "dark_mode", X)
table1_matched
匹配后的平衡表,图片由作者提供
现在两组之间的平均差异已经缩小了至少几个数量级。但是,请注意样本大小略有减少(300 → 208 ),因为(1)我们只匹配经过治疗的用户,而(2)我们无法为所有用户找到良好的匹配。
我们可以用成对的小提琴手来观察分布的差异。
plot_distributions(df_matched, X, "dark_mode")
匹配后控制变量的分布,按作者排列的图像
可视化匹配前后协变量平衡的一种流行方法是平衡图,它基本上显示了每个控制变量匹配前后的标准化平均差异。
平衡情节,作者图像
正如我们所看到的,现在两组之间所有可观察特征的差异基本上为零。我们也可以使用其他度量或测试统计来比较这些分布,例如 Kolmogorov-Smirnov 测试统计。
我们如何估计治疗效果?我们可以简单地在手段上有所不同。一种自动提供标准误差的等效方法是对治疗结果dark_mode
进行线性回归read_time
。
请注意,由于我们已经为每个接受治疗的用户执行了匹配*,我们估计的治疗效果是接受治疗者(ATT)的**平均治疗效果,*如果接受治疗的样本不同于总体样本(很可能是这种情况,因为我们首先在进行匹配),则平均治疗效果可能不同。
smf.ols("read_time ~ dark_mode", data=df_matched).fit().summary().tables[1]
匹配后回归结果,按作者排序的图像
这种影响现在是积极的,在 5%的水平上有统计学意义。
注意我们可能将多个治疗过的用户与同一个未治疗的用户进行了匹配,这违反了跨观察的独立性假设,进而扭曲了推论。我们有两种解决方案:
- 聚类预匹配个体水平的标准误差
- 通过引导程序计算标准误差(首选)
我们通过原始的个体标识符(数据帧索引)来实现第一和集群标准误差。
smf.ols("read_time ~ dark_mode", data=df_matched)\
.fit(cov_type='cluster', cov_kwds={'groups': df_matched.index})\
.summary().tables[1]
聚类 SEs 的匹配后回归结果,按作者排序的图像
现在这种影响在统计学上不那么显著了。
倾向得分
Rosenbaum 和 Rubin (1983) 证明了一个非常有力的结果:如果强可忽略性假设成立,则足以使分析以治疗的概率倾向得分为条件,以便具有条件独立性。
罗森鲍姆和鲁宾(1983)的结果,由作者图像
其中 e(Xᵢ) 是个体 i 接受治疗的概率,给定可观察特征 Xᵢ 。
倾向得分,按作者分类的图像
注意在 A/B 测试中,个人的倾向得分是不变的。
Rosenbaum 和 Rubin (1983 年)的结果令人难以置信地强大而实用,因为倾向得分是一个一维变量,而控制变量 X 可能是非常高维的。
在上面介绍的不成立假设下,我们可以将平均治疗效果改写为
平均治疗效果,图片由作者提供
注意,这个平均治疗效果的公式并不取决于潜在的结果yᵢ⁽⁾yᵢ⁽⁰⁾,而仅仅取决于观察到的结果 Yᵢ 。
这个平均治疗效果的公式意味着反向倾向加权(IPW) 估计量,它是平均治疗效果 τ 的无偏估计量。
平均治疗效果的 IPW 估计值,图片由作者提供
这个估计量不可行,因为我们没有观察到倾向得分e(xᵢ】。但是,我们可以估计它们。实际上, Imbens,Hirano,Ridder (2003) 表明,即使你知道真实值(例如,因为你知道抽样程序),你也应该使用估计的倾向得分。这个想法是,如果估计的倾向分数与真实的不同,这可以在估计中提供信息。
估计概率有几种可能的方法,最简单最常见的是 逻辑回归 。
from sklearn.linear_model import LogisticRegressionCVdf["pscore"] = LogisticRegressionCV().fit(y=df["dark_mode"], X=df[X]).predict_proba(df[X])[:,1]
最佳实践是,每当我们拟合一个预测模型时,在一个不同的样本上拟合该模型,相对于我们用于推断的样本。这种做法通常被称为交叉验证或交叉拟合。最好的(但计算代价昂贵的)交叉验证程序之一是留一法(LOO) 交叉拟合:当预测观察值 *i、*时,我们使用除了 i 之外的所有观察值。我们使用[skearn](https://scikit-learn.org/)
包中的cross_val_predict
和LeaveOneOut
函数实现 LOO 交叉拟合过程。
from sklearn.model_selection import cross_val_predict, LeaveOneOutdf['pscore'] = cross_val_predict(estimator=LogisticRegressionCV(),
X=df[X],
y=df["dark_mode"],
cv=LeaveOneOut(),
method='predict_proba',
n_jobs=-1)[:,1]
在估计倾向分数后,一个重要的检查是在治疗组和对照组之间绘制倾向分数。首先,我们然后可以观察两组是否平衡,这取决于两个分布有多接近。此外,我们还可以检查满足重叠假设的可能性。理想情况下,两种分布应该跨越相同的时间间隔。
sns.histplot(data=df, x='pscore', hue='dark_mode', bins=30, stat='density', common_norm=False).\
set(ylabel="", title="Distribution of Propensity Scores");
倾向得分分布,按作者分类的图像
正如预期的那样,治疗组和对照组之间的倾向得分分布显著不同,这表明两组几乎没有可比性。然而,这两个分布跨越了相似的区间,表明重叠假设可能得到满足。
我们如何估计平均治疗效果?
一旦我们计算出倾向得分,我们只需要用各自的倾向得分对观察值进行加权。然后,我们可以计算加权的read_time
平均值之间的差值,或者使用wls
函数(加权最小二乘法)对dark_mode
进行加权回归read_time
。
w = 1 / (df["pscore"] * df["dark_mode"] + (1-df["pscore"]) * (1-df["dark_mode"]))
smf.wls("read_time ~ dark_mode", weights=w, data=df).fit().summary().tables[1]
IPW 回归结果,图片由作者提供
在 5%的水平上,dark_mode
的影响现在是积极的,几乎具有统计学意义。
请注意wls
函数会自动对权重进行归一化,使其总和为 1,这大大提高了估计器的稳定性。事实上,当倾向分数接近 0 或 1 时,非标准化的 IPW 估计量可能非常不稳定。
控制变量回归
我们今天要复习的最后一种方法是带控制变量的线性回归。这个估计器非常容易实现,因为我们只需要将用户特征— gender
、age
和hours
—添加到dark_mode
上的read_time
的回归中。
smf.ols("read_time ~ dark_mode + male + age + hours", data=df).fit().summary().tables[1]
条件回归结果,按作者排序的图像
平均治疗效果再次为阳性,在 1%的水平上具有统计学显著性!
比较
不同的方法如何与相互关联?
IPW 和回归
IPW 估计量和有协变量的线性回归之间有一个紧密联系。当我们有一个一维的离散协变量 X 时,这一点尤其明显。
在这种情况下,IPW 的估计量(即 IPW 估计的数量)由下式给出
IPW 估算要求的等效公式,图片由作者提供
IPW 估计值是治疗效果 τₓ 的加权平均值,其中权重由治疗概率给出。
有控制变量的线性回归的要求是
OLS 需求的等效公式,作者图片
OLS 估计量是治疗效果 τₓ 的加权平均值,其中权重由治疗概率的方差给出。这意味着线性回归是一个加权的估计量,它对具有我们观察到的治疗可变性更大的特征的观察值给予更大的权重。由于二进制随机变量在其期望值为 0.5 时具有最高的方差, OLS 对具有我们观察到的治疗组和对照组之间 50/50 分割的特征的观察值给予最大的权重。另一方面,如果对于某些特征,我们只观察经过治疗或未经治疗的个体,那么这些观察结果的权重将为零。我推荐安格里斯特和皮施克(2009) 的第三章了解更多细节。
IPW 和匹配
正如我们在 IPW 一节中所看到的,Rosenbaum 和 Rubin (1983) 的结果告诉我们,我们不需要对所有协变量 X 进行条件分析,但对倾向得分 e(X) 进行条件分析就足够了。
我们已经看到了这个结果是如何暗示一个加权估计量的,但它也延伸到匹配:我们不需要匹配所有协变量 X 上的观察值,但在倾向得分 e(X) 上匹配它们就足够了。这种方法被称为倾向得分匹配**。**
psm = NearestNeighborMatch(replace=False, random_state=1)
df_ipwmatched = psm.match(data=df, treatment_col="dark_mode", score_cols=['pscore'])
和以前一样,在匹配之后,我们可以简单地将估计值计算为均值差,记住观察值不是独立的,因此我们在进行推断时需要谨慎。
smf.ols("read_time ~ dark_mode", data=df_ipwmatched)\
.fit(cov_type='cluster', cov_kwds={'groups': df_ipwmatched.index})\
.summary().tables[1]
匹配回归结果的倾向得分,按作者排序的图片
dark_mode
的估计效果为正,在 1%水平显著,非常接近真实值 2!
结论
在这篇博文中,我们已经看到了如何使用不同的方法执行条件分析。匹配直接匹配治疗组和对照组中最相似的单位。加权只是根据接受治疗的概率,对不同的观察值赋予不同的权重。相反,回归根据条件处理方差对观察值进行加权,对具有处理组和对照组共有特征的观察值给予更大的权重。
这些程序非常有帮助,因为它们可以让我们从(非常丰富的)观察数据中估计因果关系,或者在随机化不完美或我们只有少量样本时纠正实验估计。
最后但同样重要的是,如果你想了解更多,我强烈推荐这个来自保罗·戈德史密斯-平克姆的关于倾向分数的视频讲座,它可以在网上免费获得。
整个课程是的瑰宝,能够在网上免费获得如此高质量的资料是一种难以置信的特权!
参考
[1] P. Rosenbaum,D. Rubin,因果效应观察研究中倾向评分的核心作用 (1983 年),生物计量学。
[2] G. Imbens,K. Hirano,G. Ridder,利用估计倾向得分对平均治疗效果的有效估计 (2003),计量经济学。
[3] J .安格里斯特,J. S .皮施克,大多无害的计量经济学:一个经验主义者的伴侣 (2009),普林斯顿大学出版社。
相关文章
- 理解弗里希-沃-洛弗尔定理
- 如何比较两个或多个分布
- Dag 和控制变量
密码
你可以在这里找到 Jupyter 的原始笔记本:
https://github.com/matteocourthoud/Blog-Posts/blob/main/notebooks/ipw.ipynb
感谢您的阅读!
我真的很感激!🤗如果你喜欢这个帖子并且想看更多,可以考虑 关注我 。我每周发布一次与因果推断和数据分析相关的主题。我尽量让我的帖子简单而精确,总是提供代码、例子和模拟。
还有,一个小小的 免责声明 :我写作是为了学习所以出错是家常便饭,尽管我尽力了。当你发现他们的时候,请告诉我。也很欣赏新话题的建议!
物化视图:提取洞察力的经济有效的方法
为什么不应该对每个任务都使用常规视图
照片由 Karolina Grabowska 拍摄:https://www . pexels . com/photo/style-round-glasses-with-optical-lenses-4226865/
很容易习惯于将 SQL 中的视图作为一种方便的工具。视图是通用的,可以省去用户重复编写复杂查询的麻烦。
也就是说,它们并不总是这项工作的最佳工具。
事实上,根据用例的不同,视图可能不如被低估的替代品:物化视图。
在这里,我们提供了物化视图的简要概述,讨论了物化视图胜过视图的方法,并解释了为什么拥抱物化视图对您最有利,即使到目前为止没有物化视图您也能管理得很好。
物化视图
物化视图类似于视图,但是它们之间有明显的区别。
与存储查询的视图不同,物化视图存储查询的输出。
这似乎是一个微不足道的区别。毕竟,如果返回相同的信息,存储查询输出而不是查询会有什么不同呢?
利益
当计算是决定性因素时,物化视图可以胜过视图。物化视图的关键好处是它们不需要太多的计算,因为查询结果已经预先确定了。
图像处理对大量数据执行分析,其中通过连接和聚合定期获得相同的信息。这种方法是有效的,但是重复运行相同的查询来获取相同的信息只是浪费计算。
使用物化视图,用户可以提取相同的信息,而不需要重复运行计算密集型查询。
限制
当然,物化视图也有自己的缺点。
因为它们只存储查询输出,所以不会根据底层数据库的变化进行调整。这可能是一个问题,因为物化视图可能会报告可能过时的值。
为了使物化视图保持可用,无论何时数据被以多种方式修改,都必须手动更新它们。
此外,与视图不同,物化视图需要内存使用,因为它们包含的查询结果存储在磁盘上。
个案研究
在 SQL 中创建和维护物化视图的过程与视图非常相似,但是有一些区别值得一提。因此,让我们进行一个简单的案例研究来演示物化视图。
我们有一个名为Animals
的简单表格,显示了动物的记录和它们对应的年龄。
代码输出(由作者创建)
假设我们计划在整个研究中经常检查每种动物的数量。我们可以编写生成该计数的查询,并将其存储在名为animal_count
的物化视图中。
有了这个物化视图,我们可以随时检查动物数量。
代码输出(由作者创建)
因为现在查询输出是预先计算的,所以现在通过物化视图访问它将需要更少的时间和计算。
不幸的是,与运行存储查询并根据底层数据的任何变化进行调整的视图不同,物化视图需要手动刷新。
为了展示刷新物化视图的重要性,我们可以删除表中兔子的所有记录,并查看修改后物化视图显示的内容。
代码输出(由作者创建)
如输出所示,尽管已经删除了兔子的所有记录,但是兔子的数量仍然存在。这意味着物化视图现在正在报告过时的信息。
为了确保物化视图跟上对数据库所做的更改,用户必须使用REFRESH
命令。
现在,物化视图与底层数据相匹配。
代码输出(由作者创建)
为什么要使用物化视图
物化视图在语法和功能上都类似于视图。但是,它们需要额外的维护才能保持可用。那么,当用户可以坚持使用常规视图时,为什么还要依赖物化视图呢?
对于习惯于使用免费 RDBMS 系统(例如 PostgreSQL)编写查询的用户来说,这可能是一个有效的论据,在这些系统中,查询的计算需求和执行时间是无关紧要的。
这种软件的用户可以免费编写查询,并且可以养成为了方便而单独使用视图的习惯。他们可能不加选择地使用视图来运行和重新运行查询,而不考虑他们的计算需求。
不幸的是,这种方法不能很好地转化为在数据仓库中进行的分析。数据仓库(本地或云)提供了巨大的存储空间和强大的计算能力。作为回报,这些仓库根据自己的定价模式向用户收费。
由于需要更高计算量的查询会导致更高的价格,这些平台的用户需要对他们如何进行分析保持警惕,因为他们可能会因查询而收费。因此,在用数据仓库规划分析时,必须考虑价格优化。
物化视图作为从数据库中提取信息的视图的一种更经济的替代方式(假设数据不经常修改)。
此外,根据所选择的数据仓库,它们的主要缺点(即需要手动维护它们)甚至可能不是问题。许多云数据仓库(如 Snowflake)都包含在数据发生变化后自动更新物化视图的特性,而不需要用户进行任何维护工作。
结论
照片由 Unsplash 上的 Prateek Katyal 拍摄
物化视图是分析很少或没有变化的数据的最佳选择,因为它们使用户无需重新运行相同的查询就能获得信息。
为数据仓库服务付费的企业自然会采用价格友好的分析方法。
因此,即使您习惯于使用免费的 RDMBS 软件来分析数据,您也可能不得不在未来迁移到大数据解决方案,因此熟悉物化视图并接受成本优化的思维方式并没有什么坏处。
希望您已经开始理解物化视图,并将它们更多地结合到您的脚本中。但是,如果您认为需要后退一步,从总体上探索常规视图,请查看下面的文章:
我祝你在数据科学的努力中好运!
基于显微图像处理的材料数据挖掘
基于显微图像的特征提取的基本处理步骤
照片由 Joyita Bhattacharya 在 Unsplash 上拍摄
背景
在我的文章“使用 Matminer 和 Pymatgen 发现材料数据的潜力”中,我讨论了材料四面体的概念——为各种技术用途开发材料的基本框架。占据四面体顶点的重要参数是过程、结构、属性和性能。
结构的特征基于特征长度尺度,该尺度可以从几埃( 1 埃= 10 ⁻ ⁰ *米)*到几百微米 (1 微米= 10 ⁻ *⁶米)*变化。长度刻度用于在显微镜下辨别特征。使用非常强大的高分辨率显微镜,如这台,人们可以识别小到原子甚至更小的特征。
微米和亚微米尺寸的特征,称为微结构,使用不同类型的显微镜捕获为图像或显微照片。这些微观结构存储了大量信息,这些信息对于理解材料的性质和性能至关重要。我们可以使用机器学习/深度学习从这些图像中挖掘重要的特征。典型的微结构包含基于像素强度或对比度可区分的特征。一些重要的特征是分隔不同域(也称为颗粒或晶粒)的边界或界面。这些域在形状、大小、取向、大小分布和空间排列(统称为形态学)上可以变化。这些特征影响材料的性质。
作者图片:微观结构示意图
尽管可以使用其他材料表征技术来研究这些特征,但显微镜的优势在于可视化。自’*‘眼见为实’*以来,显微照片为材料的行为提供了更可信的解释。显微照片的处理揭示了定性和定量特征。比方说,如果我们想要进行颗粒尺寸和形态的定量测量,就需要对边缘进行精确的识别。此外,边缘检测也有助于研究颗粒之间的界面结构。在某些材料(尤其是合金)中,某些元素可能会在界面上偏析,从而影响强度、导电性等性能。
通过人工观察进行边缘检测是一项单调乏味的任务,并且涉及各种人为误差。为了最小化这样的错误,需要自动化这样的过程。该过程的自动化需要实施稳健的数字图像处理和数据挖掘算法。既然我已经强调了显微照片的数字图像分析的重要性,让我带你看一些基本的处理步骤。我使用了基于 python 的开源库 scikit-image 进行演示。出于同样的目的,您也可以探索 PIL 的 OpenCV。
查看图像并获得形状
下面的代码片段显示了如何读取图像并找到它的尺寸或“形状”。shape 属性以元组的形式产生图像的维度。在这张当前的显微照片中,它是(1800,1500)-高度和宽度分别是 1800 和 1500 像素。请注意,这是一个灰度图像,因为元组中的第三项没有被提及,而是采用默认值 1。
p = io.imread("Particles.jpg")
p.shape
一般处理步骤:图像的去噪、锐化和亮度调整
由 Vadim Bogulov 在 Unsplash 上拍摄的照片
用于去噪、锐化、边缘检测的图像矩阵的细化主要涉及与滤波器/核矩阵的卷积运算。卷积操作包括首先水平和垂直翻转 2D 滤波器(内核),随后是图像矩阵**的逐元素乘法和加法。**注意,在对称核矩阵的情况下,翻转是不必要的。
作者图片
这里,我将考虑两个滤波器- 高斯和中值,分别用于显示与卷积相关和不相关的图像中的噪声降低。
线性高斯滤波器通过卷积对图像矩阵进行操作。属性“sigma”是高斯滤波器中的标准偏差。sigma 值越高,越模糊。
高斯模糊随着西格玛而增加
中值滤波是一个非线性过程。当滤波器窗口沿图像矩阵滑动时,所述矩阵的中值像素值作为输出信号。为了更好地理解,示出了图形表示。
作者图片:中值过滤器的图形表示
在移除诸如盐和胡椒类型的电子噪声时,该滤波器优于高斯滤波器。虽然高斯平滑会导致边缘模糊,但中值平滑会保留边缘。
与去噪相反,我们可以使用 skimage 的*“滤镜模块”*的“*反锐化掩模”*功能锐化散焦或模糊的图像以准确识别其中的特征。下面显示了代码片段以及输入(散焦图像)和输出(锐化)图像,以供比较。
Sharpimg = filters.unsharp_mask(p, radius = 20.0, amount = 1.0)fig, ax = plt.subplots(nrows =1, ncols =2, sharex = True, figsize =(15,15))ax[0].imshow(p, cmap = 'gray')
ax[0].set_title("Original", fontsize = 10)
ax[1].imshow(Sharpimg, cmap ='gray')
ax[1].set_title("Sharpened",fontsize = 10)
代码输出
有时,调节亮度也会使图像变得清晰,以便进行特征检测,这可以使用 skimage 的*“曝光模块”的功能“adjust _ gamma”*来完成。通过应用幂律变换获得伽马校正的输出图像。
伽马校正显微照片
图像分割阈值:生成二值图像
阈值处理当我们需要根据像素强度将图像背景从前景中分离出来时,就需要用到阈值处理。例如,在超合金(用于飞机喷气发动机、燃气轮机的材料)微观结构的情况下,背景是基底金属,前景由赋予这类材料超高强度的沉淀物组成。有时,背景可能只是用于装载样品以便在显微镜下进行研究的样品架。在这种情况下,用于阈值处理的图像显示了透射电子显微镜(TEM)网格/支架上的四角颗粒。应用不同类型的阈值方法来区分背景(TEM 网格)和粒子。对于给定的显微照片,我们发现均值阈值处理比其他方法做得更好,它清晰地形成了二值图像,如代码输出片段所示。
显示不同阈值方法的代码输出
上述方法是在对图像进行进一步处理以提取有意义的定性和定量特征之前,获得无噪声图像的一些初步步骤。正如本文开头提到的,粒度是控制材料性能的重要参数/特征之一。为了从图像中估计颗粒尺寸,我们需要生动地检测颗粒的边缘。并且使用各种边缘检测滤波器,该任务变得容易。在这里,我将讨论其中的几个。
使用 Roberts、Sobel、Canny 滤波器进行边缘检测
罗伯茨 和 索贝尔 滤波器分别是 2×2 和 3×3 卷积核。这两种滤波器都具有用于水平和垂直边缘检测的 x 和 y 分量。Sobel 内核操作的代码片段以及相应的输出如下所示。
from skimage.filters import sobel,sobel_v, sobel_h
p_sobel = sobel(p_g, mode='reflect')
p_sobel_v=sobel_v(p_g)
p_sobel_h=sobel_h(p_g)
Sobel 过滤器:代码输出片段
Canny 滤波器通过平滑、Sobel 算子的梯度近似、使用滞后阈值检测边缘来执行多任务工作。函数’ canny '具有用于高斯平滑和高低阈值的 sigma 等参数。
用于检测边缘方向的 Gabor 滤波器
该线性过滤器捕捉微结构中特征的纹理或取向分布。取向分布决定了材料性能的均匀性。颗粒/晶粒的随机取向导致均匀/各向同性性质,而特定方向的取向,技术上称为优选取向,导致各向异性性质。Gabor 滤波器的复正弦分量提供了与方向相关的信息。所述滤波器的输出具有实部和虚部。想了解更多关于 Gabor 滤镜的知识,请点击 这里 。
让我们尝试生成一组具有 0°、60°、90°和 120°取向的 Gabor 滤波器,并将它们应用于显示不同取向粒子的原始透射电子显微照片。我正在分享代码片段以及下面的输出过滤图像。我们可以清楚地看到各个过滤图像中的方向*(用与粒子边缘一致的黄色虚线突出显示)*。请注意,已过滤图像的实部会显示出来。
p_gabor =[]for degree in (0, 60, 90, 120):
real,imag = filters.gabor(p, frequency=0.05, theta =(degree* (np.pi)/180))
p_gabor.append(real)
Gabor 滤波图像捕捉粒子的方向
我们也可以应用 Gabor 核来提取方向信息。生成一组不同方向的 Gabor 核,然后允许它们与图像矩阵交互,以解析相应方向的边缘,如下面的快照所示。
作者对原始图像进行了 Gabor 核处理
点击 这里 从我的 GitHub 库中获取基本图像处理的完整代码。
总结
挖掘定性和定量特征是理解材料微观结构信息属性的关键。此外,来自生物成像的数据是医疗保健行业的支柱。因此,明智地处理这些显微照片是至关重要的。
尽管有几个商业图像处理软件,为同样的目的生成你自己的算法是有成本效益和灵活性的。最重要的是,它打开了通过机器学习实现自动化的大门。
在这篇文章中,我解释了一些定性提取信息的基本处理步骤,例如检测粒子边缘和方向。然而,从显微照片中提取完整的数据还需要对相关特征进行定量评估,这将在我的下一篇文章中讨论。
直到那时,快乐的图像处理!!
通过显微照片的图像处理挖掘材料数据:第二部分
从图像测量材料特性
斯文·米克在 Unsplash 上的照片
“一图胜千言。”—阿尔伯特·爱因斯坦
背景
我的上一篇文章“通过显微照片的图像处理进行材料数据挖掘”解释了显微照片处理的一些基本和关键步骤。如前所述,显微照片捕捉材料的细微微米/亚微米尺寸特征,这些特征会显著影响材料的性能。在这里,我将讨论使用 scikit-image 对一个这样的特征——“颗粒大小”的定量评估。
参数测量
案例研究 1:应用分水岭算法从聚类中检索单个颗粒尺寸
鲍里斯·巴尔丁格在 Unsplash 上拍摄的照片
让我先展示一张不同形状和大小的颗粒重叠在一起的合成显微照片。我们首先将单个粒子从簇/聚集体中分离出来。然后我们测量这些分离出来的粒子的大小。借助分水岭算法——一种图像分割方法——实现颗粒的解聚。我正在分享上述活动的一步一步的指南。
第一步:导入必要的库并加载镜像
**# IMPORTING LIBRARIES**
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy import ndimage as ndi
from skimage import io, color
from skimage.segmentation import watershed
from skimage.feature import peak_local_max**# LOADING IMAGE**im1 = io.imread("O3.jpg")
plt.imshow(im1, cmap ='gray')
作者图片:显示集群的合成显微照片
图像有几个粒子簇,它的维数用一个元组(1254,1313,3)表示。元组的前两个元素表示高度和宽度,第三个元素告诉我们通道的数量(颜色的数量)。该图像是具有三个通道/颜色的 RGB 图像。
第二步:转换成灰度图像
我已经应用了从 skimage 的颜色模块导入的函数 rgb2gray 来将 rgb 图像转换为灰度。
第三步:阈值化得到二值图像
在使用 try_all_threshold 函数对该图像进行其他阈值方法处理之后,发现平均阈值方法最适合于该图像。
第四步:计算欧氏距离变换
欧几里德距离是图像中任意两个像素之间最近距离的度量。图像的每个像素被分类为源/背景像素(具有零强度)或目标像素(具有非零强度)。我们观察到从任何物体的中心到背景的欧几里得距离有一个递减的梯度。从图像中获得的距离图很好地捕捉到了这一点:
距离地图
第五步:寻找局部峰值和分水岭线
从地理上来说,分水岭是分隔流域的高地。我们将相同的概念应用于粒子/对象集群的图像分割。在这种情况下,簇中单个对象边界的像素强度构成分水岭,将它们彼此分开。
我们首先通过应用函数 peak_local_max 来定位粒子的峰值(局部最大值)的坐标。我们使用该函数的输出作为分水岭函数中的标记。我们为图像中的每个粒子标记这些标记。这些标记构成了将单个颗粒从聚集体中分离出来的分水岭。该任务的代码片段如下所示。
coords = peak_local_max(distance, min_distance =100)
mask = np.zeros(distance.shape, dtype=bool)
mask[tuple(coords.T)] = True
markers,_= ndi.label(mask)
第六步:应用分水岭算法进行图像分割
最后,应用具有诸如反距离变换图像、标记和掩模(二值图像)的参数的分水岭算法,我们获得分离的粒子。通过应用 matplotlib 颜色图,簇中这些粒子的个性以不同的颜色显示。
individual_particles = watershed(-distance, markers, mask=binary)
显示原始图像、距离图和分段图像的代码输出(应用分水岭功能后)
案例研究 2:特性测量——粒度和粒度分布
让我们继续上面的图片,我们应用分水岭算法来分离粒子和簇。我们现在开始测量颗粒尺寸。
图像中比例因子的作用
在详细讨论特征/参数值的计算之前,我们需要理解图像中的“比例的概念。任何图像中的比例因子给出了对象/特征的度量的概念。数字图像由称为像素的小点组成。像素的大小决定了用来拍照的仪器的分辨率。
根据在图像中捕获的实际物理对象的尺寸,在任何图像上提供代表以千米/厘米/毫米/微米/纳米为单位的特定值的几个像素的刻度线或条。例如,谷歌地图有一个以英里或公里为单位的比例尺。相反,显微照片具有微米/纳米的比例尺,因为它们捕捉材料的小长度尺度特征。
没有比例尺是不可能测量图像的特征的。因此,具有可量化特征的图像应该总是包含比例尺。
尺寸测量使用region propsskimage . measure 模块 功能
将 regionprops 函数应用于标记图像区域的测量代码如下所示。
from skimage.measure import label, regionprops, regionprops_table
label_img = label(individual_particles, connectivity = im2.ndim)props=regionprops(label_img, im2)for p in props:
display('label: {} area: {}'.format(p.label, p.area))
props_t = regionprops_table(label_img, im2, properties='label','area', 'equivalent_diameter','solidity'])df = pd.DataFrame(props_t)
使用 regionprops_table 函数将面积、等效直径和实度等属性制成与 pandas 兼容的表格。考虑每像素 0.005 微米的标度来计算颗粒的尺寸。
显示颗粒大小的代码输出
当显微照片中有不同尺寸的颗粒时,参数“颗粒尺寸分布”变得很重要。所有现实世界的物质都包含无数的粒子(比如多晶金属)。显微照片只捕捉到材料的一小部分代表性区域。正如本文开头提到的,颗粒大小决定了材料的几个重要属性。
现在,问题是应该考虑平均颗粒尺寸还是整个颗粒尺寸分布。平均粒度是指粒度均匀的材料,而 PSD 是粒度在一定范围内变化的材料的重要参数。
显示 D10、D50 和 D90 百分位的粒度分布图提供了大量信息,并被用作理解材料性能的指南。此外,它通过计算如下定义的“跨度”告诉我们参数的一致性。
较小的跨度值表示颗粒尺寸更加一致。另一方面,较大的跨度值意味着分布的异质性。
我已经采取了下面的模拟图像,以显示颗粒大小分布的计算。
作者图片:模拟图片
这张图片显示了大量大小不同、分离良好的单个颗粒。因为在这个图像中没有粒子群,所以应用分水岭函数的步骤是不相关的。首先,我们将此图像转换为灰度,然后进行阈值处理。然后,我们将 label 函数应用于二进制图像,生成一个带标签的图像(如下所示),用不同的颜色显示不同大小的粒子。
标签图像
应用每像素 4 纳米的标度来计算代表颗粒尺寸的**当量直径。注意,等效直径是与区域面积相同的圆的直径。我们使用 regionprops 和 regionprops_table 直接计算并列出所需的属性。然后,该表格被导出为电子表格文件。使用 Numpy 的百分位数函数,我们获得了统计参数— D10、D50 和 D90 百分位数。因此,估计大约等于 1 的跨度,这表明模拟图像的粒子尺寸不均匀。注意,跨度越接近零表示 PSD 越一致。
粒度分布可视化
我使用了Seaborn——一个统计数据可视化库,使用 distplot 函数绘制颗粒尺寸分布。该函数结合了直方图和核密度估计(KDE)函数。下图显示了模拟图像的粒度分布图。
粒度分布图
**代码:**请点击 这里可以从我的 Github 库中获取代码。
总结
定量估计是图像数据处理的基本部分,因为它有助于决策。在图像中定义比例尺对于特征的量化是强制性的。此外,在定量分析之前,图像需要经历一组细化步骤。
在这篇文章中,我通过拍摄两张图像来说明上述观点,目的是挖掘颗粒尺寸数据。我演示了一个额外的预处理步骤的必要性,即应用分水岭函数来分离第一幅图像**中有重叠颗粒的颗粒,以进行尺寸计算。**否则我们会得到大小不同的粒子群。这将完全误导对材料属性的相同解释。
这篇文章的另一个亮点是描述了颗粒尺寸分布的意义,而不是包含不同尺寸颗粒的系统(图片)的单一平均值。PSD 是具有结构异质性的材料**(例如多晶材料)的重要量化指标。**
我希望这篇文章将有助于从图像中计算可测量的参数,这些参数可以作为预测算法的输入。
感谢阅读!
每个数据科学家都应该知道的数学优化试探法
寻找最佳解决方案(在最高的顶端)。图片由 Dall-E 2 提供。
本地搜索、遗传算法等等
解决数学优化问题有许多不同的方法。您可以使用贪婪算法、约束编程、混合整数编程、遗传算法、局部搜索等等。根据问题的大小和类型,以及期望的解决方案质量,一种技术可能比另一种更好。
这篇文章概述了解决离散优化问题的不同启发式方法。首先,我解释了用数学方法描述优化问题所需的三个要素。然后,我将给出一些常见的和性能良好的搜索试探法的解释。
最优化问题
这里有一个简短的数学优化复习。为了从数学上定义一个优化问题,你需要以下三个组成部分:决策变量、约束和目标。
让我们看一个简单的例子。你是一个小邮局,你每投递一个包裹就赚不同的钱。送货车的空间有限。投递部门希望在每一轮投递中投递尽可能高的总价值。你应该递送哪些包裹?
图片作者。
决策变量
决策变量可以取不同的值。目标是找到决策变量的最佳值。最好的价值观是什么?这取决于目标和约束条件。在投递后的例子中,每个包裹都有一个二元决策变量。如果不传递包,则变量为 0,如果传递包,则变量为 1。
限制
约束是不允许的事情或界限,通过正确设置它们,你肯定会找到一个在现实生活中可以实际使用的解决方案。投递后的例子中的一个约束:你不能投递所有的包裹,因为投递车的空间有限。如果货车的最大空间等于 600,您应该添加一个约束,以确保所选的包不会超过此限制。
目标
目标是你在最优化问题中的目标,这是你想要最大化或最小化的。投递局的目标是选择最有价值的包裹进行投递。在目标中,您希望最大化所选包的总价值。
以下是该示例的完整描述:
问题的数学描述。图片作者。
如果一个问题是明确定义的(即存在一个可行解),那么对于优化问题总是存在至少一个最优解。很难找到这些最优解决方案中的一个,尤其是当问题很大很复杂的时候。并不是这篇文章中讨论的所有技术都能保证找到最佳解决方案。但是,如果您将它们正确地应用于大型问题,它们会比使用约束或混合整数编程技术的解决方案更快。
优化技术
有不同的试探法可以用来解决优化问题。在这里,我将解释其中的一些。
我假设你已经熟悉蛮力,即尝试所有可能的解决方案并跟踪最佳方案。另一个你可能知道的技术是动态编程,在这里问题被分解成更小的子问题。如果你不熟悉动态编程和暴力破解,这篇文章解释了它们。当你的问题很小时,蛮力和动态编程完全可以使用。当问题越来越大时,他们将花费太多的时间并且效率低下。蛮力和动态编程不是启发式的,因为它们不会减少搜索空间。您可以通过系统地测试一个较小的解决方案子集的所有可能的解决方案(蛮力),决定将蛮力与局部搜索或遗传算法结合起来。
贪婪算法
要解决投递局问题,一个简单的方法是从贪婪算法开始。他们给出了一个基准,并很快提供解决方案。greedy 背后的想法是,你选择一个包裹,直到送货车装满。你不选择任何包,但你可以从最有价值的包开始。你继续这样做,直到货车装满。假设货车的最大载客量为 60,以下是我们将选择的包装:
贪婪选择:最高值。图片作者。
还有其他方法来决定下一个套餐。通过将每个包装的价值和尺寸相除,可以得到每个包装的每个尺寸单位的价值。你可以把它描述为价值密度。通过选择每尺寸单位价值最高的包装,有可能提出更好的解决方案。
贪婪选择:每尺寸单位的最高值。图片作者。
贪婪的一个优点是速度快。但是对于更复杂的问题,在大多数情况下,解决方案远非最优。
本地搜索
下一个技巧很有趣。本地搜索非常直观。它是这样运作的:你从一个解决方案开始,你要通过局部行动来改进这个解决方案。这意味着你对当前的解决方案做了一点小小的改动,从而提高了目标。您继续应用局部移动,直到没有更多的移动可以提高目标。
让我们再来看看交货的例子。我们可以从一个解决方案开始:
左边是本地搜索开始解决方案,右边是其他包。图片作者。
局部移动可以是将选定的包与未选定的包交换。我们密切关注容量限制,并努力满足每次本地搬迁的需求。移动的一个例子可能是:
局部移动:交换两个包来增加目标。图片作者。
移动后,新的目标值为 115。我们从选择中删除具有较低值的包,并添加具有较高值的包,同时仍然有一个可行的解决方案。
通过应用一个局部移动,你可以达到的所有可能的解决方案被称为当前解决方案的邻域。
你不能超过货车的容量限制。所以在这种情况下,如果一个包比其他任何包都大,即使价值很高,我们也绝不会选择这个包!
陷入局部最优。图片作者。
这是本地搜索的一个缺点。你可能会陷入局部最优:
局部最优。图片作者。
有办法克服这个问题。你可以选择一次交换多个包,使之等同于一次移动。通过这样做,你的邻居增加了,你可以找到更多的解决方案。
你也可以决定从多个解决方案开始,而不是一个。然后对每个解重复交换过程,这被称为迭代局部搜索。
另一种方法是选择以一定概率使目标变得更糟的行动:
降低目标值时选择包的概率。图片作者。
如果温度参数很大,接受降级移动的可能性很高。如果温度小,这个几率就低。模拟退火就是利用了这个概率。它从高温开始,然后逐渐降低。这意味着,在开始的时候,你是在执行一个随机的解决方案。当温度降低时,搜索变成局部的。模拟退火在硬基准测试中表现出色。
禁忌搜索是我想提到的最后一种避免局部最优的技术。禁忌搜索的想法是跟踪你已经访问过的解决方案。不允许再回到他们身边。将所有以前的解决方案保存在内存中的成本会很高。相反,您可以决定存储转换或保持固定大小的禁忌列表。
可以结合迭代局部搜索、模拟退火和禁忌搜索等技术。
遗传算法
你也可以决定使用遗传算法。遗传算法的核心思想是反映自然选择的过程。解决问题的方法叫做个体。首先,从生成初始种群开始。这个群体是由个体组成的。然后,你计算每一个人的适应度。您可以将适应度函数与目标进行比较。最适合的个体(他们有最好的客观价值)被选择出来进行繁殖,以便产生后代。
对于我们的递送示例,每个包裹都是一个基因,其值可以是 0 或 1。我们有四个人的初始群体看起来像这样:
遗传算法的初始种群(4 个个体)。图片作者。
现在我们选择最适合的个体(最高的客观价值)来产生后代。在我们的例子中,个体 2 和 4 具有最高的适合度值。有不同的方法可以产生后代,一种常见的方法是随机选择一个交叉点,交换基因直到这个交叉点。
交叉:交换最佳个体的基因,直到交叉点。图片作者。
下一步是突变。我们以较低的随机概率翻转一些基因,在我们的情况下,让我们取 0.14 (1 除以 7,7 是包装的数量)。这是重要的一步,因为我们希望保持多样性,防止过早趋同。我们将变异后代 1:
突变:将一个基因按一定概率从 0 -> 1 或 1 -> 0 进行切换。图片作者。
现在,我们将计算新个体的适应度。人口数量是固定的。具有最低适应值的个体死亡。
加上后代,适应值最低的个体死亡。图片作者。
下面是完整的算法:
- 创建初始种群。
- 重复直到收敛:
- 选择中最适者的个体。
-选择一个交叉点创建后代。 - 突变基因。
-计算适应度,一些个体死亡。
- 选择中最适者的个体。
使用遗传算法,也有可能陷入局部最优。有多种方法可以克服这一点。您可以创建初始群体的子集,并在选择阶段进行随机化。这可以防止在选择过程中反复使用相同的群体。另一种避免局部极小值的方法是给存活时间更长的个体和/或比其他个体更独特的个体额外的奖励,因为他们可以帮助找到一个通用的解决方案。
混合技术
这里讨论的快速找到高质量解决方案的最后一种方法是通过组合不同的技术。
一个例子是大邻域搜索,其中局部搜索与约束规划(CP)或混合整数规划(MIP)相结合。CP 和 MIP 的缺点是它们在处理大型问题时会遇到困难,并且需要大量时间来获得最优解。通过将本地搜索与 CP 或 MIP 相结合,你就拥有了两个世界的精华。你可以用 CP 或 MIP 解决小的子问题,用局部搜索选择一个新的子问题。
大型小区搜索的步骤是:
- 从问题的可行解决方案开始。你可以使用任何你喜欢的技术找到解决方法。
- 重复直到满足一个标准:
-选择一个邻域(问题的一部分)。
-优化这个子问题(用 CP 或 MIP)。
在求解过程中,你会跟踪最佳解。例如,可以通过固定一组变量来定义邻域。
混合方法的另一个例子是 memetic 算法。模因算法结合了遗传算法和局部搜索。
以混合方式结合多种“工具”。由猎人哈利在 Unsplash 上拍摄的照片
结论
在这篇文章中,你已经看到了不同的启发式方法来解决数学优化问题。希望你能通过局部搜索、遗传算法或混合方法快速找到优化问题的解决方案!还有其他有趣的和性能良好的搜索试探法,如粒子群优化,蚁群优化和随机隧道。
有关系的
Matplotlib vs. Plotly:让我们决定一劳永逸
原文:https://towardsdatascience.com/matplotlib-vs-plotly-lets-decide-once-and-for-all-dc3eca9aa011
从 7 个关键方面进行深入快速的比较
作者的愚蠢形象
足球迷有一个令人讨厌的习惯。每当一名年轻但公认杰出的球员出现时,他们都会将他与梅西或罗纳尔多等传奇人物相提并论。他们选择忘记那些传奇人物在新手长出牙齿之前就已经统治了这项运动。将 Plotly 与 Matplotlib 进行比较,从某种意义上来说,类似于开始时的情况。
Matplotlib 从 2003 年开始被大量使用,而 Plotly 在 2014 年才刚刚问世。
此时许多人已经厌倦了 Matplotlib,所以 Plotly 因其新鲜感和交互性而受到热烈欢迎。尽管如此,该库不能指望从 Matplotlib 那里夺取 Python 绘图包之王的头把交椅。
2019 年,当 Plotly 在 7 月发布其 Express API 时,事情发生了巨大的变化。这激发了人们对图书馆的兴趣,人们开始到处使用它。
随着另一个主要版本(5.0.0)于去年 6 月发布,我认为 Plotly 已经足够成熟,可以与 Matplotlib 相提并论。
说完这些,我们开始吧:
自定义函数来绘制分数。完整的功能体可以在我创建的GitHub gist上看到。
https://ibexorigin.medium.com/membership
获得由强大的 AI-Alpha 信号选择和总结的最佳和最新的 ML 和 AI 论文:
https://alphasignal.ai/?referrer=Bex
1.API 可用性
让我们从比较它们的 API 的易用性开始。两者都提供高级和低级接口来与核心功能交互。
1.1 高级 API 的一致性(Pyplot 与 Express)
一方面,Plotly Express 在一致性方面表现出色。它只包含访问内置图的高级功能。它没有引入执行现有功能的新方法,而是一个包装器。所有对 Express 的 plot 调用都返回核心人物对象。
另一方面,PyPlot 接口将所有绘图功能和定制打包到一个新的 API 中。即使绘图调用具有相同的签名,定制功能也不同于 Matplotlib 的面向对象 API 中的功能。
这意味着如果你想切换界面,你必须花时间去了解它们的区别。
此外,在 Matplotlib 中创建 plots 会返回不同的对象。例如,plt.scatter
返回一个PathCollection
对象,而plt.boxplot
返回一个字典。这是因为 Matplotlib 为每个绘图类型实现了不同的基类。对许多人来说,这确实令人困惑。
plot_scores(mpl=0, px=1)
1.2 在 API 之间切换所需的代码量
要从 PyPlot 切换到 Matplotlib 的 OOP API,只需改变与核心数据结构的交互方式,比如figure
和axes
对象。对图的调用具有相似的签名,并且参数名称不会改变。
从 Plotly Express 切换到 Plotly Graph Objects (GO)需要一个陡峭的学习曲线。创建所有绘图的函数签名发生了变化,GO 为每个绘图调用添加了更多的参数。尽管这样做是为了引入更多的定制,但我认为情节最终会异常复杂。
另一个缺点是 GO 将一些核心参数移到了情节调用之外。例如,可以在 Plotly Express 中的绘图内直接创建对数轴。在 GO 中,您可以使用update_layout
或update_axes
功能来实现这一点。而 Matplotlib 的 PyPlot 或者 OOP API 就不是这样(参数不移动也不改名字)。
plot_scores(mpl=1, px=1)
1.3 定制 API
尽管有一个单独的关于定制的章节,但是我们必须从 API 的角度来讨论它。
Matplotlib 中的所有定制都有独立的功能。这允许你以代码块的形式改变情节,并使用循环或其他程序。
相比之下,Plotly 广泛使用字典。虽然这为您与图和数据的交互提供了一定的一致性,但它以代码可读性和长度为代价。由于许多人更喜欢update_layout
函数,它的参数经常以一大堆嵌套字典结束。
您可能会停下来思考这些 API 之间的差异,但是 Matplotlib 的 API 更 Pythonic 化,可读性更好。
plot_scores(2, 1)
2.速度
要了解速度之间的真正差异,我们必须使用更大的数据集。我将从 Seaborn 导入钻石数据集,并比较创建简单散点图所需的时间。
我将使用%%timeit
magic 命令,该命令多次运行相同的代码块来查看标准偏差误差。
测量 Matplotlib:
绘图测量:
Matplotlib 几乎比 Plotly 快 80 倍,SD 误差更低。可能这是因为 Plotly 渲染了交互剧情。让我们再次检查速度,这次关闭交互性:
不幸的是,关闭交互性并没有多大帮助。Matplotlib 在速度上碾压 Plotly:
plot_scores(3, 1)
3.支持的绘图类型数量
在这一领域,Plotly 处于领先地位。从 Plotly API 参考中,我数出了近 50 个独特的图。尤其是,Plotly 是一流的,当谈到某些类型的图表。
例如,它专门支持财务绘图,并提供了figure_factory
子包来创建更复杂的自定义图表。
另一方面,Matplotlib 有适度的情节选择。我不认为他们会与 Plotly 提供的丰富选择相匹配,即使我们添加了 Seaborn 的情节:
plot_scores(3, 2)
4.交互性
嗯,如果只有 Plotly 有这个功能,怎么比较交互性呢?
没有多少人知道这一点,但在 Jupyter 笔记本之外,默认情况下,Matplotlib 绘图以交互模式呈现。
作者截图
不幸的是,这种程度的交互性与 Plotly 相比根本不算什么。所以,让我们把普洛特利的分数提高一分:
plot_scores(3, 3)
现在,对于决胜局——除了一般的交互性,Plotly 还提供了自定义按钮:
滑块:
以及更多将整体用户体验提升到更高水平的功能。这值得再提一点:
plot_scores(3, 4)
5.自定义
对于许多数据科学家来说,定制就是一切。您可能希望根据您的项目数据创建自定义主题并使用品牌颜色(一个很好的例子可以在这里的看到用于可视化网飞数据)。
也就是说,让我们看看您可以优化的最重要的组件以及它们在包之间的差异。
5.1 颜色和调色板
Matplotlib 和 Plotly 都有专门的颜色和调色板子模块。
Matplotlib 允许用户使用颜色标签、十六进制代码、RGB 和 RGBA 系统来更改绘图元素的颜色。最值得注意的是,在mpl.colors.CSS4_COLORS
下,可以通过 100 多个 CSS 颜色标签。
Plotly 确实实现了相同的功能,但是 Matplotlib 提供了来自其他绘图软件如 Tableau 的颜色。此外,在 Matplotlib 中传递颜色和调色板不会造成混乱。
在 Plotly 中,至少有六个参数处理不同的调色板。相比之下,MPL 只有两个灵活的参数color
和cmap
,可以适应您传递的任何颜色系统或调色板。
plot_scores(4, 4)
5.2 默认样式
在临时分析期间,不需要超出默认设置。这些类型的分析通常会持续很长时间,因此这些默认设置必须能够即时生成尽可能高质量的图表。
我想我们所有人都同意 Matplotlib 默认,嗯,真糟糕。看看这两个图书馆创建的奥运会运动员身高体重散点图:
普洛特利的看起来更好。
此外,我喜欢 Plotly 坚持数据可视化最佳实践的方式,比如只在必要时使用额外的颜色。
例如,当创建条形图或箱线图时,Plotly 使用统一的颜色而不是调色板。Matplotlib 做的正好相反,它给每个条形图或箱线图着色,即使这种颜色没有给图添加新的信息。
plot_scores(4, 5)
5.3 主题
Plotly 采取这一部分仅仅是因为它有神奇的“黑暗模式”(叫我主观,我不在乎)。它看起来更舒服,给情节一种奢华的感觉(尤其是和我最喜欢的红色搭配的时候):
在朱庇特实验室看起来太光滑了!
plot_scores(4, 6)
5.4 全局设置
我花了这么长时间来集成 Plotly 的原因是它缺乏控制全局设置的功能。
Matplotlib 有rcParams
字典,您可以很容易地调整它来设置全局绘图选项。你可能会认为一个非常依赖字典的图书馆会有一个类似的字典,但是不!
Plotly 在这方面真的让我很失望。
plot_scores(5, 6)
5.5 轴
轴最重要的组成部分是刻度线和刻度标签。
老实说,直到今天,我还没有完全掌握如何在 Matplotlib 中控制 ticks。这是因为 Matplotlib 没有一致的 API 来控制轴。
你可能会责怪我不够努力,但我通过查看一次文档,就明白了我需要学习的关于控制虱子的一切。
Plotly 只有一种处理 ticks 的方式(通过update_xaxes/update_yaxes
或update_layout
)。当您在 Express 和 GO 之间切换时,这些函数不会改变,而在 Matplotlib 中,情况并非如此。
plot_scores(5, 7)
5.6 控制图表的个别方面如何?
我们必须把这个给 Matplotlib。我这样做不是因为普洛特利已经领先了,我必须保持这种阴谋。
Matplotlib 将各个组件实现为单独的类,使其 API 非常精细。更精细意味着有更多的选项来控制图中可见的对象。
以箱线图为例:
即使情节看起来空白,它允许几乎无限的定制。例如,在返回的字典的boxes
键下,您可以将每个箱线图作为面片对象进行访问:
这些对象打开了所有发生在 Matplotlib 下的神奇事物的大门。它们也不局限于箱线图,你可以从许多其他图中访问面片对象。使用这些面片对象,您可以自定义绘图中形状周围的每条线、每个角和每个点。
plot_scores(6, 7)
6.散点图
散点图在统计分析中起着举足轻重的作用。
它们用于了解相关性和因果关系,检测异常值,并在线性回归中绘制最佳拟合线。所以,我决定用一整节来比较两个图书馆的散点图。
为此,我将选择前面章节中的身高与体重散点图:
Matplotlib 的默认散点图。
我知道,看起来很恶心。然而,看着我做一些定制,把情节变成一件(好吧,我不会说艺术):
减小分笔成交点大小并降低其不透明度后的相同散点图
在应用最后一步之前,我们可以看到这些点围绕不同的行和列进行分组。让我们添加抖动,看看会发生什么:
抖动后相同的散点图。
现在比较最初的图和最后的图:
左图——初始图。右—转换后。
我们甚至不知道这些点在 Plotly 中被分组为行和列。它不允许将标记大小更改为小于默认值。
这意味着我们不能抖动分布来考虑体重和身高在离散值上四舍五入的事实。
plot_scores(7, 7)
哇!到目前为止一直是不分上下。
7.证明文件
作为最后一个组成部分和决胜局,让我们比较文档。
当我还是初学者的时候,Matplotlib 文档是我期望找到问题答案的最后一个地方*。*
首先,如果不打开其他几个链接页面,你就无法阅读文档的任何一页。文件是一个混乱的怪物。
第二,它的教程和示例脚本似乎是为学术研究人员编写的,几乎就像 Matplotlib 故意恐吓初学者一样。
相比之下,Plotly 要有条理得多。它有完整的 API 参考,并且它的教程大多是单机版的。
它并不完美,但至少看起来不错——感觉不像你在读 90 年代的报纸。
*plot_scores(7, 8)*
> 2022 年更新:Matplotlib 完全修改了它的文档,看起来非常令人愉快。我们可以称这部分为平局。
摘要
老实说,我在写这个比较的时候,完全相信 Matplotlib 最终会胜出。
当我完成一半的时候,我知道会发生相反的情况,我是对的。
Plotly 确实是一个非凡的图书馆。尽管我让我的个人偏好和偏见影响了分数,但没有人能否认这个软件包已经实现了许多里程碑,并且仍在快速发展。
这篇文章并不是要说服你放弃一个包而使用另一个。相反,我想强调每个包擅长的领域,并展示如果将这两个库都添加到您的工具箱中,您可以创建什么。
感谢您的阅读!
*https://ibexorigin.medium.com/membership https://ibexorigin.medium.com/subscribe
我的更多故事:
https://ibexorigin.medium.com/yes-these-unbelievable-masterpieces-are-created-with-matplotlib-b62e0ff2d1a8 https://ibexorigin.medium.com/how-to-use-matplotlib-annotations-like-you-know-what-you-are-doing-da61e397cce3 *