Русский
Русский
English
Статистика
Реклама

Парадокс монти холла

Правильно пишем командлеты на Powershell и заодно симулируем парадокс Монти Холла

25.12.2020 16:21:43 | Автор: admin
Хабр точно знаком с парадоксом, а вот с некоторыми фичами павершелла, вероятно, нет, поэтому тут больше про него.




Используем пайплайн в Powershell


Алгоритм прост, первым идет генератор случайных дверей, затем генератор выбора пользователя, затем логика открытия дверей ведущим, еще одно действие пользователя и подсчет статистики.

А поможет нам в этом павершельный ValueFromPipeline, который позволяет указывать командлет один за другим, трансформируя объект шаг за шагом. Вот примерно должен выглядеть наш пайплайн:

New-Doors|Select-Door|Open-Door|Invoke-UserAction

New-Doors генерирует новые двери, в команде Select-Door игрок выбирает одну из дверей, в Open-Door ведущий открывает дверь в которой точно нет козы и которая не была выбрана игроком, а в Invoke-UserAction мы симулируем разное поведение пользователя.

Объект, описывающий двери, подается слева направо постепенно преобразовываясь.

Такой метод написания кода помогает разделять его на куски с четким разделением по ответственности.

В Powershell есть свои конвенции. В том числе, конвенции по правильному наименованию функций, их тоже нужно соблюдать и мы их почти соблюдаем.

Делаем двери


Так как мы собираемся симулировать ситуацию, подробно опишем еще и двери.

Дверь содержит либо козу, либо автомобиль. Дверь может быть выбрана игроком или открыта ведущим.

