Документация Engee

Начало работы с данными и графиками

В этом руководстве вы научитесь считывать табличные данные в Julia, а также ознакомитесь с основными принципами построения графиков.

Если у вас нет опыта работы с Julia, начните с руководств Getting started with Julia и Getting started with JuMP.

Одни и те же данные можно считывать в Julia разными способами. В этом руководстве основное внимание уделяется пакету DataFrames.jl, так как он предоставляет экосистему для простой работы с большинством требуемых типов файлов.

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

import JuMP
const DATA_DIR = joinpath(
    dirname(pathof(JuMP)),
    joinpath("..", "docs", "src", "tutorials", "getting_started", "data"),
);

Получение справки

Читайте документацию:

Предварительные требования

Для начала нужно установить ряд пакетов.

DataFrames.jl

Пакет DataFrames предоставляет инструментарий для работы с табличными данными. Он доступен через диспетчер пакетов Julia.

using Pkg
Pkg.add("DataFrames")
import DataFrames

DataFrame — это структура данных, похожая на таблицу. Ее можно использовать для хранения и изучения набора связанных значений данных. Уподобить ее можно массиву с расширенными возможностями для хранения табличных данных.

Plots.jl

Пакет Plots предоставляет инструментарий для построения графиков. Он доступен через диспетчер пакетов Julia.

using Pkg
Pkg.add("Plots")
import Plots

CSV.jl

Для чтения данных из файлов CSV и других текстовых файлов с разделителями можно использовать пакет CSV.jl.

Pkg.add("CSV")
import CSV

Основы DataFrame

Для считывания CSV-файла в DataFrame служит функция CSV.read.

csv_df = CSV.read(joinpath(DATA_DIR, "StarWars.csv"), DataFrames.DataFrame)

20×13 DataFrame

RowNameGenderHeightWeightEyecolorHaircolorSkincolorHomelandBornDiedJediSpeciesWeapon
String31String7Float64String7String15String7String15String15String15String15String7String15String15
1Anakin Skywalkermale1.8884blueblondfairTatooine41.9BBY4ABYjedihumanlightsaber
2Padme Amidalafemale1.6545brownbrownlightNaboo46BBY19BBYno_jedihumanunarmed
3Luke Skywalkermale1.7277blueblondfairTatooine19BBYunk_diedjedihumanlightsaber
4Leia Skywalkerfemale1.549brownbrownlightAlderaan19BBYunk_diedno_jedihumanblaster
5Qui-Gon Jinnmale1.9388.5bluebrownlightunk_planet92BBY32BBYjedihumanlightsaber
6Obi-Wan Kenobimale1.8277bluegrayauburnfairStewjon57BBY0BBYjedihumanlightsaber
7Han Solomale1.880brownbrownlightCorellia29BBYunk_diedno_jedihumanblaster
8Sheev Palpatinemale1.7375blueredpaleNaboo82BBY10ABYno_jedihumanforce-lightning
9R2-D2male0.9632NANANANaboo33BBYunk_diedno_jedidroidunarmed
10C-3POmale1.6775NANANATatooine112BBY3ABYno_jedidroidunarmed
11Yodamale0.6617brownbrowngreenunk_planet896BBY4ABYjediyodalightsaber
12Darth Maulmale1.7580yellownoneredDathomir54BBYunk_diedno_jedidathomirianlightsaber
13Dookumale1.9386brownbrownlightSerenno102BBY19BBYjedihumanlightsaber
14Chewbaccamale2.28112bluebrownNAKashyyyk200BBY25ABYno_jediwookieebowcaster
15Jabbamale3.9NAyellownonetan-greenTatooineunk_born4ABYno_jedihuttunarmed
16Lando Calrissianmale1.7879brownblankdarkSocorro31BBYunk_diedno_jedihumanblaster
17Boba Fettmale1.8378brownblackbrownKamino31.5BBYunk_diedno_jedihumanblaster
18Jango Fettmale1.8379brownblackbrownConcordDawn66BBY22BBYno_jedihumanblaster
19Grievousmale2.16159goldblackorangeKaleeunk_born19BBYno_jedikaleeshslugthrower
20Chief Chirpamale1.050blackgraybrownEndorunk_born4ABYno_jediewokspear

Попробуем построить график данных:

Plots.scatter(
    csv_df.Weight,
    csv_df.Height;
    xlabel = "Weight",
    ylabel = "Height",
)

Выглядит неправильно. Что же произошло? Если посмотреть на приведенный выше объект DataFrame, то можно заметить, что Weight считывается как столбец типа String из-за наличия полей NA. Давайте исправим это, указав, что значения NA в CSV должны считаться missing.

csv_df = CSV.read(
    joinpath(DATA_DIR, "StarWars.csv"),
    DataFrames.DataFrame;
    missingstring = "NA",
)

20×13 DataFrame

RowNameGenderHeightWeightEyecolorHaircolorSkincolorHomelandBornDiedJediSpeciesWeapon
String31String7Float64Float64?String15?String7?String15?String15String15String15String7String15String15
1Anakin Skywalkermale1.8884.0blueblondfairTatooine41.9BBY4ABYjedihumanlightsaber
2Padme Amidalafemale1.6545.0brownbrownlightNaboo46BBY19BBYno_jedihumanunarmed
3Luke Skywalkermale1.7277.0blueblondfairTatooine19BBYunk_diedjedihumanlightsaber
4Leia Skywalkerfemale1.549.0brownbrownlightAlderaan19BBYunk_diedno_jedihumanblaster
5Qui-Gon Jinnmale1.9388.5bluebrownlightunk_planet92BBY32BBYjedihumanlightsaber
6Obi-Wan Kenobimale1.8277.0bluegrayauburnfairStewjon57BBY0BBYjedihumanlightsaber
7Han Solomale1.880.0brownbrownlightCorellia29BBYunk_diedno_jedihumanblaster
8Sheev Palpatinemale1.7375.0blueredpaleNaboo82BBY10ABYno_jedihumanforce-lightning
9R2-D2male0.9632.0missingmissingmissingNaboo33BBYunk_diedno_jedidroidunarmed
10C-3POmale1.6775.0missingmissingmissingTatooine112BBY3ABYno_jedidroidunarmed
11Yodamale0.6617.0brownbrowngreenunk_planet896BBY4ABYjediyodalightsaber
12Darth Maulmale1.7580.0yellownoneredDathomir54BBYunk_diedno_jedidathomirianlightsaber
13Dookumale1.9386.0brownbrownlightSerenno102BBY19BBYjedihumanlightsaber
14Chewbaccamale2.28112.0bluebrownmissingKashyyyk200BBY25ABYno_jediwookieebowcaster
15Jabbamale3.9missingyellownonetan-greenTatooineunk_born4ABYno_jedihuttunarmed
16Lando Calrissianmale1.7879.0brownblankdarkSocorro31BBYunk_diedno_jedihumanblaster
17Boba Fettmale1.8378.0brownblackbrownKamino31.5BBYunk_diedno_jedihumanblaster
18Jango Fettmale1.8379.0brownblackbrownConcordDawn66BBY22BBYno_jedihumanblaster
19Grievousmale2.16159.0goldblackorangeKaleeunk_born19BBYno_jedikaleeshslugthrower
20Chief Chirpamale1.050.0blackgraybrownEndorunk_born4ABYno_jediewokspear

Теперь построим график заново:

Plots.scatter(
    csv_df.Weight,
    csv_df.Height;
    title = "Height vs Weight of StarWars characters",
    xlabel = "Weight",
    ylabel = "Height",
    label = false,
    ylims = (0, 3),
)

Уже лучше.

Сведения о других параметрах анализа см. в документации по CSV.

DataFrames.jl поддерживает операции с использованием функций, аналогичных функциям pandas. Например, DataFrame можно разделить на группы по цвету глаз:

by_eyecolor = DataFrames.groupby(csv_df, :Eyecolor)

GroupedDataFrame with 7 groups based on key: Eyecolor

First Group (5 rows): Eyecolor = "blue"

RowNameGenderHeightWeightEyecolorHaircolorSkincolorHomelandBornDiedJediSpeciesWeapon
String31String7Float64Float64?String15?String7?String15?String15String15String15String7String15String15
1Anakin Skywalkermale1.8884.0blueblondfairTatooine41.9BBY4ABYjedihumanlightsaber
2Luke Skywalkermale1.7277.0blueblondfairTatooine19BBYunk_diedjedihumanlightsaber
3Qui-Gon Jinnmale1.9388.5bluebrownlightunk_planet92BBY32BBYjedihumanlightsaber
4Sheev Palpatinemale1.7375.0blueredpaleNaboo82BBY10ABYno_jedihumanforce-lightning
5Chewbaccamale2.28112.0bluebrownmissingKashyyyk200BBY25ABYno_jediwookieebowcaster

Last Group (1 row): Eyecolor = "black"

RowNameGenderHeightWeightEyecolorHaircolorSkincolorHomelandBornDiedJediSpeciesWeapon
String31String7Float64Float64?String15?String7?String15?String15String15String15String7String15String15
1Chief Chirpamale1.050.0blackgraybrownEndorunk_born4ABYno_jediewokspear

Затем эти группы можно снова объединить в единый DataFrame с помощью функции, работающей с разделенными объектами DataFrame:

eyecolor_count = DataFrames.combine(by_eyecolor) do df
    return DataFrames.nrow(df)
end

7×2 DataFrame

RowEyecolorx1
String15?Int64
1blue5
2brown8
3bluegray1
4missing2
5yellow2
6gold1
7black1

Можно переименовать столбцы:

DataFrames.rename!(eyecolor_count, :x1 => :count)

7×2 DataFrame

RowEyecolorcount
String15?Int64
1blue5
2brown8
3bluegray1
4missing2
5yellow2
6gold1
7black1

Удалять отсутствующие строки:

DataFrames.dropmissing!(eyecolor_count, :Eyecolor)

6×2 DataFrame

RowEyecolorcount
String15Int64
1blue5
2brown8
3bluegray1
4yellow2
5gold1
6black1

Затем можно визуализировать данные:

sort!(eyecolor_count, :count; rev = true)
Plots.bar(
    eyecolor_count.Eyecolor,
    eyecolor_count.count;
    xlabel = "Eye color",
    ylabel = "Number of characters",
    label = false,
)

Другие файлы с разделителями

Пакет CSV.jl также позволяет считывать данные из текстового файла с разделителями любого другого формата.

По умолчанию CSV.File пытается определить разделитель исходя из первых 10 строк файла.

Возможные разделители включают в себя ',', '\t', ' ', '|', ';' и ':'. Если определить разделитель автоматически не получается, принимается ','.

Рассмотрим пример данных, разделенных пробелами.

ss_df = CSV.read(joinpath(DATA_DIR, "Cereal.txt"), DataFrames.DataFrame)

23×10 DataFrame

RowNameCupsCaloriesCarbsFatFiberPotassiumProteinSodiumSugars
String31Float64Int64Float64Int64Float64Int64Int64Int64Int64
1CapnCrunch0.7512012.020.035122012
2CocoaPuffs1.011012.010.055118013
3Trix1.011013.010.025114012
4AppleJacks1.011011.001.030212514
5CornChex1.011022.000.02522803
6CornFlakes1.010021.001.03522902
7Nut&Honey0.6712015.010.04021909
8Smacks0.751109.011.04027015
9MultiGrain1.010015.012.09022206
10CracklinOat0.511010.034.016031407
11GrapeNuts0.2511017.003.09031793
12HoneyNutCheerios0.7511011.511.590325010
13NutriGrain0.6714021.023.013032207
14Product191.010020.001.04533203
15TotalRaisinBran1.014015.014.0230319014
16WheatChex0.6710017.013.011532303
17Oatmeal0.513013.521.5120317010
18Life0.6710012.022.09541506
19Maypo1.010016.010.095403
20QuakerOats0.510014.012.011041356
21Muesli1.015016.033.0170415011
22Cheerios1.2511017.022.010562901
23SpecialK1.011016.001.05562303

Разделитель также можно указать следующим образом:

delim_df = CSV.read(
    joinpath(DATA_DIR, "Soccer.txt"),
    DataFrames.DataFrame;
    delim = "::",
)

20×7 DataFrame

RowTeamPlayedWinsDrawsLossesGoals_forGoals_against
String31Int64Int64Int64Int64String15String15
1Barcelona383044110 goals21 goals
2Real Madrid383026118 goals38 goals
3Atletico Madrid38239667 goals29 goals
4Valencia382211570 goals32 goals
5Seville38237871 goals45 goals
6Villarreal3816121048 goals37 goals
7Athletic Bilbao3815101342 goals41 goals
8Celta Vigo3813121347 goals44 goals
9Malaga381481642 goals48 goals
10Espanyol3813101547 goals51 goals
11Rayo Vallecano381541946 goals68 goals
12Real Sociedad3811131444 goals51 goals
13Elche381181935 goals62 goals
14Levante389101934 goals67 goals
15Getafe381072133 goals64 goals
16Deportivo La Coruna387141735 goals60 goals
17Granada387141729 goals64 goals
18Eibar38982134 goals55 goals
19Almeria38882235 goals64 goals
20Cordoba383112422 goals68 goals

Работа с DataFrame

После считывания необходимых данных в DataFrame давайте рассмотрим некоторые основные операции, которые можно выполнять с ними.

Запрос базовой информации

Функция size возвращает измерения DataFrame:

DataFrames.size(ss_df)
(23, 10)

Кроме того, с помощью функций nrow и ncol можно получить соответственно количество строк и столбцов:

DataFrames.nrow(ss_df), DataFrames.ncol(ss_df)
(23, 10)

Функция describe возвращает базовую сводную статистику по данным в DataFrame:

DataFrames.describe(ss_df)

10×7 DataFrame

Rowvariablemeanminmedianmaxnmissingeltype
SymbolUnion…​AnyUnion…​AnyInt64DataType
1NameAppleJacksWheatChex0String31
2Cups0.8230430.251.01.250Float64
3Calories113.043100110.01500Int64
4Carbs15.04359.015.022.00Float64
5Fat1.1304301.030Int64
6Fiber1.565220.01.54.00Float64
7Potassium86.30432590.02300Int64
8Protein2.9130413.060Int64
9Sodium189.9570190.03200Int64
10Sugars7.5217417.0150Int64

Имя каждого столбца можно получить с помощью функции names:

DataFrames.names(ss_df)
10-element Vector{String}:
 "Name"
 "Cups"
 "Calories"
 "Carbs"
 "Fat"
 "Fiber"
 "Potassium"
 "Protein"
 "Sodium"
 "Sugars"

Для получения соответствующих типов данных применяется транслируемая функция eltype:

eltype.(ss_df)

23×10 DataFrame

Row Name Cups Calories Carbs Fat Fiber Potassium Protein Sodium Sugars

DataType

DataType

DataType

DataType

DataType

DataType

DataType

DataType

DataType

DataType

1

Char

Float64

Int64

Float64

Int64

Float64

Int64

Int64

Int64

Int64

2

Char

Float64

Int64

Float64

Int64

Float64

Int64

Int64

Int64

Int64

3

Char

Float64

Int64

Float64

Int64

Float64

Int64

Int64

Int64

Int64

4

Char

Float64

Int64

Float64

Int64

Float64

Int64

Int64

Int64

Int64

5

Char

Float64

Int64

Float64

Int64

Float64

Int64

Int64

Int64

Int64

6

Char

Float64

Int64

Float64

Int64

Float64

Int64

Int64

Int64

Int64

7

Char

Float64

Int64

Float64

Int64

Float64

Int64

Int64

Int64

Int64

8

Char

Float64

Int64

Float64

Int64

Float64

Int64

Int64

Int64

Int64

9

Char

Float64

Int64

Float64

Int64

Float64

Int64

Int64

Int64

Int64

10

Char

Float64

Int64

Float64

Int64

Float64

Int64

Int64

Int64

Int64

11

Char

Float64

Int64

Float64

Int64

Float64

Int64

Int64

Int64

Int64

12

Char

Float64

Int64

Float64

Int64

Float64

Int64

Int64

Int64

Int64

13

Char

Float64

Int64

Float64

Int64

Float64

Int64

Int64

Int64

Int64

14

Char

Float64

Int64

Float64

Int64

Float64

Int64

Int64

Int64

Int64

15

Char

Float64

Int64

Float64

Int64

Float64

Int64

Int64

Int64

Int64

16

Char

Float64

Int64

Float64

Int64

Float64

Int64

Int64

Int64

Int64

17

Char

Float64

Int64

Float64

Int64

Float64

Int64

Int64

Int64

Int64

18

Char

Float64

Int64

Float64

Int64

Float64

Int64

Int64

Int64

Int64

19

Char

Float64

Int64

Float64

Int64

Float64

Int64

Int64

Int64

Int64

20

Char

Float64

Int64

Float64

Int64

Float64

Int64

Int64

Int64

Int64

21

Char

Float64

Int64

Float64

Int64

Float64

Int64

Int64

Int64

Int64

22

Char

Float64

Int64

Float64

Int64

Float64

Int64

Int64

Int64

Int64

23

Char

Float64

Int64

Float64

Int64

Float64

Int64

Int64

Int64

Int64

Доступ к данным

Как и в случае с обычными массивами, обращение к элементам DataFrame происходит по числовым индексам:

csv_df[1, 1]
"Anakin Skywalker"

Вот несколько разных способов доступа к столбцу:

csv_df[!, 1]
20-element Vector{String31}:
 "Anakin Skywalker"
 "Padme Amidala"
 "Luke Skywalker"
 "Leia Skywalker"
 "Qui-Gon Jinn"
 "Obi-Wan Kenobi"
 "Han Solo"
 "Sheev Palpatine"
 "R2-D2"
 "C-3PO"
 "Yoda"
 "Darth Maul"
 "Dooku"
 "Chewbacca"
 "Jabba"
 "Lando Calrissian"
 "Boba Fett"
 "Jango Fett"
 "Grievous"
 "Chief Chirpa"
csv_df[!, :Name]
20-element Vector{String31}:
 "Anakin Skywalker"
 "Padme Amidala"
 "Luke Skywalker"
 "Leia Skywalker"
 "Qui-Gon Jinn"
 "Obi-Wan Kenobi"
 "Han Solo"
 "Sheev Palpatine"
 "R2-D2"
 "C-3PO"
 "Yoda"
 "Darth Maul"
 "Dooku"
 "Chewbacca"
 "Jabba"
 "Lando Calrissian"
 "Boba Fett"
 "Jango Fett"
 "Grievous"
 "Chief Chirpa"
csv_df.Name
20-element Vector{String31}:
 "Anakin Skywalker"
 "Padme Amidala"
 "Luke Skywalker"
 "Leia Skywalker"
 "Qui-Gon Jinn"
 "Obi-Wan Kenobi"
 "Han Solo"
 "Sheev Palpatine"
 "R2-D2"
 "C-3PO"
 "Yoda"
 "Darth Maul"
 "Dooku"
 "Chewbacca"
 "Jabba"
 "Lando Calrissian"
 "Boba Fett"
 "Jango Fett"
 "Grievous"
 "Chief Chirpa"
csv_df[:, 1] # Обратите внимание, что при этом создается копия.
20-element Vector{String31}:
 "Anakin Skywalker"
 "Padme Amidala"
 "Luke Skywalker"
 "Leia Skywalker"
 "Qui-Gon Jinn"
 "Obi-Wan Kenobi"
 "Han Solo"
 "Sheev Palpatine"
 "R2-D2"
 "C-3PO"
 "Yoda"
 "Darth Maul"
 "Dooku"
 "Chewbacca"
 "Jabba"
 "Lando Calrissian"
 "Boba Fett"
 "Jango Fett"
 "Grievous"
 "Chief Chirpa"

Вот несколько разных способов доступа к строке:

csv_df[1:1, :]

1×13 DataFrame

RowNameGenderHeightWeightEyecolorHaircolorSkincolorHomelandBornDiedJediSpeciesWeapon
String31String7Float64Float64?String15?String7?String15?String15String15String15String7String15String15
1Anakin Skywalkermale1.8884.0blueblondfairTatooine41.9BBY4ABYjedihumanlightsaber

csv_df[1, :] # При этом создается объект DataFrameRow.

DataFrameRow (13 columns)

RowNameGenderHeightWeightEyecolorHaircolorSkincolorHomelandBornDiedJediSpeciesWeapon
String31String7Float64Float64?String15?String7?String15?String15String15String15String7String15String15
1Anakin Skywalkermale1.8884.0blueblondfairTatooine41.9BBY4ABYjedihumanlightsaber

Значения можно изменять обычным присваиванием.

Присвоим диапазон скаляру:

csv_df[1:3, :Height] .= 1.83
3-element view(::Vector{Float64}, 1:3) with eltype Float64:
 1.83
 1.83
 1.83

Присвоим вектор:

csv_df[4:6, :Height] = [1.8, 1.6, 1.8]
3-element Vector{Float64}:
 1.8
 1.6
 1.8
csv_df

20×13 DataFrame

RowNameGenderHeightWeightEyecolorHaircolorSkincolorHomelandBornDiedJediSpeciesWeapon
String31String7Float64Float64?String15?String7?String15?String15String15String15String7String15String15
1Anakin Skywalkermale1.8384.0blueblondfairTatooine41.9BBY4ABYjedihumanlightsaber
2Padme Amidalafemale1.8345.0brownbrownlightNaboo46BBY19BBYno_jedihumanunarmed
3Luke Skywalkermale1.8377.0blueblondfairTatooine19BBYunk_diedjedihumanlightsaber
4Leia Skywalkerfemale1.849.0brownbrownlightAlderaan19BBYunk_diedno_jedihumanblaster
5Qui-Gon Jinnmale1.688.5bluebrownlightunk_planet92BBY32BBYjedihumanlightsaber
6Obi-Wan Kenobimale1.877.0bluegrayauburnfairStewjon57BBY0BBYjedihumanlightsaber
7Han Solomale1.880.0brownbrownlightCorellia29BBYunk_diedno_jedihumanblaster
8Sheev Palpatinemale1.7375.0blueredpaleNaboo82BBY10ABYno_jedihumanforce-lightning
9R2-D2male0.9632.0missingmissingmissingNaboo33BBYunk_diedno_jedidroidunarmed
10C-3POmale1.6775.0missingmissingmissingTatooine112BBY3ABYno_jedidroidunarmed
11Yodamale0.6617.0brownbrowngreenunk_planet896BBY4ABYjediyodalightsaber
12Darth Maulmale1.7580.0yellownoneredDathomir54BBYunk_diedno_jedidathomirianlightsaber
13Dookumale1.9386.0brownbrownlightSerenno102BBY19BBYjedihumanlightsaber
14Chewbaccamale2.28112.0bluebrownmissingKashyyyk200BBY25ABYno_jediwookieebowcaster
15Jabbamale3.9missingyellownonetan-greenTatooineunk_born4ABYno_jedihuttunarmed
16Lando Calrissianmale1.7879.0brownblankdarkSocorro31BBYunk_diedno_jedihumanblaster
17Boba Fettmale1.8378.0brownblackbrownKamino31.5BBYunk_diedno_jedihumanblaster
18Jango Fettmale1.8379.0brownblackbrownConcordDawn66BBY22BBYno_jedihumanblaster
19Grievousmale2.16159.0goldblackorangeKaleeunk_born19BBYno_jedikaleeshslugthrower
20Chief Chirpamale1.050.0blackgraybrownEndorunk_born4ABYno_jediewokspear

С DataFrame можно выполнять и другие действия. Дополнительные сведения см. в документации.

Сведения о синтаксисе в стиле dplyr:

Пример: задача с паспортами

Теперь применим полученные знания для решения реальной задачи.

Операции с данными

В наборе данных индексов паспортов перечислены визовые требования в 199 странах в формате .csv. Наша задача — найти минимальный набор паспортов, необходимый для посещения всех стран.

passport_data = CSV.read(
    joinpath(DATA_DIR, "passport-index-matrix.csv"),
    DataFrames.DataFrame,
);

В этом наборе данных первый столбец представляет паспорт (страна отправления), а остальные столбцы — иностранные государства (страны назначения).

В каждой ячейке может быть одно из следующих значений:

  • 3 = безвизовый режим;

  • 2 = требуется электронное разрешение на въезд;

  • 1 = визу можно получить по прибытии;

  • 0 = требуется виза;

  • --1 = страна отправления совпадает со страной назначения.

Наша задача — определить минимальный набор паспортов, необходимый для посещения всех стран без визы.

Нас интересуют значения --1 и 3. Изменим DataFrame, заменив значения --1 и 3 на 1 (истина), а все остальные значения — на 0 (ложь):

function modifier(x)
    if x == -1 || x == 3
        return 1
    else
        return 0
    end
end

for country in passport_data.Passport
    passport_data[!, country] = modifier.(passport_data[!, country])
end

Теперь значения в ячейках имеют следующий смысл:

  • 1 = туристическая виза не требуется;

  • 0 = туристическая виза требуется.

Моделирование на языке JuMP

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

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

Таким образом, эту задачу можно представить в виде следующей модели:

Теперь решим задачу с помощью JuMP:

using JuMP
import HiGHS

Сначала создадим множество стран:

C = passport_data.Passport
199-element Vector{String}:
 "Afghanistan"
 "Albania"
 "Algeria"
 "Andorra"
 "Angola"
 "Antigua and Barbuda"
 "Argentina"
 "Armenia"
 "Australia"
 "Austria"
 ⋮
 "Uruguay"
 "Uzbekistan"
 "Vanuatu"
 "Vatican"
 "Venezuela"
 "Viet Nam"
 "Yemen"
 "Zambia"
 "Zimbabwe"

Далее создадим модель и инициализируем переменные решения:

model = Model(HiGHS.Optimizer)
set_silent(model)
@variable(model, x[C], Bin)
@objective(model, Min, sum(x))
@constraint(model, [d in C], passport_data[!, d]' * x >= 1)
model
A JuMP Model
├ solver: HiGHS
├ objective_sense: MIN_SENSE
│ └ objective_function_type: AffExpr
├ num_variables: 199
├ num_constraints: 398
│ ├ AffExpr in MOI.GreaterThan{Float64}: 199
│ └ VariableRef in MOI.ZeroOne: 199
└ Names registered in the model
  └ :x

Теперь выполним оптимизацию:

optimize!(model)

С помощью функции solution_summary можно получить обзор решения:

solution_summary(model)
* Solver : HiGHS

* Status
  Result count       : 1
  Termination status : OPTIMAL
  Message from the solver:
  "kHighsModelStatusOptimal"

* Candidate solution (result #1)
  Primal status      : FEASIBLE_POINT
  Dual status        : NO_SOLUTION
  Objective value    : 2.30000e+01
  Objective bound    : 2.30000e+01
  Relative gap       : 0.00000e+00

* Work counters
  Solve time (sec)   : 8.27169e-03
  Simplex iterations : 26
  Barrier iterations : -1
  Node count         : 1

На всякий случай проверим, нашел ли решатель оптимальное решение:

@assert is_solved_and_feasible(model)

Решение

Рассмотрим решение более подробно:

println("Minimum number of passports needed: ", objective_value(model))
Minimum number of passports needed: 23.0
println("Optimal passports:")
for c in C
    if value(x[c]) > 0.5
        println(" * ", c)
    end
end
Optimal passports:
 * Afghanistan
 * Chad
 * Comoros
 * Djibouti
 * Georgia
 * Hong Kong
 * India
 * Luxembourg
 * Madagascar
 * Maldives
 * Mali
 * New Zealand
 * North Korea
 * Papua New Guinea
 * Singapore
 * Somalia
 * Sri Lanka
 * Tunisia
 * Turkey
 * Uganda
 * United Arab Emirates
 * United States
 * Zimbabwe

Некоторые требуемые паспорта, например Новой Зеландии и Соединенных Штатов, дают доступ во многие страны. Однако среди нужных есть и такие паспорта, например Северной Кореи, которые дают безвизовый доступ лишь в очень ограниченное число стран.

Мы используем value(x[c]) > 0.5 вместо value(x[c]) == 1, чтобы избежать исключения таких решений, как x[c] = 0.99999, которые равны 1 с некоторой погрешностью.