class Door {    <#    Модель данных, где описана каждая дверь.     Выбрана ли она игроком и открыта ли она ведущим.    #>    [string]$Contains = "Goat"    [bool]$Selected = $false    [bool]$Opened = $false}

Каждую из дверей мы поместим в отдельное поле в отдельном классе.

class Doors {    <#    Модель данных, где описаны 3 двери    #>    [Door]$DoorOne     [Door]$DoorTwo     [Door]$DoorThree}

Можно было их поместить все двери в массив, но чем подробнее все будет описано, тем, лучше. Кстати в Powershell 7, классы, их конструкторы, методы и все остальное ООП, которое работает почти как надо, но об этом в другой раз.

Генератор случайных дверей выглядит так. Сначала для каждого дверного косяка генерируется своя дверь, а потом генератор выбирает за которой из них будет стоят автомобиль.

function New-Doors {    <#    Генератор случайных дверей.    #>    $i = [Doors]::new()     $i.DoorOne = [Door]::new()    $i.DoorTwo = [Door]::new()    $i.DoorThree = [Door]::new()     switch ( Get-Random -Maximum 3 -Minimum 0 ) {        0 {             $i.DoorOne.Contains = "Car"        }        1 {             $i.DoorTwo.Contains = "Car"        }        2 {             $i.DoorThree.Contains = "Car"        }        Default {            Write-Error "Something in door generator went wrong"            break        }    }        return $i

Наш пайп выглядит так:

New-Doors

Игрок выбирает дверь


Теперь опишем изначальный выбор. Игрок может выбрать одну из трех дверей. Для целей симуляции большего количества ситуаций, пусть игрок сможет выбирать только первую, только вторую, только третью и случайную дверь каждый раз.

[Parameter(Mandatory)][ValidateSet("First", "Second", "Third", "Random")]$Principle

Чтобы принимать аргументы из пайплайна, в блоке параметров нужно указать переменную, которая будет это делать. Делается это так:

[parameter(ValueFromPipeline)][Doors]$i

Можно писать ValueFromPipeline без True.

Вот так выглядит законченный блок выбора двери:

function Select-Door {    <#    Игрок выбирает дверь.    #>    Param (        [parameter(ValueFromPipeline)]        [Doors]$i,        [Parameter(Mandatory)]        [ValidateSet("First", "Second", "Third", "Random")]        $Principle    )        switch ($Principle) {        "First" {            $i.DoorOne.Selected = $true        }        "Second" {            $i.DoorTwo.Selected = $true        }        "Third" {            $i.DoorThree.Selected = $true        }        "Random" {            switch ( Get-Random -Maximum 3 -Minimum 0 ) {                0 {                     $i.DoorOne.Selected = $true                }                1 {                     $i.DoorTwo.Selected = $true                }                2 {                     $i.DoorThree.Selected = $true                }                Default {                    Write-Error "Something in door selector went wrong"                    break                }            }        }        Default {            Write-Error "Something in door selector went wrong"            break        }    }     return $i 

Наш пайп выглядит так:

New-Doors | Select-Door -Principle Random

Ведущий открывает дверь


Тут все очень просто. Если дверь не была выбрана игроком и если за ней коза, то меняем поле Opened на True. Конкретно в это случае называть команду словом Open не корректно, вызываемый ресурс не читается, а изменяется. В подобных случаях используйте Set, а Open оставим для наглядности.

function Open-Door {    <#    Ведущий открывает дверь с козой, но не ту, что выбрал игрок.    #>    Param (        [parameter(ValueFromPipeline)]        [Doors]$i    )    switch ($false) {        $i.DoorOne.Selected {            if ($i.DoorOne.Contains -eq "Goat") {                $i.DoorOne.Opened = $true                continue            }                   }        $i.DoorTwo.Selected {             if ($i.DoorTwo.Contains -eq "Goat") {                $i.DoorTwo.Opened = $true                continue            }                   }        $i.DoorThree.Selected {             if ($i.DoorThree.Contains -eq "Goat") {                $i.DoorThree.Opened = $true                continue            }                    }    }    return $i

Для пущей убедительности нашей симуляции мы открываем эту дверь, меняя поле .opened на $true, а не удаляем объект из массива дверей.

Не забывайте про continue в свитчах, сравнение не останавливается после первого совпадения. Coninue выходит из свитча и продолжает выполнять скрипт, а оператор break в свитче завершит работу скрипта.

Добавляем еще одну функцию в пайп, он он теперь выглядит так:

New-Doors|Select-Door-Principle Random |Open-Door

Игрок меняет выбор


Игрок либо меняет дверь, либо не меняет. В блоке параметров у нас только переменная из пайпа и булёвый аргумент.

Используйте слово Invoke в названиях таких функций, потому что Invoke означает вызов синхронной операции, а Start асинхронной, соблюдайте конвенции и рекомендации.

function Invoke-UserAction {    <#    Ситуация, где игрок менят или не меняет свой выбор.    #>    Param (        [parameter(ValueFromPipeline)]        [Doors]$i,        [Parameter(Mandatory)]        [bool]$SwitchDoor    )     if ($true -eq $SwitchDoor) {        switch ($false) {            $i.DoorOne.Opened {                  if ( $i.DoorOne.Selected ) {                    $i.DoorOne.Selected = $false                }                else {                    $i.DoorOne.Selected = $true                }            }            $i.DoorTwo.Opened {                if ( $i.DoorTwo.Selected ) {                    $i.DoorTwo.Selected = $false                }                else {                    $i.DoorTwo.Selected = $true                }            }            $i.DoorThree.Opened {                if ( $i.DoorThree.Selected ) {                    $i.DoorThree.Selected = $false                }                else {                    $i.DoorThree.Selected = $true                }            }        }      }     return $i

В операторах ветвления и сравнения, нужно первыми указывать системные и статические переменные. Вероятно, могут возникнуть сложности с приведением одного объекта к другому, но автор не сталкивался с такими трудностями, когда раньше писал по-другому.

Еще одна функция в пайплайн.

New-Doors|Select-Door-PrincipleRandom |Open-Door|Invoke-UserAction-SwitchDoor$True

Преимущество такого подхода написания ясно, ведь разделять код на части с четким разделением функций никогда не было так удобно.

Поведение игрока


Как часто игрок меняет дверь. Предусмотрены 5 линий поведения:

  1. Never игрок никогда не меняет свой выбор
  2. Fifty-Fifty 50 на 50. Количество симуляций делится на два прохода. Первый проход игрок не меняет дверь, второй проход меняет.
  3. Random в каждой новой симуляции игрок подкидывает монетку
  4. Always игрок всегда меняет свой выбор.
  5. Ration игрок меняет выбор в N% случаях.

switch ($SwitchDoors) {        "Never" {             0..$Count | ForEach-Object {                $Win += Invoke-Simulation -Door $Door -SwitchDoors $false            }            continue        }        "FiftyFifty" {            $Fifty = [math]::Round($Count / 2)             0..$Fifty | ForEach-Object {                $Win += Invoke-Simulation -Door $Door -SwitchDoors $false            }             0..$Fifty | ForEach-Object {                $Win += Invoke-Simulation -Door $Door -SwitchDoors $true            }            continue        }        "Random" {            0..$Count | ForEach-Object {                [bool]$Random = Get-Random -Maximum 2 -Minimum 0                $Win += Invoke-Simulation -Door $Door -SwitchDoors $Random            }            continue        }        "Always" {            0..$Count | ForEach-Object {                $Win += Invoke-Simulation -Door $Door -SwitchDoors $true            }            continue        }        "Ratio" {            $TrueRatio = $Ratio / 100 * $Count             $FalseRatio = $Count - $TrueRatio             0..$TrueRatio | ForEach-Object {                $Win += Invoke-Simulation -Door $Door -SwitchDoors $true            }             0..$FalseRatio | ForEach-Object {                $Win += Invoke-Simulation -Door $Door -SwitchDoors $false            }            continue        }    }

ForEach-Object в Powershell 7 работает значительно быстрее цикла for, плюс, может быть распараллелен, поэтому тут используется вместо цикла for.

Оформляем командлет


Теперь нужно правильно дооформить командлет. Первым делом, нужно сделать валидацию входящих аргументов. Бонус не только в том, что человек не может ввести неверный аргумент в поле, но еще список всех доступных аргументов появляется в подсказках.

Так выглядит код в блоке параметров:

param (        [Parameter(Mandatory = $false,            HelpMessage = "How often the player changes his choice.")]        [ValidateSet("Never", "FiftyFifty", "Random", "Always", "Ratio")]        $SwitchDoors = "Random"    )

Так выглядит подсказка:


Перед блоком параметров можно сделать comment based help. Вот так выглядит код перед блоком параметров:

  <#      .SYNOPSIS         Performs monty hall paradox simulation.         .DESCRIPTION         The Invoke-MontyHallParadox.ps1 script invoke monty hall paradox simulation.         .PARAMETER Door      Specifies door the player will choose during the entire simulation         .PARAMETER SwitchDoors      Specifies principle how the player changes his choice.         .PARAMETER Count      Specifies how many times to run the simulation.         .PARAMETER Ratio      If -SwitchDoors Ratio, specifies how often the player changes his choice. As a percentage."         .INPUTS         None. You cannot pipe objects to Update-Month.ps1.         .OUTPUTS         None. Update-Month.ps1 does not generate any output.         .EXAMPLE         PS> Invoke-MontyHallParadox -SwitchDoors Always -Count 10000         #>

Вот так выглядит сама подсказка:


Запускаем симуляцию


Результаты симуляции:


Если человек никогда не меняет свой выбор, то он побеждает в 33,37% случаев.

В случае двух проходов, в половине которых мы отказываемся менять свой выбор, шансы на победу составляют 49.9134%, что очень близко к ровным 50%.

В случае подкидывания монетки ничего не меняется, шанс на победу остается в районе 50,131%.

Ну а если игрок всегда меняет свой выбор, шанс на победу повышается до 66,6184%, иными словами, скучно и ничего нового.

Производительность:

Что касается производительности. Скрипт кажется не оптимальным. String вместо Bool, много разных функций со свитчаим внутри, передающих друг другу объект, но тем не менее, вот результаты Measure-Command по этому скрипту и скрипту от другого автора.

Сравнение проводилось на двух системах, везде стоял pwsh 7.1, 100 000 проходов.

I5-5200u


Этот алгоритм:

Days              : 0Hours             : 0Minutes           : 0Seconds           : 4Milliseconds      : 581Ticks             : 45811819TotalDays         : 5,30229386574074E-05TotalHours        : 0,00127255052777778TotalMinutes      : 0,0763530316666667TotalSeconds      : 4,5811819TotalMilliseconds : 4581,1819

Тот алгоритм:

Days              : 0Hours             : 0Minutes           : 0Seconds           : 5Milliseconds      : 104Ticks             : 51048392TotalDays         : 5,9083787037037E-05TotalHours        : 0,00141801088888889TotalMinutes      : 0,0850806533333333TotalSeconds      : 5,1048392TotalMilliseconds : 5104,8392

I9-9900K


Этот алгоритм:

Days              : 0Hours             : 0Minutes           : 0Seconds           : 1Milliseconds      : 891Ticks             : 18917629TotalDays         : 2,18954039351852E-05TotalHours        : 0,000525489694444444TotalMinutes      : 0,0315293816666667  TotalSeconds      : 1,8917629TotalMilliseconds : 1891,7629

Тот алгоритм:

Days              : 0Hours             : 0Minutes           : 0Seconds           : 1Milliseconds      : 954Ticks             : 19543236TotalDays         : 2,26194861111111E-05TotalHours        : 0,000542867666666667TotalMinutes      : 0,03257206TotalSeconds      : 1,9543236TotalMilliseconds : 1954,3236

Преимущество 63 мс, но результаты все равно очень странные, учитывая сколько раз в скрипте сравниваются строки.

Автор надеется, что эта статья послужит убедительным примером для тех, кто считает что шансы всегда составляют 50 на 50, ну а ознакомиться с кодом вы можете под этим спойлером.

Весь код
class Doors {
<#
Модель данных, где описаны 3 двери
#>
[Door]$DoorOne
[Door]$DoorTwo
[Door]$DoorThree
}

class Door {
<#
Модель данных, где описана каждая дверь.
Выбрана ли она игроком и открыта ли она ведущим.
#>
[string]$Contains = Goat
[bool]$Selected = $false
[bool]$Opened = $false
}

function New-Doors {
<#
Генератор случайных дверей.
#>
$i = [Doors]::new()

$i.DoorOne = [Door]::new()
$i.DoorTwo = [Door]::new()
$i.DoorThree = [Door]::new()

switch ( Get-Random -Maximum 3 -Minimum 0 ) {
0 {
$i.DoorOne.Contains = Car
}
1 {
$i.DoorTwo.Contains = Car
}
2 {
$i.DoorThree.Contains = Car
}
Default {
Write-Error Something in door generator went wrong
break
}
}

return $i
}

function Select-Door {
<#
Игрок выбирает дверь.
#>
Param (
[parameter(ValueFromPipeline)]
[Doors]$i,
[Parameter(Mandatory)]
[ValidateSet(First, Second, Third, Random)]
$Principle
)

switch ($Principle) {
First {
$i.DoorOne.Selected = $true
continue
}
Second {
$i.DoorTwo.Selected = $true
continue
}
Third {
$i.DoorThree.Selected = $true
continue
}
Random {
switch ( Get-Random -Maximum 3 -Minimum 0 ) {
0 {
$i.DoorOne.Selected = $true
continue
}
1 {
$i.DoorTwo.Selected = $true
continue
}
2 {
$i.DoorThree.Selected = $true
continue
}
Default {
Write-Error Something in selector generator went wrong
break
}
}
continue
}
Default {
Write-Error Something in door selector went wrong
break
}
}

return $i
}

function Open-Door {
<#
Ведущий открывает дверь с козой, но не ту, что выбрал игрок.
#>
Param (
[parameter(ValueFromPipeline)]
[Doors]$i
)
switch ($false) {
$i.DoorOne.Selected {
if ($i.DoorOne.Contains -eq Goat) {
$i.DoorOne.Opened = $true
continue
}
}
$i.DoorTwo.Selected {
if ($i.DoorTwo.Contains -eq Goat) {
$i.DoorTwo.Opened = $true
continue
}
}
$i.DoorThree.Selected {
if ($i.DoorThree.Contains -eq Goat) {
$i.DoorThree.Opened = $true
continue
}
}
}
return $i
}

function Invoke-UserAction {
<#
Ситуация, где игрок менят или не меняет свой выбор.
#>
Param (
[parameter(ValueFromPipeline)]
[Doors]$i,
[Parameter(Mandatory)]
[bool]$SwitchDoor
)

if ($true -eq $SwitchDoor) {
switch ($false) {
$i.DoorOne.Opened {
if ( $i.DoorOne.Selected ) {
$i.DoorOne.Selected = $false
}
else {
$i.DoorOne.Selected = $true
}
}
$i.DoorTwo.Opened {
if ( $i.DoorTwo.Selected ) {
$i.DoorTwo.Selected = $false
}
else {
$i.DoorTwo.Selected = $true
}
}
$i.DoorThree.Opened {
if ( $i.DoorThree.Selected ) {
$i.DoorThree.Selected = $false
}
else {
$i.DoorThree.Selected = $true
}
}
}
}

return $i
}

function Get-Win {
Param (
[parameter(ValueFromPipeline)]
[Doors]$i
)
switch ($true) {
($i.DoorOne.Selected -and $i.DoorOne.Contains -eq Car) {
return $true
}
($i.DoorTwo.Selected -and $i.DoorTwo.Contains -eq Car) {
return $true
}
($i.DoorThree.Selected -and $i.DoorThree.Contains -eq Car) {
return $true
}
default {
return $false
}
}
}

function Invoke-Simulation {
param (
[Parameter(Mandatory = $false,
HelpMessage = Which door the player will choose during the entire simulation.)]
[ValidateSet(First, Second, Third, Random)]
$Door = Random,

[bool]$SwitchDoors
)
return New-Doors | Select-Door -Principle $Door | Open-Door | Invoke-UserAction -SwitchDoor $SwitchDoors | Get-Win
}

function Invoke-MontyHallParadox {
<#
.SYNOPSIS

Performs monty hall paradox simulation.

.DESCRIPTION

The Invoke-MontyHallParadox.ps1 script invoke monty hall paradox simulation.

.PARAMETER Door
Specifies door the player will choose during the entire simulation

.PARAMETER SwitchDoors
Specifies principle how the player changes his choice.

.PARAMETER Count
Specifies how many times to run the simulation.

.PARAMETER Ratio
If -SwitchDoors Ratio, specifies how often the player changes his choice. As a percentage."

.INPUTS

None. You cannot pipe objects to Update-Month.ps1.

.OUTPUTS

None. Update-Month.ps1 does not generate any output.

.EXAMPLE

PS> Invoke-MontyHallParadox -SwitchDoors Always -Count 10000

#>
param (
[Parameter(Mandatory = $false,
HelpMessage = Which door the player will choose during the entire simulation.)]
[ValidateSet(First, Second, Third, Random)]
$Door = Random,

[Parameter(Mandatory = $false,
HelpMessage = How often the player changes his choice.)]
[ValidateSet(Never, FiftyFifty, Random, Always, Ratio)]
$SwitchDoors = Random,

[Parameter(Mandatory = $false,
HelpMessage = How many times to run the simulation.)]
[uint32]$Count = 10000,

[Parameter(Mandatory = $false,
HelpMessage = How often the player changes his choice. As a percentage.)]
[uint32]$Ratio = 30
)

[uint32]$Win = 0

switch ($SwitchDoors) {
Never {
0..$Count | ForEach-Object {
$Win += Invoke-Simulation -Door $Door -SwitchDoors $false
}
continue
}
FiftyFifty {
$Fifty = [math]::Round($Count / 2)

0..$Fifty | ForEach-Object {
$Win += Invoke-Simulation -Door $Door -SwitchDoors $false
}

0..$Fifty | ForEach-Object {
$Win += Invoke-Simulation -Door $Door -SwitchDoors $true
}
continue
}
Random {
0..$Count | ForEach-Object {
[bool]$Random = Get-Random -Maximum 2 -Minimum 0
$Win += Invoke-Simulation -Door $Door -SwitchDoors $Random
}
continue
}
Always {
0..$Count | ForEach-Object {
$Win += Invoke-Simulation -Door $Door -SwitchDoors $true
}
continue
}
Ratio {
$TrueRatio = $Ratio / 100 * $Count
$FalseRatio = $Count $TrueRatio

0..$TrueRatio | ForEach-Object {
$Win += Invoke-Simulation -Door $Door -SwitchDoors $true
}

0..$FalseRatio | ForEach-Object {
$Win += Invoke-Simulation -Door $Door -SwitchDoors $false
}
continue
}
}

Write-Output (Player won in + $Win + " times out of " + $Count)
Write-Output (Whitch is + ($Win / $Count * 100) + "%")

return $Win
}

#Invoke-MontyHallParadox -SwitchDoors Always -Count 500000




Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru