diff --git a/.r-cache/R/renv/projects b/.r-cache/R/renv/projects new file mode 100644 index 0000000..f4944cb --- /dev/null +++ b/.r-cache/R/renv/projects @@ -0,0 +1 @@ +C:/Users/mattp/ArchNetSci diff --git a/.r-cache/R/sass/7b36f064b920db52d4193d8e352723a7 b/.r-cache/R/sass/7b36f064b920db52d4193d8e352723a7 new file mode 100644 index 0000000..a8af72b --- /dev/null +++ b/.r-cache/R/sass/7b36f064b920db52d4193d8e352723a7 @@ -0,0 +1 @@ +:root{--bslib-bootstrap-version: 4;--bslib-preset-name: ;--bslib-preset-type: }:root{--blue: #007bff;--indigo: #6610f2;--purple: #6f42c1;--pink: #e83e8c;--red: #dc3545;--orange: #fd7e14;--yellow: #ffc107;--green: #28a745;--teal: #20c997;--cyan: #17a2b8;--white: #fff;--gray: #6c757d;--gray-dark: #343a40;--default: #dee2e6;--primary: #1e90ff;--secondary: #6c757d;--success: #28a745;--info: #0DCAF0;--warning: #ffc107;--danger: #dc3545;--light: #f8f9fa;--dark: #343a40;--breakpoint-xs: 0;--breakpoint-sm: 576px;--breakpoint-md: 768px;--breakpoint-lg: 992px;--breakpoint-xl: 1200px;--font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-family-monospace: "Source Code Pro"}*,*::before,*::after{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0)}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:Lato;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0 !important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[title],abbr[data-original-title]{text-decoration:underline;text-decoration:underline dotted;-webkit-text-decoration:underline dotted;-moz-text-decoration:underline dotted;-ms-text-decoration:underline dotted;-o-text-decoration:underline dotted;cursor:help;border-bottom:0;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul,dl{margin-top:0;margin-bottom:1rem}ol ol,ul ul,ol ul,ul ol{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem;padding:.625rem 1.25rem;border-left:.25rem solid #e9ecef}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#1e90ff;text-decoration:none;-webkit-text-decoration:none;-moz-text-decoration:none;-ms-text-decoration:none;-o-text-decoration:none;background-color:transparent}a:hover{color:#006ad1;text-decoration:underline;-webkit-text-decoration:underline;-moz-text-decoration:underline;-ms-text-decoration:underline;-o-text-decoration:underline}a:not([href]):not([class]){color:inherit;text-decoration:none}a:not([href]):not([class]):hover{color:inherit;text-decoration:none}pre,code,kbd,samp{font-family:"Source Code Pro";font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit;text-align:-webkit-match-parent}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus:not(:focus-visible){outline:0}input,button,select,optgroup,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[role="button"]{cursor:pointer}select{word-wrap:normal}button,[type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}button:not(:disabled),[type="button"]:not(:disabled),[type="reset"]:not(:disabled),[type="submit"]:not(:disabled){cursor:pointer}button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner{padding:0;border-style:none}input[type="radio"],input[type="checkbox"]{box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto}[type="search"]{outline-offset:-2px;-webkit-appearance:none}[type="search"]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none !important}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{margin-bottom:.5rem;font-family:"News Cycle";font-weight:500;line-height:1.2}h1,.h1{font-size:2.5rem}h2,.h2{font-size:2rem}h3,.h3{font-size:1.75rem}h4,.h4{font-size:1.5rem}h5,.h5{font-size:1.25rem}h6,.h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,0.1)}small,.small{font-size:80%;font-weight:400}mark,.mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:80%;color:#6c757d}.blockquote-footer::before{content:"\2014\00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#6c757d}code{font-size:87.5%;color:#000;background-color:#f6f6f6;border-radius:.25rem;padding:.125rem .25rem;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#000;background-color:#f6f6f6;padding:.5rem;border:1px solid #dee2e6;border-radius:.25rem}pre code{background-color:transparent;font-size:inherit;color:inherit;word-break:normal;padding:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container,.container-fluid,.container-xl,.container-lg,.container-md,.container-sm{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width: 576px){.container-sm,.container{max-width:540px}}@media (min-width: 768px){.container-md,.container-sm,.container{max-width:720px}}@media (min-width: 992px){.container-lg,.container-md,.container-sm,.container{max-width:960px}}@media (min-width: 1200px){.container-xl,.container-lg,.container-md,.container-sm,.container{max-width:1140px}}.row{display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*="col-"]{padding-right:0;padding-left:0}.col-xl,.col-xl-auto,.col-xl-12,.col-xl-11,.col-xl-10,.col-xl-9,.col-xl-8,.col-xl-7,.col-xl-6,.col-xl-5,.col-xl-4,.col-xl-3,.col-xl-2,.col-xl-1,.col-lg,.col-lg-auto,.col-lg-12,.col-lg-11,.col-lg-10,.col-lg-9,.col-lg-8,.col-lg-7,.col-lg-6,.col-lg-5,.col-lg-4,.col-lg-3,.col-lg-2,.col-lg-1,.col-md,.col-md-auto,.col-md-12,.col-md-11,.col-md-10,.col-md-9,.col-md-8,.col-md-7,.col-md-6,.col-md-5,.col-md-4,.col-md-3,.col-md-2,.col-md-1,.col-sm,.col-sm-auto,.col-sm-12,.col-sm-11,.col-sm-10,.col-sm-9,.col-sm-8,.col-sm-7,.col-sm-6,.col-sm-5,.col-sm-4,.col-sm-3,.col-sm-2,.col-sm-1,.col,.col-auto,.col-12,.col-11,.col-10,.col-9,.col-8,.col-7,.col-6,.col-5,.col-4,.col-3,.col-2,.col-1{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{flex-basis:0;-webkit-flex-basis:0;flex-grow:1;-webkit-flex-grow:1;max-width:100%}.row-cols-1>*{flex:0 0 100%;-webkit-flex:0 0 100%;max-width:100%}.row-cols-2>*{flex:0 0 50%;-webkit-flex:0 0 50%;max-width:50%}.row-cols-3>*{flex:0 0 33.33333%;-webkit-flex:0 0 33.33333%;max-width:33.33333%}.row-cols-4>*{flex:0 0 25%;-webkit-flex:0 0 25%;max-width:25%}.row-cols-5>*{flex:0 0 20%;-webkit-flex:0 0 20%;max-width:20%}.row-cols-6>*{flex:0 0 16.66667%;-webkit-flex:0 0 16.66667%;max-width:16.66667%}.col-auto{flex:0 0 auto;-webkit-flex:0 0 auto;width:auto;max-width:100%}.col-1{flex:0 0 8.33333%;-webkit-flex:0 0 8.33333%;max-width:8.33333%}.col-2{flex:0 0 16.66667%;-webkit-flex:0 0 16.66667%;max-width:16.66667%}.col-3{flex:0 0 25%;-webkit-flex:0 0 25%;max-width:25%}.col-4{flex:0 0 33.33333%;-webkit-flex:0 0 33.33333%;max-width:33.33333%}.col-5{flex:0 0 41.66667%;-webkit-flex:0 0 41.66667%;max-width:41.66667%}.col-6{flex:0 0 50%;-webkit-flex:0 0 50%;max-width:50%}.col-7{flex:0 0 58.33333%;-webkit-flex:0 0 58.33333%;max-width:58.33333%}.col-8{flex:0 0 66.66667%;-webkit-flex:0 0 66.66667%;max-width:66.66667%}.col-9{flex:0 0 75%;-webkit-flex:0 0 75%;max-width:75%}.col-10{flex:0 0 83.33333%;-webkit-flex:0 0 83.33333%;max-width:83.33333%}.col-11{flex:0 0 91.66667%;-webkit-flex:0 0 91.66667%;max-width:91.66667%}.col-12{flex:0 0 100%;-webkit-flex:0 0 100%;max-width:100%}.order-first{order:-1}.order-last{order:13}.order-0{order:0}.order-1{order:1}.order-2{order:2}.order-3{order:3}.order-4{order:4}.order-5{order:5}.order-6{order:6}.order-7{order:7}.order-8{order:8}.order-9{order:9}.order-10{order:10}.order-11{order:11}.order-12{order:12}.offset-1{margin-left:8.33333%}.offset-2{margin-left:16.66667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333%}.offset-5{margin-left:41.66667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333%}.offset-8{margin-left:66.66667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333%}.offset-11{margin-left:91.66667%}@media (min-width: 576px){.col-sm{flex-basis:0;-webkit-flex-basis:0;flex-grow:1;-webkit-flex-grow:1;max-width:100%}.row-cols-sm-1>*{flex:0 0 100%;-webkit-flex:0 0 100%;max-width:100%}.row-cols-sm-2>*{flex:0 0 50%;-webkit-flex:0 0 50%;max-width:50%}.row-cols-sm-3>*{flex:0 0 33.33333%;-webkit-flex:0 0 33.33333%;max-width:33.33333%}.row-cols-sm-4>*{flex:0 0 25%;-webkit-flex:0 0 25%;max-width:25%}.row-cols-sm-5>*{flex:0 0 20%;-webkit-flex:0 0 20%;max-width:20%}.row-cols-sm-6>*{flex:0 0 16.66667%;-webkit-flex:0 0 16.66667%;max-width:16.66667%}.col-sm-auto{flex:0 0 auto;-webkit-flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{flex:0 0 8.33333%;-webkit-flex:0 0 8.33333%;max-width:8.33333%}.col-sm-2{flex:0 0 16.66667%;-webkit-flex:0 0 16.66667%;max-width:16.66667%}.col-sm-3{flex:0 0 25%;-webkit-flex:0 0 25%;max-width:25%}.col-sm-4{flex:0 0 33.33333%;-webkit-flex:0 0 33.33333%;max-width:33.33333%}.col-sm-5{flex:0 0 41.66667%;-webkit-flex:0 0 41.66667%;max-width:41.66667%}.col-sm-6{flex:0 0 50%;-webkit-flex:0 0 50%;max-width:50%}.col-sm-7{flex:0 0 58.33333%;-webkit-flex:0 0 58.33333%;max-width:58.33333%}.col-sm-8{flex:0 0 66.66667%;-webkit-flex:0 0 66.66667%;max-width:66.66667%}.col-sm-9{flex:0 0 75%;-webkit-flex:0 0 75%;max-width:75%}.col-sm-10{flex:0 0 83.33333%;-webkit-flex:0 0 83.33333%;max-width:83.33333%}.col-sm-11{flex:0 0 91.66667%;-webkit-flex:0 0 91.66667%;max-width:91.66667%}.col-sm-12{flex:0 0 100%;-webkit-flex:0 0 100%;max-width:100%}.order-sm-first{order:-1}.order-sm-last{order:13}.order-sm-0{order:0}.order-sm-1{order:1}.order-sm-2{order:2}.order-sm-3{order:3}.order-sm-4{order:4}.order-sm-5{order:5}.order-sm-6{order:6}.order-sm-7{order:7}.order-sm-8{order:8}.order-sm-9{order:9}.order-sm-10{order:10}.order-sm-11{order:11}.order-sm-12{order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333%}.offset-sm-2{margin-left:16.66667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333%}.offset-sm-5{margin-left:41.66667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333%}.offset-sm-8{margin-left:66.66667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333%}.offset-sm-11{margin-left:91.66667%}}@media (min-width: 768px){.col-md{flex-basis:0;-webkit-flex-basis:0;flex-grow:1;-webkit-flex-grow:1;max-width:100%}.row-cols-md-1>*{flex:0 0 100%;-webkit-flex:0 0 100%;max-width:100%}.row-cols-md-2>*{flex:0 0 50%;-webkit-flex:0 0 50%;max-width:50%}.row-cols-md-3>*{flex:0 0 33.33333%;-webkit-flex:0 0 33.33333%;max-width:33.33333%}.row-cols-md-4>*{flex:0 0 25%;-webkit-flex:0 0 25%;max-width:25%}.row-cols-md-5>*{flex:0 0 20%;-webkit-flex:0 0 20%;max-width:20%}.row-cols-md-6>*{flex:0 0 16.66667%;-webkit-flex:0 0 16.66667%;max-width:16.66667%}.col-md-auto{flex:0 0 auto;-webkit-flex:0 0 auto;width:auto;max-width:100%}.col-md-1{flex:0 0 8.33333%;-webkit-flex:0 0 8.33333%;max-width:8.33333%}.col-md-2{flex:0 0 16.66667%;-webkit-flex:0 0 16.66667%;max-width:16.66667%}.col-md-3{flex:0 0 25%;-webkit-flex:0 0 25%;max-width:25%}.col-md-4{flex:0 0 33.33333%;-webkit-flex:0 0 33.33333%;max-width:33.33333%}.col-md-5{flex:0 0 41.66667%;-webkit-flex:0 0 41.66667%;max-width:41.66667%}.col-md-6{flex:0 0 50%;-webkit-flex:0 0 50%;max-width:50%}.col-md-7{flex:0 0 58.33333%;-webkit-flex:0 0 58.33333%;max-width:58.33333%}.col-md-8{flex:0 0 66.66667%;-webkit-flex:0 0 66.66667%;max-width:66.66667%}.col-md-9{flex:0 0 75%;-webkit-flex:0 0 75%;max-width:75%}.col-md-10{flex:0 0 83.33333%;-webkit-flex:0 0 83.33333%;max-width:83.33333%}.col-md-11{flex:0 0 91.66667%;-webkit-flex:0 0 91.66667%;max-width:91.66667%}.col-md-12{flex:0 0 100%;-webkit-flex:0 0 100%;max-width:100%}.order-md-first{order:-1}.order-md-last{order:13}.order-md-0{order:0}.order-md-1{order:1}.order-md-2{order:2}.order-md-3{order:3}.order-md-4{order:4}.order-md-5{order:5}.order-md-6{order:6}.order-md-7{order:7}.order-md-8{order:8}.order-md-9{order:9}.order-md-10{order:10}.order-md-11{order:11}.order-md-12{order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333%}.offset-md-2{margin-left:16.66667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333%}.offset-md-5{margin-left:41.66667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333%}.offset-md-8{margin-left:66.66667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333%}.offset-md-11{margin-left:91.66667%}}@media (min-width: 992px){.col-lg{flex-basis:0;-webkit-flex-basis:0;flex-grow:1;-webkit-flex-grow:1;max-width:100%}.row-cols-lg-1>*{flex:0 0 100%;-webkit-flex:0 0 100%;max-width:100%}.row-cols-lg-2>*{flex:0 0 50%;-webkit-flex:0 0 50%;max-width:50%}.row-cols-lg-3>*{flex:0 0 33.33333%;-webkit-flex:0 0 33.33333%;max-width:33.33333%}.row-cols-lg-4>*{flex:0 0 25%;-webkit-flex:0 0 25%;max-width:25%}.row-cols-lg-5>*{flex:0 0 20%;-webkit-flex:0 0 20%;max-width:20%}.row-cols-lg-6>*{flex:0 0 16.66667%;-webkit-flex:0 0 16.66667%;max-width:16.66667%}.col-lg-auto{flex:0 0 auto;-webkit-flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{flex:0 0 8.33333%;-webkit-flex:0 0 8.33333%;max-width:8.33333%}.col-lg-2{flex:0 0 16.66667%;-webkit-flex:0 0 16.66667%;max-width:16.66667%}.col-lg-3{flex:0 0 25%;-webkit-flex:0 0 25%;max-width:25%}.col-lg-4{flex:0 0 33.33333%;-webkit-flex:0 0 33.33333%;max-width:33.33333%}.col-lg-5{flex:0 0 41.66667%;-webkit-flex:0 0 41.66667%;max-width:41.66667%}.col-lg-6{flex:0 0 50%;-webkit-flex:0 0 50%;max-width:50%}.col-lg-7{flex:0 0 58.33333%;-webkit-flex:0 0 58.33333%;max-width:58.33333%}.col-lg-8{flex:0 0 66.66667%;-webkit-flex:0 0 66.66667%;max-width:66.66667%}.col-lg-9{flex:0 0 75%;-webkit-flex:0 0 75%;max-width:75%}.col-lg-10{flex:0 0 83.33333%;-webkit-flex:0 0 83.33333%;max-width:83.33333%}.col-lg-11{flex:0 0 91.66667%;-webkit-flex:0 0 91.66667%;max-width:91.66667%}.col-lg-12{flex:0 0 100%;-webkit-flex:0 0 100%;max-width:100%}.order-lg-first{order:-1}.order-lg-last{order:13}.order-lg-0{order:0}.order-lg-1{order:1}.order-lg-2{order:2}.order-lg-3{order:3}.order-lg-4{order:4}.order-lg-5{order:5}.order-lg-6{order:6}.order-lg-7{order:7}.order-lg-8{order:8}.order-lg-9{order:9}.order-lg-10{order:10}.order-lg-11{order:11}.order-lg-12{order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333%}.offset-lg-2{margin-left:16.66667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333%}.offset-lg-5{margin-left:41.66667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333%}.offset-lg-8{margin-left:66.66667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333%}.offset-lg-11{margin-left:91.66667%}}@media (min-width: 1200px){.col-xl{flex-basis:0;-webkit-flex-basis:0;flex-grow:1;-webkit-flex-grow:1;max-width:100%}.row-cols-xl-1>*{flex:0 0 100%;-webkit-flex:0 0 100%;max-width:100%}.row-cols-xl-2>*{flex:0 0 50%;-webkit-flex:0 0 50%;max-width:50%}.row-cols-xl-3>*{flex:0 0 33.33333%;-webkit-flex:0 0 33.33333%;max-width:33.33333%}.row-cols-xl-4>*{flex:0 0 25%;-webkit-flex:0 0 25%;max-width:25%}.row-cols-xl-5>*{flex:0 0 20%;-webkit-flex:0 0 20%;max-width:20%}.row-cols-xl-6>*{flex:0 0 16.66667%;-webkit-flex:0 0 16.66667%;max-width:16.66667%}.col-xl-auto{flex:0 0 auto;-webkit-flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{flex:0 0 8.33333%;-webkit-flex:0 0 8.33333%;max-width:8.33333%}.col-xl-2{flex:0 0 16.66667%;-webkit-flex:0 0 16.66667%;max-width:16.66667%}.col-xl-3{flex:0 0 25%;-webkit-flex:0 0 25%;max-width:25%}.col-xl-4{flex:0 0 33.33333%;-webkit-flex:0 0 33.33333%;max-width:33.33333%}.col-xl-5{flex:0 0 41.66667%;-webkit-flex:0 0 41.66667%;max-width:41.66667%}.col-xl-6{flex:0 0 50%;-webkit-flex:0 0 50%;max-width:50%}.col-xl-7{flex:0 0 58.33333%;-webkit-flex:0 0 58.33333%;max-width:58.33333%}.col-xl-8{flex:0 0 66.66667%;-webkit-flex:0 0 66.66667%;max-width:66.66667%}.col-xl-9{flex:0 0 75%;-webkit-flex:0 0 75%;max-width:75%}.col-xl-10{flex:0 0 83.33333%;-webkit-flex:0 0 83.33333%;max-width:83.33333%}.col-xl-11{flex:0 0 91.66667%;-webkit-flex:0 0 91.66667%;max-width:91.66667%}.col-xl-12{flex:0 0 100%;-webkit-flex:0 0 100%;max-width:100%}.order-xl-first{order:-1}.order-xl-last{order:13}.order-xl-0{order:0}.order-xl-1{order:1}.order-xl-2{order:2}.order-xl-3{order:3}.order-xl-4{order:4}.order-xl-5{order:5}.order-xl-6{order:6}.order-xl-7{order:7}.order-xl-8{order:8}.order-xl-9{order:9}.order-xl-10{order:10}.order-xl-11{order:11}.order-xl-12{order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333%}.offset-xl-2{margin-left:16.66667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333%}.offset-xl-5{margin-left:41.66667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333%}.offset-xl-8{margin-left:66.66667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333%}.offset-xl-11{margin-left:91.66667%}}.table{width:100%;margin-bottom:1rem;color:#212529}.table th,.table td{padding:.75rem;vertical-align:top;border-top:1px solid #dee2e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dee2e6}.table tbody+tbody{border-top:2px solid #dee2e6}.table-sm th,.table-sm td{padding:.3rem}.table-bordered{border:1px solid #dee2e6}.table-bordered th,.table-bordered td{border:1px solid #dee2e6}.table-bordered thead th,.table-bordered thead td{border-bottom-width:2px}.table-borderless th,.table-borderless td,.table-borderless thead th,.table-borderless tbody+tbody{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,0.05)}.table-hover tbody tr:hover{color:#212529;background-color:rgba(0,0,0,0.075)}.table-default,.table-default>th,.table-default>td{background-color:#f6f7f8}.table-default th,.table-default td,.table-default thead th,.table-default tbody+tbody{border-color:#eef0f2}.table-hover .table-default:hover{background-color:#e8eaed}.table-hover .table-default:hover>td,.table-hover .table-default:hover>th{background-color:#e8eaed}.table-primary,.table-primary>th,.table-primary>td{background-color:#c0e0ff}.table-primary th,.table-primary td,.table-primary thead th,.table-primary tbody+tbody{border-color:#8ac5ff}.table-hover .table-primary:hover{background-color:#a7d3ff}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#a7d3ff}.table-secondary,.table-secondary>th,.table-secondary>td{background-color:#d6d8db}.table-secondary th,.table-secondary td,.table-secondary thead th,.table-secondary tbody+tbody{border-color:#b3b7bb}.table-hover .table-secondary:hover{background-color:#c8cbcf}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#c8cbcf}.table-success,.table-success>th,.table-success>td{background-color:#c3e6cb}.table-success th,.table-success td,.table-success thead th,.table-success tbody+tbody{border-color:#8fd19e}.table-hover .table-success:hover{background-color:#b1dfbb}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b1dfbb}.table-info,.table-info>th,.table-info>td{background-color:#bbf0fb}.table-info th,.table-info td,.table-info thead th,.table-info tbody+tbody{border-color:#81e3f7}.table-hover .table-info:hover{background-color:#a3ebfa}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#a3ebfa}.table-warning,.table-warning>th,.table-warning>td{background-color:#ffeeba}.table-warning th,.table-warning td,.table-warning thead th,.table-warning tbody+tbody{border-color:#ffdf7e}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>th,.table-danger>td{background-color:#f5c6cb}.table-danger th,.table-danger td,.table-danger thead th,.table-danger tbody+tbody{border-color:#ed969e}.table-hover .table-danger:hover{background-color:#f1b0b7}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f1b0b7}.table-light,.table-light>th,.table-light>td{background-color:#fdfdfe}.table-light th,.table-light td,.table-light thead th,.table-light tbody+tbody{border-color:#fbfcfc}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>th,.table-dark>td{background-color:#c6c8ca}.table-dark th,.table-dark td,.table-dark thead th,.table-dark tbody+tbody{border-color:#95999c}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>th,.table-active>td{background-color:rgba(0,0,0,0.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,0.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,0.075)}.table .thead-dark th{color:#fff;background-color:#343a40;border-color:#454d55}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#dee2e6}.table-dark{color:#fff;background-color:#343a40}.table-dark th,.table-dark td,.table-dark thead th{border-color:#454d55}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,0.05)}.table-dark.table-hover tbody tr:hover{color:#fff;background-color:rgba(255,255,255,0.075)}@media (max-width: 575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-sm>.table-bordered{border:0}}@media (max-width: 767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-md>.table-bordered{border:0}}@media (max-width: 991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-lg>.table-bordered{border:0}}@media (max-width: 1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}.form-control:focus{color:#495057;background-color:#fff;border-color:#9ecfff;outline:0;box-shadow:0 0 0 .2rem rgba(30,144,255,0.25)}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}input[type="date"].form-control,input[type="time"].form-control,input[type="datetime-local"].form-control,input[type="month"].form-control{appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;font-size:1rem;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-sm,.form-control-plaintext.form-control-lg{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.form-control-lg{height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}select.form-control[size],select.form-control[multiple]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text,.help-text,.help-block{display:block;margin-top:.25rem}.form-row{display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*="col-"]{padding-right:5px;padding-left:5px}.form-check,.shiny-input-checkboxgroup .checkbox,.shiny-input-checkboxgroup .radio,.shiny-input-radiogroup .checkbox,.shiny-input-radiogroup .radio{position:relative;display:block;padding-left:1.25rem}.form-check-input,.shiny-input-checkboxgroup .checkbox label>input,.shiny-input-checkboxgroup .radio label>input,.shiny-input-radiogroup .checkbox label>input,.shiny-input-radiogroup .radio label>input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input[disabled]~.form-check-label,.shiny-input-checkboxgroup .checkbox label>input[disabled]~.form-check-label,.shiny-input-checkboxgroup .radio label>input[disabled]~.form-check-label,.shiny-input-radiogroup .checkbox label>input[disabled]~.form-check-label,.shiny-input-radiogroup .radio label>input[disabled]~.form-check-label,.shiny-input-checkboxgroup .checkbox .form-check-input[disabled]~label,.shiny-input-checkboxgroup .checkbox label>input[disabled]~label,.shiny-input-checkboxgroup .radio .form-check-input[disabled]~label,.shiny-input-checkboxgroup .radio label>input[disabled]~label,.shiny-input-radiogroup .checkbox .form-check-input[disabled]~label,.shiny-input-radiogroup .checkbox label>input[disabled]~label,.shiny-input-radiogroup .radio .form-check-input[disabled]~label,.shiny-input-radiogroup .radio label>input[disabled]~label,.form-check-input:disabled~.form-check-label,.shiny-input-checkboxgroup .checkbox label>input:disabled~.form-check-label,.shiny-input-checkboxgroup .radio label>input:disabled~.form-check-label,.shiny-input-radiogroup .checkbox label>input:disabled~.form-check-label,.shiny-input-radiogroup .radio label>input:disabled~.form-check-label,.shiny-input-checkboxgroup .checkbox .form-check-input:disabled~label,.shiny-input-checkboxgroup .checkbox label>input:disabled~label,.shiny-input-checkboxgroup .radio .form-check-input:disabled~label,.shiny-input-checkboxgroup .radio label>input:disabled~label,.shiny-input-radiogroup .checkbox .form-check-input:disabled~label,.shiny-input-radiogroup .checkbox label>input:disabled~label,.shiny-input-radiogroup .radio .form-check-input:disabled~label,.shiny-input-radiogroup .radio label>input:disabled~label{color:#6c757d}.form-check-label,.shiny-input-checkboxgroup .checkbox label,.shiny-input-checkboxgroup .radio label,.shiny-input-radiogroup .checkbox label,.shiny-input-radiogroup .radio label{margin-bottom:0}.form-check-inline{display:inline-flex;align-items:center;-webkit-align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input,.form-check-inline .shiny-input-checkboxgroup .checkbox label>input,.shiny-input-checkboxgroup .checkbox .form-check-inline label>input,.form-check-inline .shiny-input-checkboxgroup .radio label>input,.shiny-input-checkboxgroup .radio .form-check-inline label>input,.form-check-inline .shiny-input-radiogroup .checkbox label>input,.shiny-input-radiogroup .checkbox .form-check-inline label>input,.form-check-inline .shiny-input-radiogroup .radio label>input,.shiny-input-radiogroup .radio .form-check-inline label>input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#28a745}.valid-tooltip{position:absolute;top:100%;left:0;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(40,167,69,0.9);border-radius:.25rem}.form-row>.col>.valid-tooltip,.form-row>[class*="col-"]>.valid-tooltip{left:5px}.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip,.is-valid~.valid-feedback,.is-valid~.valid-tooltip{display:block}.was-validated .form-control:valid,.form-control.is-valid{border-color:#28a745;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.was-validated .form-control:valid:focus,.form-control.is-valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,0.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.was-validated .custom-select:valid,.custom-select.is-valid{border-color:#28a745;padding-right:calc(.75em + 2.3125rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") right .75rem center/8px 10px no-repeat,#fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem) no-repeat}.was-validated .custom-select:valid:focus,.custom-select.is-valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,0.25)}.was-validated .form-check-input:valid~.form-check-label,.was-validated .shiny-input-checkboxgroup .checkbox label>input:valid~.form-check-label,.shiny-input-checkboxgroup .checkbox .was-validated label>input:valid~.form-check-label,.was-validated .shiny-input-checkboxgroup .radio label>input:valid~.form-check-label,.shiny-input-checkboxgroup .radio .was-validated label>input:valid~.form-check-label,.was-validated .shiny-input-radiogroup .checkbox label>input:valid~.form-check-label,.shiny-input-radiogroup .checkbox .was-validated label>input:valid~.form-check-label,.was-validated .shiny-input-radiogroup .radio label>input:valid~.form-check-label,.shiny-input-radiogroup .radio .was-validated label>input:valid~.form-check-label,.was-validated .shiny-input-checkboxgroup .checkbox .form-check-input:valid~label,.was-validated .shiny-input-checkboxgroup .checkbox label>input:valid~label,.was-validated .shiny-input-checkboxgroup .checkbox .radio label>input:valid~label,.was-validated .shiny-input-checkboxgroup .radio .checkbox label>input:valid~label,.was-validated .shiny-input-checkboxgroup .shiny-input-radiogroup .checkbox label>input:valid~label,.shiny-input-radiogroup .was-validated .shiny-input-checkboxgroup .checkbox label>input:valid~label,.was-validated .shiny-input-checkboxgroup .checkbox .shiny-input-radiogroup .radio label>input:valid~label,.shiny-input-radiogroup .radio .was-validated .shiny-input-checkboxgroup .checkbox label>input:valid~label,.shiny-input-checkboxgroup .checkbox .was-validated .form-check-input:valid~label,.shiny-input-checkboxgroup .checkbox .was-validated label>input:valid~label,.shiny-input-checkboxgroup .checkbox .was-validated .radio label>input:valid~label,.shiny-input-checkboxgroup .radio .checkbox .was-validated label>input:valid~label,.shiny-input-checkboxgroup .shiny-input-radiogroup .checkbox .was-validated label>input:valid~label,.shiny-input-radiogroup .shiny-input-checkboxgroup .checkbox .was-validated label>input:valid~label,.shiny-input-checkboxgroup .checkbox .was-validated .shiny-input-radiogroup .radio label>input:valid~label,.shiny-input-radiogroup .radio .shiny-input-checkboxgroup .checkbox .was-validated label>input:valid~label,.was-validated .shiny-input-checkboxgroup .radio .form-check-input:valid~label,.was-validated .shiny-input-checkboxgroup .radio .checkbox label>input:valid~label,.was-validated .shiny-input-checkboxgroup .checkbox .radio label>input:valid~label,.was-validated .shiny-input-checkboxgroup .radio label>input:valid~label,.was-validated .shiny-input-checkboxgroup .radio .shiny-input-radiogroup .checkbox label>input:valid~label,.shiny-input-radiogroup .checkbox .was-validated .shiny-input-checkboxgroup .radio label>input:valid~label,.was-validated .shiny-input-checkboxgroup .shiny-input-radiogroup .radio label>input:valid~label,.shiny-input-radiogroup .was-validated .shiny-input-checkboxgroup .radio label>input:valid~label,.shiny-input-checkboxgroup .radio .was-validated .form-check-input:valid~label,.shiny-input-checkboxgroup .radio .was-validated .checkbox label>input:valid~label,.shiny-input-checkboxgroup .checkbox .radio .was-validated label>input:valid~label,.shiny-input-checkboxgroup .radio .was-validated label>input:valid~label,.shiny-input-checkboxgroup .radio .was-validated .shiny-input-radiogroup .checkbox label>input:valid~label,.shiny-input-radiogroup .checkbox .shiny-input-checkboxgroup .radio .was-validated label>input:valid~label,.shiny-input-checkboxgroup .shiny-input-radiogroup .radio .was-validated label>input:valid~label,.shiny-input-radiogroup .shiny-input-checkboxgroup .radio .was-validated label>input:valid~label,.was-validated .shiny-input-radiogroup .checkbox .form-check-input:valid~label,.was-validated .shiny-input-radiogroup .shiny-input-checkboxgroup .checkbox label>input:valid~label,.shiny-input-checkboxgroup .was-validated .shiny-input-radiogroup .checkbox label>input:valid~label,.was-validated .shiny-input-radiogroup .checkbox .shiny-input-checkboxgroup .radio label>input:valid~label,.shiny-input-checkboxgroup .radio .was-validated .shiny-input-radiogroup .checkbox label>input:valid~label,.was-validated .shiny-input-radiogroup .checkbox label>input:valid~label,.was-validated .shiny-input-radiogroup .checkbox .radio label>input:valid~label,.was-validated .shiny-input-radiogroup .radio .checkbox label>input:valid~label,.shiny-input-radiogroup .checkbox .was-validated .form-check-input:valid~label,.shiny-input-radiogroup .shiny-input-checkboxgroup .checkbox .was-validated label>input:valid~label,.shiny-input-checkboxgroup .shiny-input-radiogroup .checkbox .was-validated label>input:valid~label,.shiny-input-radiogroup .checkbox .was-validated .shiny-input-checkboxgroup .radio label>input:valid~label,.shiny-input-checkboxgroup .radio .shiny-input-radiogroup .checkbox .was-validated label>input:valid~label,.shiny-input-radiogroup .checkbox .was-validated label>input:valid~label,.shiny-input-radiogroup .checkbox .was-validated .radio label>input:valid~label,.shiny-input-radiogroup .radio .checkbox .was-validated label>input:valid~label,.was-validated .shiny-input-radiogroup .radio .form-check-input:valid~label,.was-validated .shiny-input-radiogroup .radio .shiny-input-checkboxgroup .checkbox label>input:valid~label,.shiny-input-checkboxgroup .checkbox .was-validated .shiny-input-radiogroup .radio label>input:valid~label,.was-validated .shiny-input-radiogroup .shiny-input-checkboxgroup .radio label>input:valid~label,.shiny-input-checkboxgroup .was-validated .shiny-input-radiogroup .radio label>input:valid~label,.was-validated .shiny-input-radiogroup .radio .checkbox label>input:valid~label,.was-validated .shiny-input-radiogroup .checkbox .radio label>input:valid~label,.was-validated .shiny-input-radiogroup .radio label>input:valid~label,.shiny-input-radiogroup .radio .was-validated .form-check-input:valid~label,.shiny-input-radiogroup .radio .was-validated .shiny-input-checkboxgroup .checkbox label>input:valid~label,.shiny-input-checkboxgroup .checkbox .shiny-input-radiogroup .radio .was-validated label>input:valid~label,.shiny-input-radiogroup .shiny-input-checkboxgroup .radio .was-validated label>input:valid~label,.shiny-input-checkboxgroup .shiny-input-radiogroup .radio .was-validated label>input:valid~label,.shiny-input-radiogroup .radio .was-validated .checkbox label>input:valid~label,.shiny-input-radiogroup .checkbox .radio .was-validated label>input:valid~label,.shiny-input-radiogroup .radio .was-validated label>input:valid~label,.form-check-input.is-valid~.form-check-label,.shiny-input-checkboxgroup .checkbox label>input.is-valid~.form-check-label,.shiny-input-checkboxgroup .radio label>input.is-valid~.form-check-label,.shiny-input-radiogroup .checkbox label>input.is-valid~.form-check-label,.shiny-input-radiogroup .radio label>input.is-valid~.form-check-label,.shiny-input-checkboxgroup .checkbox .form-check-input.is-valid~label,.shiny-input-checkboxgroup .checkbox label>input.is-valid~label,.shiny-input-checkboxgroup .checkbox .radio label>input.is-valid~label,.shiny-input-checkboxgroup .radio .checkbox label>input.is-valid~label,.shiny-input-checkboxgroup .shiny-input-radiogroup .checkbox label>input.is-valid~label,.shiny-input-radiogroup .shiny-input-checkboxgroup .checkbox label>input.is-valid~label,.shiny-input-checkboxgroup .checkbox .shiny-input-radiogroup .radio label>input.is-valid~label,.shiny-input-radiogroup .radio .shiny-input-checkboxgroup .checkbox label>input.is-valid~label,.shiny-input-checkboxgroup .radio .form-check-input.is-valid~label,.shiny-input-checkboxgroup .radio .checkbox label>input.is-valid~label,.shiny-input-checkboxgroup .checkbox .radio label>input.is-valid~label,.shiny-input-checkboxgroup .radio label>input.is-valid~label,.shiny-input-checkboxgroup .radio .shiny-input-radiogroup .checkbox label>input.is-valid~label,.shiny-input-radiogroup .checkbox .shiny-input-checkboxgroup .radio label>input.is-valid~label,.shiny-input-checkboxgroup .shiny-input-radiogroup .radio label>input.is-valid~label,.shiny-input-radiogroup .shiny-input-checkboxgroup .radio label>input.is-valid~label,.shiny-input-radiogroup .checkbox .form-check-input.is-valid~label,.shiny-input-radiogroup .shiny-input-checkboxgroup .checkbox label>input.is-valid~label,.shiny-input-checkboxgroup .shiny-input-radiogroup .checkbox label>input.is-valid~label,.shiny-input-radiogroup .checkbox .shiny-input-checkboxgroup .radio label>input.is-valid~label,.shiny-input-checkboxgroup .radio .shiny-input-radiogroup .checkbox label>input.is-valid~label,.shiny-input-radiogroup .checkbox label>input.is-valid~label,.shiny-input-radiogroup .checkbox .radio label>input.is-valid~label,.shiny-input-radiogroup .radio .checkbox label>input.is-valid~label,.shiny-input-radiogroup .radio .form-check-input.is-valid~label,.shiny-input-radiogroup .radio .shiny-input-checkboxgroup .checkbox label>input.is-valid~label,.shiny-input-checkboxgroup .checkbox .shiny-input-radiogroup .radio label>input.is-valid~label,.shiny-input-radiogroup .shiny-input-checkboxgroup .radio label>input.is-valid~label,.shiny-input-checkboxgroup .shiny-input-radiogroup .radio label>input.is-valid~label,.shiny-input-radiogroup .radio .checkbox label>input.is-valid~label,.shiny-input-radiogroup .checkbox .radio label>input.is-valid~label,.shiny-input-radiogroup .radio label>input.is-valid~label{color:#28a745}.was-validated .form-check-input:valid~.valid-feedback,.was-validated .shiny-input-checkboxgroup .checkbox label>input:valid~.valid-feedback,.shiny-input-checkboxgroup .checkbox .was-validated label>input:valid~.valid-feedback,.was-validated .shiny-input-checkboxgroup .radio label>input:valid~.valid-feedback,.shiny-input-checkboxgroup .radio .was-validated label>input:valid~.valid-feedback,.was-validated .shiny-input-radiogroup .checkbox label>input:valid~.valid-feedback,.shiny-input-radiogroup .checkbox .was-validated label>input:valid~.valid-feedback,.was-validated .shiny-input-radiogroup .radio label>input:valid~.valid-feedback,.shiny-input-radiogroup .radio .was-validated label>input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip,.was-validated .shiny-input-checkboxgroup .checkbox label>input:valid~.valid-tooltip,.shiny-input-checkboxgroup .checkbox .was-validated label>input:valid~.valid-tooltip,.was-validated .shiny-input-checkboxgroup .radio label>input:valid~.valid-tooltip,.shiny-input-checkboxgroup .radio .was-validated label>input:valid~.valid-tooltip,.was-validated .shiny-input-radiogroup .checkbox label>input:valid~.valid-tooltip,.shiny-input-radiogroup .checkbox .was-validated label>input:valid~.valid-tooltip,.was-validated .shiny-input-radiogroup .radio label>input:valid~.valid-tooltip,.shiny-input-radiogroup .radio .was-validated label>input:valid~.valid-tooltip,.form-check-input.is-valid~.valid-feedback,.shiny-input-checkboxgroup .checkbox label>input.is-valid~.valid-feedback,.shiny-input-checkboxgroup .radio label>input.is-valid~.valid-feedback,.shiny-input-radiogroup .checkbox label>input.is-valid~.valid-feedback,.shiny-input-radiogroup .radio label>input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.shiny-input-checkboxgroup .checkbox label>input.is-valid~.valid-tooltip,.shiny-input-checkboxgroup .radio label>input.is-valid~.valid-tooltip,.shiny-input-radiogroup .checkbox label>input.is-valid~.valid-tooltip,.shiny-input-radiogroup .radio label>input.is-valid~.valid-tooltip{display:block}.was-validated .custom-control-input:valid~.custom-control-label,.custom-control-input.is-valid~.custom-control-label{color:#28a745}.was-validated .custom-control-input:valid~.custom-control-label::before,.custom-control-input.is-valid~.custom-control-label::before{border-color:#28a745}.was-validated .custom-control-input:valid:checked~.custom-control-label::before,.custom-control-input.is-valid:checked~.custom-control-label::before{border-color:#34ce57;background-color:#34ce57}.was-validated .custom-control-input:valid:focus~.custom-control-label::before,.custom-control-input.is-valid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(40,167,69,0.25)}.was-validated .custom-control-input:valid:focus:not(:checked)~.custom-control-label::before,.custom-control-input.is-valid:focus:not(:checked)~.custom-control-label::before{border-color:#28a745}.was-validated .custom-file-input:valid~.custom-file-label,.custom-file-input.is-valid~.custom-file-label{border-color:#28a745}.was-validated .custom-file-input:valid:focus~.custom-file-label,.custom-file-input.is-valid:focus~.custom-file-label{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,0.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;left:0;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(220,53,69,0.9);border-radius:.25rem}.form-row>.col>.invalid-tooltip,.form-row>[class*="col-"]>.invalid-tooltip{left:5px}.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip,.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip{display:block}.was-validated .form-control:invalid,.form-control.is-invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.was-validated .form-control:invalid:focus,.form-control.is-invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,0.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.was-validated .custom-select:invalid,.custom-select.is-invalid{border-color:#dc3545;padding-right:calc(.75em + 2.3125rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") right .75rem center/8px 10px no-repeat,#fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e") center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem) no-repeat}.was-validated .custom-select:invalid:focus,.custom-select.is-invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,0.25)}.was-validated .form-check-input:invalid~.form-check-label,.was-validated .shiny-input-checkboxgroup .checkbox label>input:invalid~.form-check-label,.shiny-input-checkboxgroup .checkbox .was-validated label>input:invalid~.form-check-label,.was-validated .shiny-input-checkboxgroup .radio label>input:invalid~.form-check-label,.shiny-input-checkboxgroup .radio .was-validated label>input:invalid~.form-check-label,.was-validated .shiny-input-radiogroup .checkbox label>input:invalid~.form-check-label,.shiny-input-radiogroup .checkbox .was-validated label>input:invalid~.form-check-label,.was-validated .shiny-input-radiogroup .radio label>input:invalid~.form-check-label,.shiny-input-radiogroup .radio .was-validated label>input:invalid~.form-check-label,.was-validated .shiny-input-checkboxgroup .checkbox .form-check-input:invalid~label,.was-validated .shiny-input-checkboxgroup .checkbox label>input:invalid~label,.was-validated .shiny-input-checkboxgroup .checkbox .radio label>input:invalid~label,.was-validated .shiny-input-checkboxgroup .radio .checkbox label>input:invalid~label,.was-validated .shiny-input-checkboxgroup .shiny-input-radiogroup .checkbox label>input:invalid~label,.shiny-input-radiogroup .was-validated .shiny-input-checkboxgroup .checkbox label>input:invalid~label,.was-validated .shiny-input-checkboxgroup .checkbox .shiny-input-radiogroup .radio label>input:invalid~label,.shiny-input-radiogroup .radio .was-validated .shiny-input-checkboxgroup .checkbox label>input:invalid~label,.shiny-input-checkboxgroup .checkbox .was-validated .form-check-input:invalid~label,.shiny-input-checkboxgroup .checkbox .was-validated label>input:invalid~label,.shiny-input-checkboxgroup .checkbox .was-validated .radio label>input:invalid~label,.shiny-input-checkboxgroup .radio .checkbox .was-validated label>input:invalid~label,.shiny-input-checkboxgroup .shiny-input-radiogroup .checkbox .was-validated label>input:invalid~label,.shiny-input-radiogroup .shiny-input-checkboxgroup .checkbox .was-validated label>input:invalid~label,.shiny-input-checkboxgroup .checkbox .was-validated .shiny-input-radiogroup .radio label>input:invalid~label,.shiny-input-radiogroup .radio .shiny-input-checkboxgroup .checkbox .was-validated label>input:invalid~label,.was-validated .shiny-input-checkboxgroup .radio .form-check-input:invalid~label,.was-validated .shiny-input-checkboxgroup .radio .checkbox label>input:invalid~label,.was-validated .shiny-input-checkboxgroup .checkbox .radio label>input:invalid~label,.was-validated .shiny-input-checkboxgroup .radio label>input:invalid~label,.was-validated .shiny-input-checkboxgroup .radio .shiny-input-radiogroup .checkbox label>input:invalid~label,.shiny-input-radiogroup .checkbox .was-validated .shiny-input-checkboxgroup .radio label>input:invalid~label,.was-validated .shiny-input-checkboxgroup .shiny-input-radiogroup .radio label>input:invalid~label,.shiny-input-radiogroup .was-validated .shiny-input-checkboxgroup .radio label>input:invalid~label,.shiny-input-checkboxgroup .radio .was-validated .form-check-input:invalid~label,.shiny-input-checkboxgroup .radio .was-validated .checkbox label>input:invalid~label,.shiny-input-checkboxgroup .checkbox .radio .was-validated label>input:invalid~label,.shiny-input-checkboxgroup .radio .was-validated label>input:invalid~label,.shiny-input-checkboxgroup .radio .was-validated .shiny-input-radiogroup .checkbox label>input:invalid~label,.shiny-input-radiogroup .checkbox .shiny-input-checkboxgroup .radio .was-validated label>input:invalid~label,.shiny-input-checkboxgroup .shiny-input-radiogroup .radio .was-validated label>input:invalid~label,.shiny-input-radiogroup .shiny-input-checkboxgroup .radio .was-validated label>input:invalid~label,.was-validated .shiny-input-radiogroup .checkbox .form-check-input:invalid~label,.was-validated .shiny-input-radiogroup .shiny-input-checkboxgroup .checkbox label>input:invalid~label,.shiny-input-checkboxgroup .was-validated .shiny-input-radiogroup .checkbox label>input:invalid~label,.was-validated .shiny-input-radiogroup .checkbox .shiny-input-checkboxgroup .radio label>input:invalid~label,.shiny-input-checkboxgroup .radio .was-validated .shiny-input-radiogroup .checkbox label>input:invalid~label,.was-validated .shiny-input-radiogroup .checkbox label>input:invalid~label,.was-validated .shiny-input-radiogroup .checkbox .radio label>input:invalid~label,.was-validated .shiny-input-radiogroup .radio .checkbox label>input:invalid~label,.shiny-input-radiogroup .checkbox .was-validated .form-check-input:invalid~label,.shiny-input-radiogroup .shiny-input-checkboxgroup .checkbox .was-validated label>input:invalid~label,.shiny-input-checkboxgroup .shiny-input-radiogroup .checkbox .was-validated label>input:invalid~label,.shiny-input-radiogroup .checkbox .was-validated .shiny-input-checkboxgroup .radio label>input:invalid~label,.shiny-input-checkboxgroup .radio .shiny-input-radiogroup .checkbox .was-validated label>input:invalid~label,.shiny-input-radiogroup .checkbox .was-validated label>input:invalid~label,.shiny-input-radiogroup .checkbox .was-validated .radio label>input:invalid~label,.shiny-input-radiogroup .radio .checkbox .was-validated label>input:invalid~label,.was-validated .shiny-input-radiogroup .radio .form-check-input:invalid~label,.was-validated .shiny-input-radiogroup .radio .shiny-input-checkboxgroup .checkbox label>input:invalid~label,.shiny-input-checkboxgroup .checkbox .was-validated .shiny-input-radiogroup .radio label>input:invalid~label,.was-validated .shiny-input-radiogroup .shiny-input-checkboxgroup .radio label>input:invalid~label,.shiny-input-checkboxgroup .was-validated .shiny-input-radiogroup .radio label>input:invalid~label,.was-validated .shiny-input-radiogroup .radio .checkbox label>input:invalid~label,.was-validated .shiny-input-radiogroup .checkbox .radio label>input:invalid~label,.was-validated .shiny-input-radiogroup .radio label>input:invalid~label,.shiny-input-radiogroup .radio .was-validated .form-check-input:invalid~label,.shiny-input-radiogroup .radio .was-validated .shiny-input-checkboxgroup .checkbox label>input:invalid~label,.shiny-input-checkboxgroup .checkbox .shiny-input-radiogroup .radio .was-validated label>input:invalid~label,.shiny-input-radiogroup .shiny-input-checkboxgroup .radio .was-validated label>input:invalid~label,.shiny-input-checkboxgroup .shiny-input-radiogroup .radio .was-validated label>input:invalid~label,.shiny-input-radiogroup .radio .was-validated .checkbox label>input:invalid~label,.shiny-input-radiogroup .checkbox .radio .was-validated label>input:invalid~label,.shiny-input-radiogroup .radio .was-validated label>input:invalid~label,.form-check-input.is-invalid~.form-check-label,.shiny-input-checkboxgroup .checkbox label>input.is-invalid~.form-check-label,.shiny-input-checkboxgroup .radio label>input.is-invalid~.form-check-label,.shiny-input-radiogroup .checkbox label>input.is-invalid~.form-check-label,.shiny-input-radiogroup .radio label>input.is-invalid~.form-check-label,.shiny-input-checkboxgroup .checkbox .form-check-input.is-invalid~label,.shiny-input-checkboxgroup .checkbox label>input.is-invalid~label,.shiny-input-checkboxgroup .checkbox .radio label>input.is-invalid~label,.shiny-input-checkboxgroup .radio .checkbox label>input.is-invalid~label,.shiny-input-checkboxgroup .shiny-input-radiogroup .checkbox label>input.is-invalid~label,.shiny-input-radiogroup .shiny-input-checkboxgroup .checkbox label>input.is-invalid~label,.shiny-input-checkboxgroup .checkbox .shiny-input-radiogroup .radio label>input.is-invalid~label,.shiny-input-radiogroup .radio .shiny-input-checkboxgroup .checkbox label>input.is-invalid~label,.shiny-input-checkboxgroup .radio .form-check-input.is-invalid~label,.shiny-input-checkboxgroup .radio .checkbox label>input.is-invalid~label,.shiny-input-checkboxgroup .checkbox .radio label>input.is-invalid~label,.shiny-input-checkboxgroup .radio label>input.is-invalid~label,.shiny-input-checkboxgroup .radio .shiny-input-radiogroup .checkbox label>input.is-invalid~label,.shiny-input-radiogroup .checkbox .shiny-input-checkboxgroup .radio label>input.is-invalid~label,.shiny-input-checkboxgroup .shiny-input-radiogroup .radio label>input.is-invalid~label,.shiny-input-radiogroup .shiny-input-checkboxgroup .radio label>input.is-invalid~label,.shiny-input-radiogroup .checkbox .form-check-input.is-invalid~label,.shiny-input-radiogroup .shiny-input-checkboxgroup .checkbox label>input.is-invalid~label,.shiny-input-checkboxgroup .shiny-input-radiogroup .checkbox label>input.is-invalid~label,.shiny-input-radiogroup .checkbox .shiny-input-checkboxgroup .radio label>input.is-invalid~label,.shiny-input-checkboxgroup .radio .shiny-input-radiogroup .checkbox label>input.is-invalid~label,.shiny-input-radiogroup .checkbox label>input.is-invalid~label,.shiny-input-radiogroup .checkbox .radio label>input.is-invalid~label,.shiny-input-radiogroup .radio .checkbox label>input.is-invalid~label,.shiny-input-radiogroup .radio .form-check-input.is-invalid~label,.shiny-input-radiogroup .radio .shiny-input-checkboxgroup .checkbox label>input.is-invalid~label,.shiny-input-checkboxgroup .checkbox .shiny-input-radiogroup .radio label>input.is-invalid~label,.shiny-input-radiogroup .shiny-input-checkboxgroup .radio label>input.is-invalid~label,.shiny-input-checkboxgroup .shiny-input-radiogroup .radio label>input.is-invalid~label,.shiny-input-radiogroup .radio .checkbox label>input.is-invalid~label,.shiny-input-radiogroup .checkbox .radio label>input.is-invalid~label,.shiny-input-radiogroup .radio label>input.is-invalid~label{color:#dc3545}.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .shiny-input-checkboxgroup .checkbox label>input:invalid~.invalid-feedback,.shiny-input-checkboxgroup .checkbox .was-validated label>input:invalid~.invalid-feedback,.was-validated .shiny-input-checkboxgroup .radio label>input:invalid~.invalid-feedback,.shiny-input-checkboxgroup .radio .was-validated label>input:invalid~.invalid-feedback,.was-validated .shiny-input-radiogroup .checkbox label>input:invalid~.invalid-feedback,.shiny-input-radiogroup .checkbox .was-validated label>input:invalid~.invalid-feedback,.was-validated .shiny-input-radiogroup .radio label>input:invalid~.invalid-feedback,.shiny-input-radiogroup .radio .was-validated label>input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip,.was-validated .shiny-input-checkboxgroup .checkbox label>input:invalid~.invalid-tooltip,.shiny-input-checkboxgroup .checkbox .was-validated label>input:invalid~.invalid-tooltip,.was-validated .shiny-input-checkboxgroup .radio label>input:invalid~.invalid-tooltip,.shiny-input-checkboxgroup .radio .was-validated label>input:invalid~.invalid-tooltip,.was-validated .shiny-input-radiogroup .checkbox label>input:invalid~.invalid-tooltip,.shiny-input-radiogroup .checkbox .was-validated label>input:invalid~.invalid-tooltip,.was-validated .shiny-input-radiogroup .radio label>input:invalid~.invalid-tooltip,.shiny-input-radiogroup .radio .was-validated label>input:invalid~.invalid-tooltip,.form-check-input.is-invalid~.invalid-feedback,.shiny-input-checkboxgroup .checkbox label>input.is-invalid~.invalid-feedback,.shiny-input-checkboxgroup .radio label>input.is-invalid~.invalid-feedback,.shiny-input-radiogroup .checkbox label>input.is-invalid~.invalid-feedback,.shiny-input-radiogroup .radio label>input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.shiny-input-checkboxgroup .checkbox label>input.is-invalid~.invalid-tooltip,.shiny-input-checkboxgroup .radio label>input.is-invalid~.invalid-tooltip,.shiny-input-radiogroup .checkbox label>input.is-invalid~.invalid-tooltip,.shiny-input-radiogroup .radio label>input.is-invalid~.invalid-tooltip{display:block}.was-validated .custom-control-input:invalid~.custom-control-label,.custom-control-input.is-invalid~.custom-control-label{color:#dc3545}.was-validated .custom-control-input:invalid~.custom-control-label::before,.custom-control-input.is-invalid~.custom-control-label::before{border-color:#dc3545}.was-validated .custom-control-input:invalid:checked~.custom-control-label::before,.custom-control-input.is-invalid:checked~.custom-control-label::before{border-color:#e4606d;background-color:#e4606d}.was-validated .custom-control-input:invalid:focus~.custom-control-label::before,.custom-control-input.is-invalid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(220,53,69,0.25)}.was-validated .custom-control-input:invalid:focus:not(:checked)~.custom-control-label::before,.custom-control-input.is-invalid:focus:not(:checked)~.custom-control-label::before{border-color:#dc3545}.was-validated .custom-file-input:invalid~.custom-file-label,.custom-file-input.is-invalid~.custom-file-label{border-color:#dc3545}.was-validated .custom-file-input:invalid:focus~.custom-file-label,.custom-file-input.is-invalid:focus~.custom-file-label{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,0.25)}.form-inline{display:flex;display:-webkit-flex;flex-flow:row wrap;-webkit-flex-flow:row wrap;align-items:center;-webkit-align-items:center}.form-inline .form-check,.form-inline .shiny-input-checkboxgroup .checkbox,.shiny-input-checkboxgroup .form-inline .checkbox,.form-inline .shiny-input-checkboxgroup .radio,.shiny-input-checkboxgroup .form-inline .radio,.form-inline .shiny-input-radiogroup .checkbox,.shiny-input-radiogroup .form-inline .checkbox,.form-inline .shiny-input-radiogroup .radio,.shiny-input-radiogroup .form-inline .radio{width:100%}@media (min-width: 576px){.form-inline label{display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;justify-content:center;-webkit-justify-content:center;margin-bottom:0}.form-inline .form-group{display:flex;display:-webkit-flex;flex:0 0 auto;-webkit-flex:0 0 auto;flex-flow:row wrap;-webkit-flex-flow:row wrap;align-items:center;-webkit-align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .input-group,.form-inline .custom-select{width:auto}.form-inline .form-check,.form-inline .shiny-input-checkboxgroup .checkbox,.shiny-input-checkboxgroup .form-inline .checkbox,.form-inline .shiny-input-checkboxgroup .radio,.shiny-input-checkboxgroup .form-inline .radio,.form-inline .shiny-input-radiogroup .checkbox,.shiny-input-radiogroup .form-inline .checkbox,.form-inline .shiny-input-radiogroup .radio,.shiny-input-radiogroup .form-inline .radio{display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;justify-content:center;-webkit-justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input,.form-inline .shiny-input-checkboxgroup .checkbox label>input,.shiny-input-checkboxgroup .checkbox .form-inline label>input,.form-inline .shiny-input-checkboxgroup .radio label>input,.shiny-input-checkboxgroup .radio .form-inline label>input,.form-inline .shiny-input-radiogroup .checkbox label>input,.shiny-input-radiogroup .checkbox .form-inline label>input,.form-inline .shiny-input-radiogroup .radio label>input,.shiny-input-radiogroup .radio .form-inline label>input{position:relative;flex-shrink:0;-webkit-flex-shrink:0;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{align-items:center;-webkit-align-items:center;justify-content:center;-webkit-justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#212529;text-align:center;vertical-align:middle;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.btn{transition:none}}.btn:hover{color:#212529;text-decoration:none}.btn:focus,.btn.focus{outline:0;box-shadow:0 0 0 .2rem rgba(30,144,255,0.25)}.btn.disabled,.btn:disabled{opacity:.65}.btn:not(:disabled):not(.disabled){cursor:pointer}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-default{color:#000;background-color:#dee2e6;border-color:#dee2e6}.btn-default:hover{color:#000;background-color:#c8cfd6;border-color:#c1c9d0}.btn-default:focus,.btn-default.focus{color:#000;background-color:#c8cfd6;border-color:#c1c9d0;box-shadow:0 0 0 .2rem rgba(189,192,196,0.5)}.btn-default.disabled,.btn-default:disabled{color:#000;background-color:#dee2e6;border-color:#dee2e6}.btn-default:not(:disabled):not(.disabled):active,.btn-default:not(:disabled):not(.disabled).active,.show>.btn-default.dropdown-toggle,.in>.btn-default.dropdown-toggle{color:#000;background-color:#c1c9d0;border-color:#bac2cb}.btn-default:not(:disabled):not(.disabled):active:focus,.btn-default:not(:disabled):not(.disabled).active:focus,.show>.btn-default.dropdown-toggle:focus,.in>.btn-default.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(189,192,196,0.5)}.btn-primary{color:#fff;background-color:#1e90ff;border-color:#1e90ff}.btn-primary:hover{color:#fff;background-color:#007df7;border-color:#0077ea}.btn-primary:focus,.btn-primary.focus{color:#fff;background-color:#007df7;border-color:#0077ea;box-shadow:0 0 0 .2rem rgba(64,161,255,0.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#1e90ff;border-color:#1e90ff}.btn-primary:not(:disabled):not(.disabled):active,.btn-primary:not(:disabled):not(.disabled).active,.show>.btn-primary.dropdown-toggle,.in>.btn-primary.dropdown-toggle{color:#fff;background-color:#0077ea;border-color:#0070dd}.btn-primary:not(:disabled):not(.disabled):active:focus,.btn-primary:not(:disabled):not(.disabled).active:focus,.show>.btn-primary.dropdown-toggle:focus,.in>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(64,161,255,0.5)}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5a6268;border-color:#545b62}.btn-secondary:focus,.btn-secondary.focus{color:#fff;background-color:#5a6268;border-color:#545b62;box-shadow:0 0 0 .2rem rgba(130,138,145,0.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:not(:disabled):not(.disabled):active,.btn-secondary:not(:disabled):not(.disabled).active,.show>.btn-secondary.dropdown-toggle,.in>.btn-secondary.dropdown-toggle{color:#fff;background-color:#545b62;border-color:#4e555b}.btn-secondary:not(:disabled):not(.disabled):active:focus,.btn-secondary:not(:disabled):not(.disabled).active:focus,.show>.btn-secondary.dropdown-toggle:focus,.in>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(130,138,145,0.5)}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{color:#fff;background-color:#218838;border-color:#1e7e34}.btn-success:focus,.btn-success.focus{color:#fff;background-color:#218838;border-color:#1e7e34;box-shadow:0 0 0 .2rem rgba(72,180,97,0.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:not(:disabled):not(.disabled):active,.btn-success:not(:disabled):not(.disabled).active,.show>.btn-success.dropdown-toggle,.in>.btn-success.dropdown-toggle{color:#fff;background-color:#1e7e34;border-color:#1c7430}.btn-success:not(:disabled):not(.disabled):active:focus,.btn-success:not(:disabled):not(.disabled).active:focus,.show>.btn-success.dropdown-toggle:focus,.in>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(72,180,97,0.5)}.btn-info{color:#000;background-color:#0DCAF0;border-color:#0DCAF0}.btn-info:hover{color:#000;background-color:#0babcc;border-color:#0aa1c0}.btn-info:focus,.btn-info.focus{color:#000;background-color:#0babcc;border-color:#0aa1c0;box-shadow:0 0 0 .2rem rgba(11,172,204,0.5)}.btn-info.disabled,.btn-info:disabled{color:#000;background-color:#0DCAF0;border-color:#0DCAF0}.btn-info:not(:disabled):not(.disabled):active,.btn-info:not(:disabled):not(.disabled).active,.show>.btn-info.dropdown-toggle,.in>.btn-info.dropdown-toggle{color:#fff;background-color:#0aa1c0;border-color:#0a97b4}.btn-info:not(:disabled):not(.disabled):active:focus,.btn-info:not(:disabled):not(.disabled).active:focus,.show>.btn-info.dropdown-toggle:focus,.in>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(11,172,204,0.5)}.btn-warning{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#000;background-color:#e0a800;border-color:#d39e00}.btn-warning:focus,.btn-warning.focus{color:#000;background-color:#e0a800;border-color:#d39e00;box-shadow:0 0 0 .2rem rgba(217,164,6,0.5)}.btn-warning.disabled,.btn-warning:disabled{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled):active,.btn-warning:not(:disabled):not(.disabled).active,.show>.btn-warning.dropdown-toggle,.in>.btn-warning.dropdown-toggle{color:#000;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled):active:focus,.btn-warning:not(:disabled):not(.disabled).active:focus,.show>.btn-warning.dropdown-toggle:focus,.in>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(217,164,6,0.5)}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#c82333;border-color:#bd2130}.btn-danger:focus,.btn-danger.focus{color:#fff;background-color:#c82333;border-color:#bd2130;box-shadow:0 0 0 .2rem rgba(225,83,97,0.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:not(:disabled):not(.disabled):active,.btn-danger:not(:disabled):not(.disabled).active,.show>.btn-danger.dropdown-toggle,.in>.btn-danger.dropdown-toggle{color:#fff;background-color:#bd2130;border-color:#b21f2d}.btn-danger:not(:disabled):not(.disabled):active:focus,.btn-danger:not(:disabled):not(.disabled).active:focus,.show>.btn-danger.dropdown-toggle:focus,.in>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(225,83,97,0.5)}.btn-light{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#000;background-color:#e2e6ea;border-color:#dae0e5}.btn-light:focus,.btn-light.focus{color:#000;background-color:#e2e6ea;border-color:#dae0e5;box-shadow:0 0 0 .2rem rgba(211,212,213,0.5)}.btn-light.disabled,.btn-light:disabled{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:not(:disabled):not(.disabled):active,.btn-light:not(:disabled):not(.disabled).active,.show>.btn-light.dropdown-toggle,.in>.btn-light.dropdown-toggle{color:#000;background-color:#dae0e5;border-color:#d3d9df}.btn-light:not(:disabled):not(.disabled):active:focus,.btn-light:not(:disabled):not(.disabled).active:focus,.show>.btn-light.dropdown-toggle:focus,.in>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(211,212,213,0.5)}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark:focus,.btn-dark.focus{color:#fff;background-color:#23272b;border-color:#1d2124;box-shadow:0 0 0 .2rem rgba(82,88,93,0.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled):active,.btn-dark:not(:disabled):not(.disabled).active,.show>.btn-dark.dropdown-toggle,.in>.btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled):active:focus,.btn-dark:not(:disabled):not(.disabled).active:focus,.show>.btn-dark.dropdown-toggle:focus,.in>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(82,88,93,0.5)}.btn-outline-default{color:#dee2e6;border-color:#dee2e6;background-color:transparent}.btn-outline-default:hover{color:#000;background-color:#dee2e6;border-color:#dee2e6}.btn-outline-default:focus,.btn-outline-default.focus{box-shadow:0 0 0 .2rem rgba(222,226,230,0.5)}.btn-outline-default.disabled,.btn-outline-default:disabled{color:#dee2e6;background-color:transparent}.btn-outline-default:not(:disabled):not(.disabled):active,.btn-outline-default:not(:disabled):not(.disabled).active,.show>.btn-outline-default.dropdown-toggle,.in>.btn-outline-default.dropdown-toggle{color:#000;background-color:#dee2e6;border-color:#dee2e6}.btn-outline-default:not(:disabled):not(.disabled):active:focus,.btn-outline-default:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-default.dropdown-toggle:focus,.in>.btn-outline-default.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(222,226,230,0.5)}.btn-outline-primary{color:#1e90ff;border-color:#1e90ff;background-color:transparent}.btn-outline-primary:hover{color:#fff;background-color:#1e90ff;border-color:#1e90ff}.btn-outline-primary:focus,.btn-outline-primary.focus{box-shadow:0 0 0 .2rem rgba(30,144,255,0.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#1e90ff;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled):active,.btn-outline-primary:not(:disabled):not(.disabled).active,.show>.btn-outline-primary.dropdown-toggle,.in>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#1e90ff;border-color:#1e90ff}.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-primary.dropdown-toggle:focus,.in>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(30,144,255,0.5)}.btn-outline-secondary{color:#6c757d;border-color:#6c757d;background-color:transparent}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary:focus,.btn-outline-secondary.focus{box-shadow:0 0 0 .2rem rgba(108,117,125,0.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled):active,.btn-outline-secondary:not(:disabled):not(.disabled).active,.show>.btn-outline-secondary.dropdown-toggle,.in>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus,.in>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,0.5)}.btn-outline-success{color:#28a745;border-color:#28a745;background-color:transparent}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success:focus,.btn-outline-success.focus{box-shadow:0 0 0 .2rem rgba(40,167,69,0.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled):active,.btn-outline-success:not(:disabled):not(.disabled).active,.show>.btn-outline-success.dropdown-toggle,.in>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success:not(:disabled):not(.disabled):active:focus,.btn-outline-success:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-success.dropdown-toggle:focus,.in>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,0.5)}.btn-outline-info{color:#0DCAF0;border-color:#0DCAF0;background-color:transparent}.btn-outline-info:hover{color:#000;background-color:#0DCAF0;border-color:#0DCAF0}.btn-outline-info:focus,.btn-outline-info.focus{box-shadow:0 0 0 .2rem rgba(13,202,240,0.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#0DCAF0;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled):active,.btn-outline-info:not(:disabled):not(.disabled).active,.show>.btn-outline-info.dropdown-toggle,.in>.btn-outline-info.dropdown-toggle{color:#000;background-color:#0DCAF0;border-color:#0DCAF0}.btn-outline-info:not(:disabled):not(.disabled):active:focus,.btn-outline-info:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-info.dropdown-toggle:focus,.in>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(13,202,240,0.5)}.btn-outline-warning{color:#ffc107;border-color:#ffc107;background-color:transparent}.btn-outline-warning:hover{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:focus,.btn-outline-warning.focus{box-shadow:0 0 0 .2rem rgba(255,193,7,0.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled):active,.btn-outline-warning:not(:disabled):not(.disabled).active,.show>.btn-outline-warning.dropdown-toggle,.in>.btn-outline-warning.dropdown-toggle{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-warning.dropdown-toggle:focus,.in>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,0.5)}.btn-outline-danger{color:#dc3545;border-color:#dc3545;background-color:transparent}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:focus,.btn-outline-danger.focus{box-shadow:0 0 0 .2rem rgba(220,53,69,0.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled):active,.btn-outline-danger:not(:disabled):not(.disabled).active,.show>.btn-outline-danger.dropdown-toggle,.in>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-danger.dropdown-toggle:focus,.in>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,0.5)}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa;background-color:transparent}.btn-outline-light:hover{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:focus,.btn-outline-light.focus{box-shadow:0 0 0 .2rem rgba(248,249,250,0.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled):active,.btn-outline-light:not(:disabled):not(.disabled).active,.show>.btn-outline-light.dropdown-toggle,.in>.btn-outline-light.dropdown-toggle{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:not(:disabled):not(.disabled):active:focus,.btn-outline-light:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-light.dropdown-toggle:focus,.in>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,0.5)}.btn-outline-dark{color:#343a40;border-color:#343a40;background-color:transparent}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:focus,.btn-outline-dark.focus{box-shadow:0 0 0 .2rem rgba(52,58,64,0.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled):active,.btn-outline-dark:not(:disabled):not(.disabled).active,.show>.btn-outline-dark.dropdown-toggle,.in>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-dark.dropdown-toggle:focus,.in>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,0.5)}.btn-link{font-weight:400;color:#1e90ff;text-decoration:none;-webkit-text-decoration:none;-moz-text-decoration:none;-ms-text-decoration:none;-o-text-decoration:none}.btn-link:hover{color:#006ad1;text-decoration:underline;-webkit-text-decoration:underline;-moz-text-decoration:underline;-ms-text-decoration:underline;-o-text-decoration:underline}.btn-link:focus,.btn-link.focus{text-decoration:underline;-webkit-text-decoration:underline;-moz-text-decoration:underline;-ms-text-decoration:underline;-o-text-decoration:underline}.btn-link:disabled,.btn-link.disabled{color:#6c757d;pointer-events:none}.btn-lg,.btn-group-lg>.btn{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-sm,.btn-group-sm>.btn{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{transition:opacity 0.15s linear}@media (prefers-reduced-motion: reduce){.fade{transition:none}}.fade:not(.show):not(.in){opacity:0}.collapse:not(.show):not(.in){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height 0.35s ease}@media (prefers-reduced-motion: reduce){.collapsing{transition:none}}.dropup,.dropright,.dropdown,.dropleft{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,0.15);border-radius:.25rem}.dropdown-menu-left{right:auto;left:0}.dropdown-menu-right{right:0;left:auto}@media (min-width: 576px){.dropdown-menu-sm-left{right:auto;left:0}.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width: 768px){.dropdown-menu-md-left{right:auto;left:0}.dropdown-menu-md-right{right:0;left:auto}}@media (min-width: 992px){.dropdown-menu-lg-left{right:auto;left:0}.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width: 1200px){.dropdown-menu-xl-left{right:auto;left:0}.dropdown-menu-xl-right{right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^="top"],.dropdown-menu[x-placement^="right"],.dropdown-menu[x-placement^="bottom"],.dropdown-menu[x-placement^="left"]{right:auto;bottom:auto}.dropdown-divider,.dropdown-menu>li.divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item,.dropdown-menu>li>a{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:hover,.dropdown-menu>li>a:hover,.dropdown-item:focus,.dropdown-menu>li>a:focus{color:#16181b;text-decoration:none;background-color:#e9ecef}.dropdown-item.active,.dropdown-menu>li>a.active,.dropdown-item:active,.dropdown-menu>li>a:active{color:#fff;text-decoration:none;background-color:#1e90ff}.dropdown-item.disabled,.dropdown-menu>li>a.disabled,.dropdown-item:disabled,.dropdown-menu>li>a:disabled{color:#adb5bd;pointer-events:none;background-color:transparent}.dropdown-menu.show,.dropdown-menu.in{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1.5rem;color:#212529}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;flex:1 1 auto;-webkit-flex:1 1 auto}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover{z-index:1}.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn.active{z-index:1}.btn-toolbar{display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;justify-content:flex-start;-webkit-justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn:not(:first-child),.btn-group>.btn-group:not(:first-child){margin-left:-1px}.btn-group>.btn:not(:last-child):not(.dropdown-toggle),.btn-group>.btn-group:not(:last-child)>.btn{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:not(:first-child),.btn-group>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-sm+.dropdown-toggle-split,.btn-group-sm>.btn+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-lg+.dropdown-toggle-split,.btn-group-lg>.btn+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;-webkit-flex-direction:column;align-items:flex-start;-webkit-align-items:flex-start;justify-content:center;-webkit-justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn:not(:first-child),.btn-group-vertical>.btn-group:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle),.btn-group-vertical>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:not(:first-child),.btn-group-vertical>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type="radio"],.btn-group-toggle>.btn input[type="checkbox"],.btn-group-toggle>.btn-group>.btn input[type="radio"],.btn-group-toggle>.btn-group>.btn input[type="checkbox"]{position:absolute;clip:rect(0, 0, 0, 0);pointer-events:none}.input-group{position:relative;display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;align-items:stretch;-webkit-align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-control-plaintext,.input-group>.custom-select,.input-group>.custom-file{position:relative;flex:1 1 auto;-webkit-flex:1 1 auto;width:1%;min-width:0;margin-bottom:0}.input-group>.form-control+.form-control,.input-group>.form-control+.custom-select,.input-group>.form-control+.custom-file,.input-group>.form-control-plaintext+.form-control,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.custom-file,.input-group>.custom-select+.form-control,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.custom-file,.input-group>.custom-file+.form-control,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.custom-file{margin-left:-1px}.input-group>.form-control:focus,.input-group>.custom-select:focus,.input-group>.custom-file .custom-file-input:focus~.custom-file-label{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.form-control:not(:first-child),.input-group>.custom-select:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group:not(.has-validation)>.form-control:not(:last-child),.input-group:not(.has-validation)>.custom-select:not(:last-child),.input-group:not(.has-validation)>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.form-control:nth-last-child(n + 3),.input-group.has-validation>.custom-select:nth-last-child(n + 3),.input-group.has-validation>.custom-file:nth-last-child(n + 3) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group-prepend,.input-group-append{display:flex;display:-webkit-flex}.input-group-prepend .btn,.input-group-append .btn{position:relative;z-index:2}.input-group-prepend .btn:focus,.input-group-append .btn:focus{z-index:3}.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.input-group-text,.input-group-append .input-group-text+.btn{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-text input[type="radio"],.input-group-text input[type="checkbox"]{margin-top:0}.input-group-lg>.form-control:not(textarea),.input-group-lg>.custom-select{height:calc(1.5em + 1rem + 2px)}.input-group-lg>.form-control,.input-group-lg>.custom-select,.input-group-lg>.input-group-prepend>.input-group-text,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-append>.btn{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-sm>.form-control:not(textarea),.input-group-sm>.custom-select{height:calc(1.5em + .5rem + 2px)}.input-group-sm>.form-control,.input-group-sm>.custom-select,.input-group-sm>.input-group-prepend>.input-group-text,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-append>.btn{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text,.input-group:not(.has-validation)>.input-group-append:not(:last-child)>.btn,.input-group:not(.has-validation)>.input-group-append:not(:last-child)>.input-group-text,.input-group.has-validation>.input-group-append:nth-last-child(n + 3)>.btn,.input-group.has-validation>.input-group-append:nth-last-child(n + 3)>.input-group-text,.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;z-index:1;display:block;min-height:1.5rem;padding-left:1.5rem;color-adjust:exact;-webkit-print-color-adjust:exact}.custom-control-inline{display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;left:0;z-index:-1;width:1rem;height:1.25rem;opacity:0}.custom-control-input:checked~.custom-control-label::before{color:#fff;border-color:#1e90ff;background-color:#1e90ff}.custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(30,144,255,0.25)}.custom-control-input:focus:not(:checked)~.custom-control-label::before{border-color:#9ecfff}.custom-control-input:not(:disabled):active~.custom-control-label::before{color:#000;background-color:#d1e8ff;border-color:#d1e8ff}.custom-control-input[disabled]~.custom-control-label,.custom-control-input:disabled~.custom-control-label{color:#6c757d}.custom-control-input[disabled]~.custom-control-label::before,.custom-control-input:disabled~.custom-control-label::before{background-color:#e9ecef}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label::before{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";background-color:#fff;border:#adb5bd solid 1px}.custom-control-label::after{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background:50% / 50% 50% no-repeat}.custom-checkbox .custom-control-label::before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::before{border-color:#1e90ff;background-color:#1e90ff}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(30,144,255,0.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label::before{background-color:rgba(30,144,255,0.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(30,144,255,0.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label::before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:.5rem}.custom-switch .custom-control-label::after{top:calc(.25rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#adb5bd;border-radius:.5rem;transition:transform 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.custom-switch .custom-control-label::after{transition:none}}.custom-switch .custom-control-input:checked~.custom-control-label::after{background-color:#fff;transform:translateX(.75rem)}.custom-switch .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(30,144,255,0.5)}.custom-select{display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem 1.75rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;vertical-align:middle;background:#fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") right .75rem center/8px 10px no-repeat;border:1px solid #ced4da;border-radius:.25rem;appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none}.custom-select:focus{border-color:#9ecfff;outline:0;box-shadow:0 0 0 .2rem rgba(30,144,255,0.25)}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{display:none}.custom-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}.custom-select-sm{height:calc(1.5em + .5rem + 2px);padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.custom-select-lg{height:calc(1.5em + 1rem + 2px);padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.custom-file{position:relative;display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(1.5em + .75rem + 2px);margin:0;overflow:hidden;opacity:0}.custom-file-input:focus~.custom-file-label{border-color:#9ecfff;box-shadow:0 0 0 .2rem rgba(30,144,255,0.25)}.custom-file-input[disabled]~.custom-file-label,.custom-file-input:disabled~.custom-file-label{background-color:#e9ecef}.custom-file-input:lang(en)~.custom-file-label::after{content:"Browse"}.custom-file-input~.custom-file-label[data-browse]::after{content:attr(data-browse)}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;overflow:hidden;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:calc(1.5em + .75rem);padding:.375rem .75rem;line-height:1.5;color:#495057;content:"Browse";background-color:#e9ecef;border-left:inherit;border-radius:0 .25rem .25rem 0}.custom-range{width:100%;height:1.4rem;padding:0;background-color:transparent;appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none}.custom-range:focus{outline:0}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(30,144,255,0.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(30,144,255,0.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(30,144,255,0.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#1e90ff;border:0;border-radius:1rem;transition:background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out;appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none}@media (prefers-reduced-motion: reduce){.custom-range::-webkit-slider-thumb{transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#d1e8ff}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#1e90ff;border:0;border-radius:1rem;transition:background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out;appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none}@media (prefers-reduced-motion: reduce){.custom-range::-moz-range-thumb{transition:none}}.custom-range::-moz-range-thumb:active{background-color:#d1e8ff}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:.2rem;margin-left:.2rem;background-color:#1e90ff;border:0;border-radius:1rem;transition:background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out;appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none}@media (prefers-reduced-motion: reduce){.custom-range::-ms-thumb{transition:none}}.custom-range::-ms-thumb:active{background-color:#d1e8ff}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#adb5bd}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#adb5bd}.custom-control-label::before,.custom-file-label,.custom-select{transition:background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.custom-control-label::before,.custom-file-label,.custom-select{transition:none}}.nav{display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link,.nav-tabs>li>a,.nav-pills>li>a,:where(ul.nav.navbar-nav > li)>a{display:block;padding:.5rem 1rem}.nav-link:hover,.nav-tabs>li>a:hover,.nav-pills>li>a:hover,:where(ul.nav.navbar-nav > li)>a:hover,.nav-link:focus,.nav-tabs>li>a:focus,.nav-pills>li>a:focus,:where(ul.nav.navbar-nav > li)>a:focus{text-decoration:none}.nav-link.disabled,.nav-tabs>li>a.disabled,.nav-pills>li>a.disabled,:where(ul.nav.navbar-nav > li)>a.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-link,.nav-tabs>li>a,.nav-tabs .nav-pills>li>a,.nav-tabs :where(ul.nav.navbar-nav > li)>a{margin-bottom:-1px;border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:hover,.nav-tabs>li>a:hover,.nav-tabs .nav-pills>li>a:hover,.nav-tabs :where(ul.nav.navbar-nav > li)>a:hover,.nav-tabs .nav-link:focus,.nav-tabs>li>a:focus,.nav-tabs .nav-pills>li>a:focus,.nav-tabs :where(ul.nav.navbar-nav > li)>a:focus{border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled,.nav-tabs>li>a.disabled,.nav-tabs .nav-pills>li>a.disabled,.nav-tabs :where(ul.nav.navbar-nav > li)>a.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-link.active,.nav-tabs>li>a.active,.nav-tabs .nav-pills>li>a.active,.nav-tabs :where(ul.nav.navbar-nav > li)>a.active,.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-item.in .nav-link,.nav-tabs .nav-item.show .nav-tabs>li>a,.nav-tabs .nav-item.in .nav-tabs>li>a,.nav-tabs .nav-item.show .nav-pills>li>a,.nav-tabs .nav-item.in .nav-pills>li>a,.nav-tabs>li.show .nav-link,.nav-tabs>li.in .nav-link,.nav-tabs>li.show .nav-tabs>li>a,.nav-tabs>li.in .nav-tabs>li>a,.nav-tabs>li.show .nav-pills>li>a,.nav-tabs>li.in .nav-pills>li>a,.nav-tabs .nav-pills>li.show .nav-link,.nav-tabs .nav-pills>li.in .nav-link,.nav-tabs .nav-pills>li.show .nav-tabs>li>a,.nav-tabs .nav-pills>li.in .nav-tabs>li>a,.nav-tabs .nav-pills>li.show .nav-pills>li>a,.nav-tabs .nav-pills>li.in .nav-pills>li>a,.nav-tabs .nav-item.show :where(ul.nav.navbar-nav > li)>a,.nav-tabs .nav-item.in :where(ul.nav.navbar-nav > li)>a,.nav-tabs>li.show :where(ul.nav.navbar-nav > li)>a,.nav-tabs>li.in :where(ul.nav.navbar-nav > li)>a,.nav-tabs .nav-pills>li.show :where(ul.nav.navbar-nav > li)>a,.nav-tabs .nav-pills>li.in :where(ul.nav.navbar-nav > li)>a,.nav-tabs .show:where(ul.nav.navbar-nav > li):not(.dropdown) .nav-link,.nav-tabs .in:where(ul.nav.navbar-nav > li):not(.dropdown) .nav-link,.nav-tabs .show:where(ul.nav.navbar-nav > li):not(.dropdown) .nav-tabs>li>a,.nav-tabs .in:where(ul.nav.navbar-nav > li):not(.dropdown) .nav-tabs>li>a,.nav-tabs .show:where(ul.nav.navbar-nav > li):not(.dropdown) .nav-pills>li>a,.nav-tabs .in:where(ul.nav.navbar-nav > li):not(.dropdown) .nav-pills>li>a,.nav-tabs .show:where(ul.nav.navbar-nav > li):not(.dropdown) :where(ul.nav.navbar-nav > li)>a,.nav-tabs .in:where(ul.nav.navbar-nav > li):not(.dropdown) :where(ul.nav.navbar-nav > li)>a{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link,.nav-pills .nav-tabs>li>a,.nav-pills>li>a,.nav-pills :where(ul.nav.navbar-nav > li)>a{border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .nav-tabs>li>a.active,.nav-pills>li>a.active,.nav-pills :where(ul.nav.navbar-nav > li)>a.active,.nav-pills .show>.nav-link,.nav-pills .in>.nav-link,.nav-pills .nav-tabs>li.show>a,.nav-pills .nav-tabs>li.in>a,.nav-pills>li.show>a,.nav-pills>li.in>a,.nav-pills .show:where(ul.nav.navbar-nav > li)>a,.nav-pills .in:where(ul.nav.navbar-nav > li)>a{color:#fff;background-color:#1e90ff}.nav-fill>.nav-link,.nav-tabs>li.nav-fill>a,.nav-pills>li.nav-fill>a,.nav-fill:where(ul.nav.navbar-nav > li)>a,.nav-fill .nav-item,.nav-fill .nav-tabs>li,.nav-fill .nav-pills>li,.nav-fill :where(ul.nav.navbar-nav > li):not(.dropdown){flex:1 1 auto;-webkit-flex:1 1 auto;text-align:center}.nav-justified>.nav-link,.nav-tabs>li.nav-justified>a,.nav-pills>li.nav-justified>a,.nav-justified:where(ul.nav.navbar-nav > li)>a,.nav-justified .nav-item,.nav-justified .nav-tabs>li,.nav-justified .nav-pills>li,.nav-justified :where(ul.nav.navbar-nav > li):not(.dropdown){flex-basis:0;-webkit-flex-basis:0;flex-grow:1;-webkit-flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;align-items:center;-webkit-align-items:center;justify-content:space-between;-webkit-justify-content:space-between;padding:.5rem 1rem}.navbar .container,.navbar .container-fluid,.navbar .container-sm,.navbar .container-md,.navbar .container-lg,.navbar .container-xl{display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;align-items:center;-webkit-align-items:center;justify-content:space-between;-webkit-justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}.navbar-nav{display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link,.navbar-nav .nav-tabs>li>a,.navbar-nav .nav-pills>li>a,.navbar-nav :where(ul.nav.navbar-nav > li)>a{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{flex-basis:100%;-webkit-flex-basis:100%;flex-grow:1;-webkit-flex-grow:1;align-items:center;-webkit-align-items:center}.navbar-toggler,.navbar-toggle{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:hover,.navbar-toggle:hover,.navbar-toggler:focus,.navbar-toggle:focus{text-decoration:none}.navbar-toggler-icon,.navbar-toggle>.icon-bar:last-child{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:50% / 100% 100% no-repeat}.navbar-nav-scroll{max-height:75vh;overflow-y:auto}@media (max-width: 575.98px){.navbar-expand-sm>.container,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl)>.container,.navbar-expand-sm>.container-fluid,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl)>.container-fluid,.navbar-expand-sm>.container-sm,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl)>.container-sm,.navbar-expand-sm>.container-md,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl)>.container-md,.navbar-expand-sm>.container-lg,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl)>.container-lg,.navbar-expand-sm>.container-xl,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl)>.container-xl{padding-right:0;padding-left:0}}@media (min-width: 576px){.navbar-expand-sm,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl){flex-flow:row nowrap;-webkit-flex-flow:row nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand-sm .navbar-nav,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl) .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl) .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link,.navbar-expand-sm .navbar-nav .nav-tabs>li>a,.navbar-expand-sm .navbar-nav .nav-pills>li>a,.navbar-expand-sm .navbar-nav :where(ul.nav.navbar-nav > li)>a,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl) .navbar-nav .nav-link,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl) .navbar-nav .nav-tabs>li>a,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl) .navbar-nav .nav-pills>li>a,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl) .navbar-nav :where(ul.nav.navbar-nav > li)>a{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl)>.container,.navbar-expand-sm>.container-fluid,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl)>.container-fluid,.navbar-expand-sm>.container-sm,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl)>.container-sm,.navbar-expand-sm>.container-md,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl)>.container-md,.navbar-expand-sm>.container-lg,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl)>.container-lg,.navbar-expand-sm>.container-xl,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl)>.container-xl{flex-wrap:nowrap;-webkit-flex-wrap:nowrap}.navbar-expand-sm .navbar-nav-scroll,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl) .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl) .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand-sm .navbar-toggler,.navbar-expand-sm .navbar-toggle,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl) .navbar-toggler,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl) .navbar-toggle{display:none}}@media (max-width: 767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-md,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-xl{padding-right:0;padding-left:0}}@media (min-width: 768px){.navbar-expand-md{flex-flow:row nowrap;-webkit-flex-flow:row nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link,.navbar-expand-md .navbar-nav .nav-tabs>li>a,.navbar-expand-md .navbar-nav .nav-pills>li>a,.navbar-expand-md .navbar-nav :where(ul.nav.navbar-nav > li)>a{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-md,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-xl{flex-wrap:nowrap;-webkit-flex-wrap:nowrap}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand-md .navbar-toggler,.navbar-expand-md .navbar-toggle{display:none}}@media (max-width: 991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-xl{padding-right:0;padding-left:0}}@media (min-width: 992px){.navbar-expand-lg{flex-flow:row nowrap;-webkit-flex-flow:row nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link,.navbar-expand-lg .navbar-nav .nav-tabs>li>a,.navbar-expand-lg .navbar-nav .nav-pills>li>a,.navbar-expand-lg .navbar-nav :where(ul.nav.navbar-nav > li)>a{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-xl{flex-wrap:nowrap;-webkit-flex-wrap:nowrap}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand-lg .navbar-toggler,.navbar-expand-lg .navbar-toggle{display:none}}@media (max-width: 1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-xl{padding-right:0;padding-left:0}}@media (min-width: 1200px){.navbar-expand-xl{flex-flow:row nowrap;-webkit-flex-flow:row nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link,.navbar-expand-xl .navbar-nav .nav-tabs>li>a,.navbar-expand-xl .navbar-nav .nav-pills>li>a,.navbar-expand-xl .navbar-nav :where(ul.nav.navbar-nav > li)>a{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-xl{flex-wrap:nowrap;-webkit-flex-wrap:nowrap}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand-xl .navbar-toggler,.navbar-expand-xl .navbar-toggle{display:none}}.navbar-expand{flex-flow:row nowrap;-webkit-flex-flow:row nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-sm,.navbar-expand>.container-md,.navbar-expand>.container-lg,.navbar-expand>.container-xl{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link,.navbar-expand .navbar-nav .nav-tabs>li>a,.navbar-expand .navbar-nav .nav-pills>li>a,.navbar-expand .navbar-nav :where(ul.nav.navbar-nav > li)>a{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-sm,.navbar-expand>.container-md,.navbar-expand>.container-lg,.navbar-expand>.container-xl{flex-wrap:nowrap;-webkit-flex-wrap:nowrap}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand .navbar-toggler,.navbar-expand .navbar-toggle{display:none}.navbar-light,.navbar.navbar-default{background-color:#f8f9fa}.navbar-light .navbar-brand,.navbar.navbar-default .navbar-brand{color:#000}.navbar-light .navbar-brand:hover,.navbar.navbar-default .navbar-brand:hover,.navbar-light .navbar-brand:focus,.navbar.navbar-default .navbar-brand:focus{color:#000}.navbar-light .navbar-nav .nav-link,.navbar-light .navbar-nav .nav-tabs>li>a,.navbar-light .navbar-nav .nav-pills>li>a,.navbar.navbar-default .navbar-nav .nav-link,.navbar.navbar-default .navbar-nav .nav-tabs>li>a,.navbar.navbar-default .navbar-nav .nav-pills>li>a,.navbar-light .navbar-nav :where(ul.nav.navbar-nav > li)>a,.navbar.navbar-default .navbar-nav :where(ul.nav.navbar-nav > li)>a{color:rgba(0,0,0,0.5)}.navbar-light .navbar-nav .nav-link:hover,.navbar-light .navbar-nav .nav-tabs>li>a:hover,.navbar-light .navbar-nav .nav-pills>li>a:hover,.navbar.navbar-default .navbar-nav .nav-link:hover,.navbar.navbar-default .navbar-nav .nav-tabs>li>a:hover,.navbar.navbar-default .navbar-nav .nav-pills>li>a:hover,.navbar-light .navbar-nav :where(ul.nav.navbar-nav > li)>a:hover,.navbar.navbar-default .navbar-nav :where(ul.nav.navbar-nav > li)>a:hover,.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-tabs>li>a:focus,.navbar-light .navbar-nav .nav-pills>li>a:focus,.navbar.navbar-default .navbar-nav .nav-link:focus,.navbar.navbar-default .navbar-nav .nav-tabs>li>a:focus,.navbar.navbar-default .navbar-nav .nav-pills>li>a:focus,.navbar-light .navbar-nav :where(ul.nav.navbar-nav > li)>a:focus,.navbar.navbar-default .navbar-nav :where(ul.nav.navbar-nav > li)>a:focus{color:rgba(0,0,0,0.75)}.navbar-light .navbar-nav .nav-link.disabled,.navbar-light .navbar-nav .nav-tabs>li>a.disabled,.navbar-light .navbar-nav .nav-pills>li>a.disabled,.navbar.navbar-default .navbar-nav .nav-link.disabled,.navbar.navbar-default .navbar-nav .nav-tabs>li>a.disabled,.navbar.navbar-default .navbar-nav .nav-pills>li>a.disabled,.navbar-light .navbar-nav :where(ul.nav.navbar-nav > li)>a.disabled,.navbar.navbar-default .navbar-nav :where(ul.nav.navbar-nav > li)>a.disabled{color:rgba(0,0,0,0.25)}.navbar-light .navbar-nav .show>.nav-link,.navbar-light .navbar-nav .in>.nav-link,.navbar-light .navbar-nav .nav-tabs>li.show>a,.navbar-light .navbar-nav .nav-tabs>li.in>a,.navbar-light .navbar-nav .nav-pills>li.show>a,.navbar-light .navbar-nav .nav-pills>li.in>a,.navbar.navbar-default .navbar-nav .show>.nav-link,.navbar.navbar-default .navbar-nav .in>.nav-link,.navbar.navbar-default .navbar-nav .nav-tabs>li.show>a,.navbar.navbar-default .navbar-nav .nav-tabs>li.in>a,.navbar.navbar-default .navbar-nav .nav-pills>li.show>a,.navbar.navbar-default .navbar-nav .nav-pills>li.in>a,.navbar-light .navbar-nav .show:where(ul.nav.navbar-nav > li)>a,.navbar-light .navbar-nav .in:where(ul.nav.navbar-nav > li)>a,.navbar.navbar-default .navbar-nav .show:where(ul.nav.navbar-nav > li)>a,.navbar.navbar-default .navbar-nav .in:where(ul.nav.navbar-nav > li)>a,.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-tabs>li.active>a,.navbar-light .navbar-nav .nav-pills>li.active>a,.navbar.navbar-default .navbar-nav .active>.nav-link,.navbar.navbar-default .navbar-nav .nav-tabs>li.active>a,.navbar.navbar-default .navbar-nav .nav-pills>li.active>a,.navbar-light .navbar-nav .active:where(ul.nav.navbar-nav > li)>a,.navbar.navbar-default .navbar-nav .active:where(ul.nav.navbar-nav > li)>a,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .nav-link.in,.navbar-light .navbar-nav .nav-tabs>li>a.show,.navbar-light .navbar-nav .nav-tabs>li>a.in,.navbar-light .navbar-nav .nav-pills>li>a.show,.navbar-light .navbar-nav .nav-pills>li>a.in,.navbar.navbar-default .navbar-nav .nav-link.show,.navbar.navbar-default .navbar-nav .nav-link.in,.navbar.navbar-default .navbar-nav .nav-tabs>li>a.show,.navbar.navbar-default .navbar-nav .nav-tabs>li>a.in,.navbar.navbar-default .navbar-nav .nav-pills>li>a.show,.navbar.navbar-default .navbar-nav .nav-pills>li>a.in,.navbar-light .navbar-nav :where(ul.nav.navbar-nav > li)>a.show,.navbar-light .navbar-nav :where(ul.nav.navbar-nav > li)>a.in,.navbar.navbar-default .navbar-nav :where(ul.nav.navbar-nav > li)>a.show,.navbar.navbar-default .navbar-nav :where(ul.nav.navbar-nav > li)>a.in,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-tabs>li>a.active,.navbar-light .navbar-nav .nav-pills>li>a.active,.navbar.navbar-default .navbar-nav .nav-link.active,.navbar.navbar-default .navbar-nav .nav-tabs>li>a.active,.navbar.navbar-default .navbar-nav .nav-pills>li>a.active,.navbar-light .navbar-nav :where(ul.nav.navbar-nav > li)>a.active,.navbar.navbar-default .navbar-nav :where(ul.nav.navbar-nav > li)>a.active{color:#000}.navbar-light .navbar-toggler,.navbar-light .navbar-toggle,.navbar.navbar-default .navbar-toggler,.navbar.navbar-default .navbar-toggle{color:rgba(0,0,0,0.5);border-color:rgba(0,0,0,0.1)}.navbar-light .navbar-toggler-icon,.navbar-light .navbar-toggle>.icon-bar:last-child,.navbar.navbar-default .navbar-toggler-icon,.navbar.navbar-default .navbar-toggle>.icon-bar:last-child{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280,0,0,0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text,.navbar.navbar-default .navbar-text{color:rgba(0,0,0,0.5)}.navbar-light .navbar-text a,.navbar.navbar-default .navbar-text a{color:#000}.navbar-light .navbar-text a:hover,.navbar.navbar-default .navbar-text a:hover,.navbar-light .navbar-text a:focus,.navbar.navbar-default .navbar-text a:focus{color:#000}.navbar-dark,.navbar.navbar-inverse{background-color:#343a40}.navbar-dark .navbar-brand,.navbar.navbar-inverse .navbar-brand{color:#fff}.navbar-dark .navbar-brand:hover,.navbar.navbar-inverse .navbar-brand:hover,.navbar-dark .navbar-brand:focus,.navbar.navbar-inverse .navbar-brand:focus{color:#fff}.navbar-dark .navbar-nav .nav-link,.navbar-dark .navbar-nav .nav-tabs>li>a,.navbar-dark .navbar-nav .nav-pills>li>a,.navbar.navbar-inverse .navbar-nav .nav-link,.navbar.navbar-inverse .navbar-nav .nav-tabs>li>a,.navbar.navbar-inverse .navbar-nav .nav-pills>li>a,.navbar-dark .navbar-nav :where(ul.nav.navbar-nav > li)>a,.navbar.navbar-inverse .navbar-nav :where(ul.nav.navbar-nav > li)>a{color:rgba(255,255,255,0.5)}.navbar-dark .navbar-nav .nav-link:hover,.navbar-dark .navbar-nav .nav-tabs>li>a:hover,.navbar-dark .navbar-nav .nav-pills>li>a:hover,.navbar.navbar-inverse .navbar-nav .nav-link:hover,.navbar.navbar-inverse .navbar-nav .nav-tabs>li>a:hover,.navbar.navbar-inverse .navbar-nav .nav-pills>li>a:hover,.navbar-dark .navbar-nav :where(ul.nav.navbar-nav > li)>a:hover,.navbar.navbar-inverse .navbar-nav :where(ul.nav.navbar-nav > li)>a:hover,.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-tabs>li>a:focus,.navbar-dark .navbar-nav .nav-pills>li>a:focus,.navbar.navbar-inverse .navbar-nav .nav-link:focus,.navbar.navbar-inverse .navbar-nav .nav-tabs>li>a:focus,.navbar.navbar-inverse .navbar-nav .nav-pills>li>a:focus,.navbar-dark .navbar-nav :where(ul.nav.navbar-nav > li)>a:focus,.navbar.navbar-inverse .navbar-nav :where(ul.nav.navbar-nav > li)>a:focus{color:rgba(255,255,255,0.75)}.navbar-dark .navbar-nav .nav-link.disabled,.navbar-dark .navbar-nav .nav-tabs>li>a.disabled,.navbar-dark .navbar-nav .nav-pills>li>a.disabled,.navbar.navbar-inverse .navbar-nav .nav-link.disabled,.navbar.navbar-inverse .navbar-nav .nav-tabs>li>a.disabled,.navbar.navbar-inverse .navbar-nav .nav-pills>li>a.disabled,.navbar-dark .navbar-nav :where(ul.nav.navbar-nav > li)>a.disabled,.navbar.navbar-inverse .navbar-nav :where(ul.nav.navbar-nav > li)>a.disabled{color:rgba(255,255,255,0.25)}.navbar-dark .navbar-nav .show>.nav-link,.navbar-dark .navbar-nav .in>.nav-link,.navbar-dark .navbar-nav .nav-tabs>li.show>a,.navbar-dark .navbar-nav .nav-tabs>li.in>a,.navbar-dark .navbar-nav .nav-pills>li.show>a,.navbar-dark .navbar-nav .nav-pills>li.in>a,.navbar.navbar-inverse .navbar-nav .show>.nav-link,.navbar.navbar-inverse .navbar-nav .in>.nav-link,.navbar.navbar-inverse .navbar-nav .nav-tabs>li.show>a,.navbar.navbar-inverse .navbar-nav .nav-tabs>li.in>a,.navbar.navbar-inverse .navbar-nav .nav-pills>li.show>a,.navbar.navbar-inverse .navbar-nav .nav-pills>li.in>a,.navbar-dark .navbar-nav .show:where(ul.nav.navbar-nav > li)>a,.navbar-dark .navbar-nav .in:where(ul.nav.navbar-nav > li)>a,.navbar.navbar-inverse .navbar-nav .show:where(ul.nav.navbar-nav > li)>a,.navbar.navbar-inverse .navbar-nav .in:where(ul.nav.navbar-nav > li)>a,.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-tabs>li.active>a,.navbar-dark .navbar-nav .nav-pills>li.active>a,.navbar.navbar-inverse .navbar-nav .active>.nav-link,.navbar.navbar-inverse .navbar-nav .nav-tabs>li.active>a,.navbar.navbar-inverse .navbar-nav .nav-pills>li.active>a,.navbar-dark .navbar-nav .active:where(ul.nav.navbar-nav > li)>a,.navbar.navbar-inverse .navbar-nav .active:where(ul.nav.navbar-nav > li)>a,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .nav-link.in,.navbar-dark .navbar-nav .nav-tabs>li>a.show,.navbar-dark .navbar-nav .nav-tabs>li>a.in,.navbar-dark .navbar-nav .nav-pills>li>a.show,.navbar-dark .navbar-nav .nav-pills>li>a.in,.navbar.navbar-inverse .navbar-nav .nav-link.show,.navbar.navbar-inverse .navbar-nav .nav-link.in,.navbar.navbar-inverse .navbar-nav .nav-tabs>li>a.show,.navbar.navbar-inverse .navbar-nav .nav-tabs>li>a.in,.navbar.navbar-inverse .navbar-nav .nav-pills>li>a.show,.navbar.navbar-inverse .navbar-nav .nav-pills>li>a.in,.navbar-dark .navbar-nav :where(ul.nav.navbar-nav > li)>a.show,.navbar-dark .navbar-nav :where(ul.nav.navbar-nav > li)>a.in,.navbar.navbar-inverse .navbar-nav :where(ul.nav.navbar-nav > li)>a.show,.navbar.navbar-inverse .navbar-nav :where(ul.nav.navbar-nav > li)>a.in,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-tabs>li>a.active,.navbar-dark .navbar-nav .nav-pills>li>a.active,.navbar.navbar-inverse .navbar-nav .nav-link.active,.navbar.navbar-inverse .navbar-nav .nav-tabs>li>a.active,.navbar.navbar-inverse .navbar-nav .nav-pills>li>a.active,.navbar-dark .navbar-nav :where(ul.nav.navbar-nav > li)>a.active,.navbar.navbar-inverse .navbar-nav :where(ul.nav.navbar-nav > li)>a.active{color:#fff}.navbar-dark .navbar-toggler,.navbar-dark .navbar-toggle,.navbar.navbar-inverse .navbar-toggler,.navbar.navbar-inverse .navbar-toggle{color:rgba(255,255,255,0.5);border-color:rgba(255,255,255,0.1)}.navbar-dark .navbar-toggler-icon,.navbar-dark .navbar-toggle>.icon-bar:last-child,.navbar.navbar-inverse .navbar-toggler-icon,.navbar.navbar-inverse .navbar-toggle>.icon-bar:last-child{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255,255,255,0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text,.navbar.navbar-inverse .navbar-text{color:rgba(255,255,255,0.5)}.navbar-dark .navbar-text a,.navbar.navbar-inverse .navbar-text a{color:#fff}.navbar-dark .navbar-text a:hover,.navbar.navbar-inverse .navbar-text a:hover,.navbar-dark .navbar-text a:focus,.navbar.navbar-inverse .navbar-text a:focus{color:#fff}.card,.well{position:relative;display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,0.125);border-radius:.25rem}.card>hr,.well>hr{margin-right:0;margin-left:0}.card>.list-group,.well>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child,.well>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child,.well>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card>.card-header+.list-group,.well>.card-header+.list-group,.card>.list-group+.card-footer,.well>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;-webkit-flex:1 1 auto;min-height:1px;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:rgba(0,0,0,0.03);border-bottom:1px solid rgba(0,0,0,0.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(0,0,0,0.03);border-top:1px solid rgba(0,0,0,0.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem;border-radius:calc(.25rem - 1px)}.card-img,.card-img-top,.card-img-bottom{flex-shrink:0;-webkit-flex-shrink:0;width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-deck .card,.card-deck .well{margin-bottom:15px}@media (min-width: 576px){.card-deck{display:flex;display:-webkit-flex;flex-flow:row wrap;-webkit-flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card,.card-deck .well{flex:1 0 0%;-webkit-flex:1 0 0%;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group>.card,.card-group>.well{margin-bottom:15px}@media (min-width: 576px){.card-group{display:flex;display:-webkit-flex;flex-flow:row wrap;-webkit-flex-flow:row wrap}.card-group>.card,.card-group>.well{flex:1 0 0%;-webkit-flex:1 0 0%;margin-bottom:0}.card-group>.card+.card,.card-group>.well+.card,.card-group>.card+.well,.card-group>.well+.well{margin-left:0;border-left:0}.card-group>.card:not(:last-child),.card-group>.well:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-img-top,.card-group>.well:not(:last-child) .card-img-top,.card-group>.card:not(:last-child) .card-header,.card-group>.well:not(:last-child) .card-header{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-img-bottom,.card-group>.well:not(:last-child) .card-img-bottom,.card-group>.card:not(:last-child) .card-footer,.card-group>.well:not(:last-child) .card-footer{border-bottom-right-radius:0}.card-group>.card:not(:first-child),.card-group>.well:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-img-top,.card-group>.well:not(:first-child) .card-img-top,.card-group>.card:not(:first-child) .card-header,.card-group>.well:not(:first-child) .card-header{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-img-bottom,.card-group>.well:not(:first-child) .card-img-bottom,.card-group>.card:not(:first-child) .card-footer,.card-group>.well:not(:first-child) .card-footer{border-bottom-left-radius:0}}.card-columns .card,.card-columns .well{margin-bottom:.75rem}@media (min-width: 576px){.card-columns{column-count:3;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card,.card-columns .well{display:inline-block;width:100%}}.accordion{overflow-anchor:none}.accordion>.card,.accordion>.well{overflow:hidden}.accordion>.card:not(:last-of-type),.accordion>.well:not(:last-of-type){border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion>.card:not(:first-of-type),.accordion>.well:not(:first-of-type){border-top-left-radius:0;border-top-right-radius:0}.accordion>.card>.card-header,.accordion>.well>.card-header{border-radius:0;margin-bottom:-1px}.breadcrumb{display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:.25rem}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:.5rem;color:#6c757d;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:flex;display:-webkit-flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#1e90ff;background-color:#fff;border:1px solid #dee2e6}.page-link:hover{z-index:2;color:#006ad1;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;outline:0;box-shadow:0 0 0 .2rem rgba(30,144,255,0.25)}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:3;color:#fff;background-color:#1e90ff;border-color:#1e90ff}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dee2e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem;transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.badge{transition:none}}a.badge:hover,a.badge:focus{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-default{color:#000;background-color:#dee2e6}a.badge-default:hover,a.badge-default:focus{color:#000;background-color:#c1c9d0}a.badge-default:focus,a.badge-default.focus{outline:0;box-shadow:0 0 0 .2rem rgba(222,226,230,0.5)}.badge-primary{color:#fff;background-color:#1e90ff}a.badge-primary:hover,a.badge-primary:focus{color:#fff;background-color:#0077ea}a.badge-primary:focus,a.badge-primary.focus{outline:0;box-shadow:0 0 0 .2rem rgba(30,144,255,0.5)}.badge-secondary{color:#fff;background-color:#6c757d}a.badge-secondary:hover,a.badge-secondary:focus{color:#fff;background-color:#545b62}a.badge-secondary:focus,a.badge-secondary.focus{outline:0;box-shadow:0 0 0 .2rem rgba(108,117,125,0.5)}.badge-success{color:#fff;background-color:#28a745}a.badge-success:hover,a.badge-success:focus{color:#fff;background-color:#1e7e34}a.badge-success:focus,a.badge-success.focus{outline:0;box-shadow:0 0 0 .2rem rgba(40,167,69,0.5)}.badge-info{color:#000;background-color:#0DCAF0}a.badge-info:hover,a.badge-info:focus{color:#000;background-color:#0aa1c0}a.badge-info:focus,a.badge-info.focus{outline:0;box-shadow:0 0 0 .2rem rgba(13,202,240,0.5)}.badge-warning{color:#000;background-color:#ffc107}a.badge-warning:hover,a.badge-warning:focus{color:#000;background-color:#d39e00}a.badge-warning:focus,a.badge-warning.focus{outline:0;box-shadow:0 0 0 .2rem rgba(255,193,7,0.5)}.badge-danger{color:#fff;background-color:#dc3545}a.badge-danger:hover,a.badge-danger:focus{color:#fff;background-color:#bd2130}a.badge-danger:focus,a.badge-danger.focus{outline:0;box-shadow:0 0 0 .2rem rgba(220,53,69,0.5)}.badge-light{color:#000;background-color:#f8f9fa}a.badge-light:hover,a.badge-light:focus{color:#000;background-color:#dae0e5}a.badge-light:focus,a.badge-light.focus{outline:0;box-shadow:0 0 0 .2rem rgba(248,249,250,0.5)}.badge-dark{color:#fff;background-color:#343a40}a.badge-dark:hover,a.badge-dark:focus{color:#fff;background-color:#1d2124}a.badge-dark:focus,a.badge-dark.focus{outline:0;box-shadow:0 0 0 .2rem rgba(52,58,64,0.5)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.3rem}@media (min-width: 576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;z-index:2;padding:.75rem 1.25rem;color:inherit}.alert-default{color:#737678;background-color:#f8f9fa;border-color:#f6f7f8}.alert-default hr{border-top-color:#e8eaed}.alert-default .alert-link{color:#5a5c5e}.alert-primary{color:#104b85;background-color:#d2e9ff;border-color:#c0e0ff}.alert-primary hr{border-top-color:#a7d3ff}.alert-primary .alert-link{color:#0b3157}.alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-secondary hr{border-top-color:#c8cbcf}.alert-secondary .alert-link{color:#202326}.alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-success hr{border-top-color:#b1dfbb}.alert-success .alert-link{color:#0b2e13}.alert-info{color:#07697d;background-color:#cff4fc;border-color:#bbf0fb}.alert-info hr{border-top-color:#a3ebfa}.alert-info .alert-link{color:#04404d}.alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#533f03}.alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-danger hr{border-top-color:#f1b0b7}.alert-danger .alert-link{color:#491217}.alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#686868}.alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.alert-dark .alert-link{color:#040505}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:flex;display:-webkit-flex;height:1rem;overflow:hidden;line-height:0;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;justify-content:center;-webkit-justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#1e90ff;transition:width 0.6s ease}@media (prefers-reduced-motion: reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-size:1rem 1rem}.progress-bar-animated{animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion: reduce){.progress-bar-animated{animation:none}}.media{display:flex;display:-webkit-flex;align-items:flex-start;-webkit-align-items:flex-start}.media-body{flex:1;-webkit-flex:1}.list-group{display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:hover,.list-group-item-action:focus{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;background-color:#fff;border:1px solid rgba(0,0,0,0.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#1e90ff;border-color:#1e90ff}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width: 576px){.list-group-horizontal-sm{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width: 768px){.list-group-horizontal-md{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width: 992px){.list-group-horizontal-lg{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width: 1200px){.list-group-horizontal-xl{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-default{color:#737678;background-color:#f6f7f8}.list-group-item-default.list-group-item-action:hover,.list-group-item-default.list-group-item-action:focus{color:#737678;background-color:#e8eaed}.list-group-item-default.list-group-item-action.active{color:#fff;background-color:#737678;border-color:#737678}.list-group-item-primary{color:#104b85;background-color:#c0e0ff}.list-group-item-primary.list-group-item-action:hover,.list-group-item-primary.list-group-item-action:focus{color:#104b85;background-color:#a7d3ff}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#104b85;border-color:#104b85}.list-group-item-secondary{color:#383d41;background-color:#d6d8db}.list-group-item-secondary.list-group-item-action:hover,.list-group-item-secondary.list-group-item-action:focus{color:#383d41;background-color:#c8cbcf}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#383d41;border-color:#383d41}.list-group-item-success{color:#155724;background-color:#c3e6cb}.list-group-item-success.list-group-item-action:hover,.list-group-item-success.list-group-item-action:focus{color:#155724;background-color:#b1dfbb}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#155724;border-color:#155724}.list-group-item-info{color:#07697d;background-color:#bbf0fb}.list-group-item-info.list-group-item-action:hover,.list-group-item-info.list-group-item-action:focus{color:#07697d;background-color:#a3ebfa}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#07697d;border-color:#07697d}.list-group-item-warning{color:#856404;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:hover,.list-group-item-warning.list-group-item-action:focus{color:#856404;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#856404;border-color:#856404}.list-group-item-danger{color:#721c24;background-color:#f5c6cb}.list-group-item-danger.list-group-item-action:hover,.list-group-item-danger.list-group-item-action:focus{color:#721c24;background-color:#f1b0b7}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#721c24;border-color:#721c24}.list-group-item-light{color:#818182;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:hover,.list-group-item-light.list-group-item-action:focus{color:#818182;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#818182;border-color:#818182}.list-group-item-dark{color:#1b1e21;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:hover,.list-group-item-dark.list-group-item-action:focus{color:#1b1e21;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1b1e21;border-color:#1b1e21}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:hover{color:#000;text-decoration:none}.close:not(:disabled):not(.disabled):hover,.close:not(:disabled):not(.disabled):focus{opacity:.75}button.close{padding:0;background-color:transparent;border:0}a.close.disabled{pointer-events:none}.toast{flex-basis:350px;-webkit-flex-basis:350px;max-width:350px;font-size:.875rem;background-color:rgba(255,255,255,0.85);background-clip:padding-box;border:1px solid rgba(0,0,0,0.1);box-shadow:0 0.25rem 0.75rem rgba(0,0,0,0.1);opacity:0;border-radius:.25rem}.toast:not(:last-child){margin-bottom:.75rem}.toast.showing{opacity:1}.toast.show,.toast.in{display:block;opacity:1}.toast.hide{display:none}.toast-header{display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;padding:.25rem .75rem;color:#6c757d;background-color:rgba(255,255,255,0.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,0.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.toast-body{padding:.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform 0.3s ease-out;transform:translate(0, -50px)}@media (prefers-reduced-motion: reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog,.modal.in .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{display:flex;display:-webkit-flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-header,.modal-dialog-scrollable .modal-footer{flex-shrink:0;-webkit-flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;min-height:calc(100% - 1rem)}.modal-dialog-centered::before{display:block;height:calc(100vh - 1rem);height:min-content;height:-webkit-min-content;height:-moz-min-content;height:-ms-min-content;height:-o-min-content;content:""}.modal-dialog-centered.modal-dialog-scrollable{flex-direction:column;-webkit-flex-direction:column;justify-content:center;-webkit-justify-content:center;height:100%}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable::before{content:none}.modal-content{position:relative;display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,0.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show,.modal-backdrop.in{opacity:.5}.modal-header{display:flex;display:-webkit-flex;align-items:flex-start;-webkit-align-items:flex-start;justify-content:space-between;-webkit-justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .close{padding:1rem 1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;-webkit-flex:1 1 auto;padding:1rem}.modal-footer{display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;align-items:center;-webkit-align-items:center;justify-content:flex-end;-webkit-justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width: 576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered::before{height:calc(100vh - 3.5rem);height:min-content;height:-webkit-min-content;height:-moz-min-content;height:-ms-min-content;height:-o-min-content}.modal-sm{max-width:300px}}@media (min-width: 992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width: 1200px){.modal-xl{max-width:1140px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:Lato;font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show,.tooltip.in{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-top,.bs-tooltip-auto[x-placement^="top"]{padding:.4rem 0}.bs-tooltip-top .arrow,.bs-tooltip-auto[x-placement^="top"] .arrow{bottom:0}.bs-tooltip-top .arrow::before,.bs-tooltip-auto[x-placement^="top"] .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-right,.bs-tooltip-auto[x-placement^="right"]{padding:0 .4rem}.bs-tooltip-right .arrow,.bs-tooltip-auto[x-placement^="right"] .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-right .arrow::before,.bs-tooltip-auto[x-placement^="right"] .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-bottom,.bs-tooltip-auto[x-placement^="bottom"]{padding:.4rem 0}.bs-tooltip-bottom .arrow,.bs-tooltip-auto[x-placement^="bottom"] .arrow{top:0}.bs-tooltip-bottom .arrow::before,.bs-tooltip-auto[x-placement^="bottom"] .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-left,.bs-tooltip-auto[x-placement^="left"]{padding:0 .4rem}.bs-tooltip-left .arrow,.bs-tooltip-auto[x-placement^="left"] .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-left .arrow::before,.bs-tooltip-auto[x-placement^="left"] .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:Lato;font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,0.2);border-radius:.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::before,.popover .arrow::after{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-top,.bs-popover-auto[x-placement^="top"]{margin-bottom:.5rem}.bs-popover-top>.arrow,.bs-popover-auto[x-placement^="top"]>.arrow{bottom:calc(-.5rem - 1px)}.bs-popover-top>.arrow::before,.bs-popover-auto[x-placement^="top"]>.arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,0.25)}.bs-popover-top>.arrow::after,.bs-popover-auto[x-placement^="top"]>.arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-right,.bs-popover-auto[x-placement^="right"]{margin-left:.5rem}.bs-popover-right>.arrow,.bs-popover-auto[x-placement^="right"]>.arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-right>.arrow::before,.bs-popover-auto[x-placement^="right"]>.arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,0.25)}.bs-popover-right>.arrow::after,.bs-popover-auto[x-placement^="right"]>.arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-bottom,.bs-popover-auto[x-placement^="bottom"]{margin-top:.5rem}.bs-popover-bottom>.arrow,.bs-popover-auto[x-placement^="bottom"]>.arrow{top:calc(-.5rem - 1px)}.bs-popover-bottom>.arrow::before,.bs-popover-auto[x-placement^="bottom"]>.arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,0.25)}.bs-popover-bottom>.arrow::after,.bs-popover-auto[x-placement^="bottom"]>.arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-bottom .popover-header::before,.bs-popover-auto[x-placement^="bottom"] .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-left,.bs-popover-auto[x-placement^="left"]{margin-right:.5rem}.bs-popover-left>.arrow,.bs-popover-auto[x-placement^="left"]>.arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-left>.arrow::before,.bs-popover-auto[x-placement^="left"]>.arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,0.25)}.bs-popover-left>.arrow::after,.bs-popover-auto[x-placement^="left"]>.arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y;-webkit-touch-action:pan-y;-moz-touch-action:pan-y;-ms-touch-action:pan-y;-o-touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;backface-visibility:hidden;-webkit-backface-visibility:hidden;-moz-backface-visibility:hidden;-ms-backface-visibility:hidden;-o-backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion: reduce){.carousel-item{transition:none}}.carousel-item.active,.carousel-item-next,.carousel-item-prev{display:block}.carousel-item-next:not(.carousel-item-left),.active.carousel-item-right{transform:translateX(100%)}.carousel-item-prev:not(.carousel-item-right),.active.carousel-item-left{transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item.active,.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right{z-index:1;opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion: reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{transition:none}}.carousel-control-prev,.carousel-control-next{position:absolute;top:0;bottom:0;z-index:1;display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;justify-content:center;-webkit-justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5;transition:opacity 0.15s ease}@media (prefers-reduced-motion: reduce){.carousel-control-prev,.carousel-control-next{transition:none}}.carousel-control-prev:hover,.carousel-control-prev:focus,.carousel-control-next:hover,.carousel-control-next:focus{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-prev-icon,.carousel-control-next-icon{display:inline-block;width:20px;height:20px;background:50% / 100% 100% no-repeat}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:15;display:flex;display:-webkit-flex;justify-content:center;-webkit-justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{box-sizing:content-box;flex:0 1 auto;-webkit-flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity 0.6s ease}@media (prefers-reduced-motion: reduce){.carousel-indicators li{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion: reduce){.spinner-border,.spinner-grow{animation-duration:1.5s;-webkit-animation-duration:1.5s;-moz-animation-duration:1.5s;-ms-animation-duration:1.5s;-o-animation-duration:1.5s}}.align-baseline{vertical-align:baseline !important}.align-top{vertical-align:top !important}.align-middle{vertical-align:middle !important}.align-bottom{vertical-align:bottom !important}.align-text-bottom{vertical-align:text-bottom !important}.align-text-top{vertical-align:text-top !important}.bg-default{background-color:#dee2e6 !important;color:#000}a.bg-default:hover,a.bg-default:focus,button.bg-default:hover,button.bg-default:focus{background-color:#c1c9d0 !important}.bg-primary{background-color:#1e90ff !important;color:#fff}a.bg-primary:hover,a.bg-primary:focus,button.bg-primary:hover,button.bg-primary:focus{background-color:#0077ea !important}.bg-secondary{background-color:#6c757d !important;color:#fff}a.bg-secondary:hover,a.bg-secondary:focus,button.bg-secondary:hover,button.bg-secondary:focus{background-color:#545b62 !important}.bg-success{background-color:#28a745 !important;color:#fff}a.bg-success:hover,a.bg-success:focus,button.bg-success:hover,button.bg-success:focus{background-color:#1e7e34 !important}.bg-info{background-color:#0DCAF0 !important;color:#000}a.bg-info:hover,a.bg-info:focus,button.bg-info:hover,button.bg-info:focus{background-color:#0aa1c0 !important}.bg-warning{background-color:#ffc107 !important;color:#000}a.bg-warning:hover,a.bg-warning:focus,button.bg-warning:hover,button.bg-warning:focus{background-color:#d39e00 !important}.bg-danger{background-color:#dc3545 !important;color:#fff}a.bg-danger:hover,a.bg-danger:focus,button.bg-danger:hover,button.bg-danger:focus{background-color:#bd2130 !important}.bg-light{background-color:#f8f9fa !important;color:#000}a.bg-light:hover,a.bg-light:focus,button.bg-light:hover,button.bg-light:focus{background-color:#dae0e5 !important}.bg-dark{background-color:#343a40 !important;color:#fff}a.bg-dark:hover,a.bg-dark:focus,button.bg-dark:hover,button.bg-dark:focus{background-color:#1d2124 !important}.bg-white{background-color:#fff !important;color:#000}.bg-transparent{background-color:transparent !important}.border{border:1px solid #dee2e6 !important}.border-top{border-top:1px solid #dee2e6 !important}.border-right{border-right:1px solid #dee2e6 !important}.border-bottom{border-bottom:1px solid #dee2e6 !important}.border-left{border-left:1px solid #dee2e6 !important}.border-0{border:0 !important}.border-top-0{border-top:0 !important}.border-right-0{border-right:0 !important}.border-bottom-0{border-bottom:0 !important}.border-left-0{border-left:0 !important}.border-default{border-color:#dee2e6 !important}.border-primary{border-color:#1e90ff !important}.border-secondary{border-color:#6c757d !important}.border-success{border-color:#28a745 !important}.border-info{border-color:#0DCAF0 !important}.border-warning{border-color:#ffc107 !important}.border-danger{border-color:#dc3545 !important}.border-light{border-color:#f8f9fa !important}.border-dark{border-color:#343a40 !important}.border-white{border-color:#fff !important}.rounded-sm{border-radius:.2rem !important}.rounded{border-radius:.25rem !important}.rounded-top{border-top-left-radius:.25rem !important;border-top-right-radius:.25rem !important}.rounded-right{border-top-right-radius:.25rem !important;border-bottom-right-radius:.25rem !important}.rounded-bottom{border-bottom-right-radius:.25rem !important;border-bottom-left-radius:.25rem !important}.rounded-left{border-top-left-radius:.25rem !important;border-bottom-left-radius:.25rem !important}.rounded-lg{border-radius:.3rem !important}.rounded-circle{border-radius:50% !important}.rounded-pill{border-radius:50rem !important}.rounded-0{border-radius:0 !important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none !important}.d-inline{display:inline !important}.d-inline-block{display:inline-block !important}.d-block{display:block !important}.d-table{display:table !important}.d-table-row{display:table-row !important}.d-table-cell{display:table-cell !important}.d-flex{display:flex !important}.d-inline-flex{display:inline-flex !important}@media (min-width: 576px){.d-sm-none{display:none !important}.d-sm-inline{display:inline !important}.d-sm-inline-block{display:inline-block !important}.d-sm-block{display:block !important}.d-sm-table{display:table !important}.d-sm-table-row{display:table-row !important}.d-sm-table-cell{display:table-cell !important}.d-sm-flex{display:flex !important}.d-sm-inline-flex{display:inline-flex !important}}@media (min-width: 768px){.d-md-none{display:none !important}.d-md-inline{display:inline !important}.d-md-inline-block{display:inline-block !important}.d-md-block{display:block !important}.d-md-table{display:table !important}.d-md-table-row{display:table-row !important}.d-md-table-cell{display:table-cell !important}.d-md-flex{display:flex !important}.d-md-inline-flex{display:inline-flex !important}}@media (min-width: 992px){.d-lg-none{display:none !important}.d-lg-inline{display:inline !important}.d-lg-inline-block{display:inline-block !important}.d-lg-block{display:block !important}.d-lg-table{display:table !important}.d-lg-table-row{display:table-row !important}.d-lg-table-cell{display:table-cell !important}.d-lg-flex{display:flex !important}.d-lg-inline-flex{display:inline-flex !important}}@media (min-width: 1200px){.d-xl-none{display:none !important}.d-xl-inline{display:inline !important}.d-xl-inline-block{display:inline-block !important}.d-xl-block{display:block !important}.d-xl-table{display:table !important}.d-xl-table-row{display:table-row !important}.d-xl-table-cell{display:table-cell !important}.d-xl-flex{display:flex !important}.d-xl-inline-flex{display:inline-flex !important}}@media print{.d-print-none{display:none !important}.d-print-inline{display:inline !important}.d-print-inline-block{display:inline-block !important}.d-print-block{display:block !important}.d-print-table{display:table !important}.d-print-table-row{display:table-row !important}.d-print-table-cell{display:table-cell !important}.d-print-flex{display:flex !important}.d-print-inline-flex{display:inline-flex !important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive iframe,.embed-responsive embed,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.85714%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{flex-direction:row !important}.flex-column{flex-direction:column !important}.flex-row-reverse{flex-direction:row-reverse !important}.flex-column-reverse{flex-direction:column-reverse !important}.flex-wrap{flex-wrap:wrap !important}.flex-nowrap{flex-wrap:nowrap !important}.flex-wrap-reverse{flex-wrap:wrap-reverse !important}.flex-fill{flex:1 1 auto !important}.flex-grow-0{flex-grow:0 !important}.flex-grow-1{flex-grow:1 !important}.flex-shrink-0{flex-shrink:0 !important}.flex-shrink-1{flex-shrink:1 !important}.justify-content-start{justify-content:flex-start !important}.justify-content-end{justify-content:flex-end !important}.justify-content-center{justify-content:center !important}.justify-content-between{justify-content:space-between !important}.justify-content-around{justify-content:space-around !important}.align-items-start{align-items:flex-start !important}.align-items-end{align-items:flex-end !important}.align-items-center{align-items:center !important}.align-items-baseline{align-items:baseline !important}.align-items-stretch{align-items:stretch !important}.align-content-start{align-content:flex-start !important}.align-content-end{align-content:flex-end !important}.align-content-center{align-content:center !important}.align-content-between{align-content:space-between !important}.align-content-around{align-content:space-around !important}.align-content-stretch{align-content:stretch !important}.align-self-auto{align-self:auto !important}.align-self-start{align-self:flex-start !important}.align-self-end{align-self:flex-end !important}.align-self-center{align-self:center !important}.align-self-baseline{align-self:baseline !important}.align-self-stretch{align-self:stretch !important}@media (min-width: 576px){.flex-sm-row{flex-direction:row !important}.flex-sm-column{flex-direction:column !important}.flex-sm-row-reverse{flex-direction:row-reverse !important}.flex-sm-column-reverse{flex-direction:column-reverse !important}.flex-sm-wrap{flex-wrap:wrap !important}.flex-sm-nowrap{flex-wrap:nowrap !important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse !important}.flex-sm-fill{flex:1 1 auto !important}.flex-sm-grow-0{flex-grow:0 !important}.flex-sm-grow-1{flex-grow:1 !important}.flex-sm-shrink-0{flex-shrink:0 !important}.flex-sm-shrink-1{flex-shrink:1 !important}.justify-content-sm-start{justify-content:flex-start !important}.justify-content-sm-end{justify-content:flex-end !important}.justify-content-sm-center{justify-content:center !important}.justify-content-sm-between{justify-content:space-between !important}.justify-content-sm-around{justify-content:space-around !important}.align-items-sm-start{align-items:flex-start !important}.align-items-sm-end{align-items:flex-end !important}.align-items-sm-center{align-items:center !important}.align-items-sm-baseline{align-items:baseline !important}.align-items-sm-stretch{align-items:stretch !important}.align-content-sm-start{align-content:flex-start !important}.align-content-sm-end{align-content:flex-end !important}.align-content-sm-center{align-content:center !important}.align-content-sm-between{align-content:space-between !important}.align-content-sm-around{align-content:space-around !important}.align-content-sm-stretch{align-content:stretch !important}.align-self-sm-auto{align-self:auto !important}.align-self-sm-start{align-self:flex-start !important}.align-self-sm-end{align-self:flex-end !important}.align-self-sm-center{align-self:center !important}.align-self-sm-baseline{align-self:baseline !important}.align-self-sm-stretch{align-self:stretch !important}}@media (min-width: 768px){.flex-md-row{flex-direction:row !important}.flex-md-column{flex-direction:column !important}.flex-md-row-reverse{flex-direction:row-reverse !important}.flex-md-column-reverse{flex-direction:column-reverse !important}.flex-md-wrap{flex-wrap:wrap !important}.flex-md-nowrap{flex-wrap:nowrap !important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse !important}.flex-md-fill{flex:1 1 auto !important}.flex-md-grow-0{flex-grow:0 !important}.flex-md-grow-1{flex-grow:1 !important}.flex-md-shrink-0{flex-shrink:0 !important}.flex-md-shrink-1{flex-shrink:1 !important}.justify-content-md-start{justify-content:flex-start !important}.justify-content-md-end{justify-content:flex-end !important}.justify-content-md-center{justify-content:center !important}.justify-content-md-between{justify-content:space-between !important}.justify-content-md-around{justify-content:space-around !important}.align-items-md-start{align-items:flex-start !important}.align-items-md-end{align-items:flex-end !important}.align-items-md-center{align-items:center !important}.align-items-md-baseline{align-items:baseline !important}.align-items-md-stretch{align-items:stretch !important}.align-content-md-start{align-content:flex-start !important}.align-content-md-end{align-content:flex-end !important}.align-content-md-center{align-content:center !important}.align-content-md-between{align-content:space-between !important}.align-content-md-around{align-content:space-around !important}.align-content-md-stretch{align-content:stretch !important}.align-self-md-auto{align-self:auto !important}.align-self-md-start{align-self:flex-start !important}.align-self-md-end{align-self:flex-end !important}.align-self-md-center{align-self:center !important}.align-self-md-baseline{align-self:baseline !important}.align-self-md-stretch{align-self:stretch !important}}@media (min-width: 992px){.flex-lg-row{flex-direction:row !important}.flex-lg-column{flex-direction:column !important}.flex-lg-row-reverse{flex-direction:row-reverse !important}.flex-lg-column-reverse{flex-direction:column-reverse !important}.flex-lg-wrap{flex-wrap:wrap !important}.flex-lg-nowrap{flex-wrap:nowrap !important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse !important}.flex-lg-fill{flex:1 1 auto !important}.flex-lg-grow-0{flex-grow:0 !important}.flex-lg-grow-1{flex-grow:1 !important}.flex-lg-shrink-0{flex-shrink:0 !important}.flex-lg-shrink-1{flex-shrink:1 !important}.justify-content-lg-start{justify-content:flex-start !important}.justify-content-lg-end{justify-content:flex-end !important}.justify-content-lg-center{justify-content:center !important}.justify-content-lg-between{justify-content:space-between !important}.justify-content-lg-around{justify-content:space-around !important}.align-items-lg-start{align-items:flex-start !important}.align-items-lg-end{align-items:flex-end !important}.align-items-lg-center{align-items:center !important}.align-items-lg-baseline{align-items:baseline !important}.align-items-lg-stretch{align-items:stretch !important}.align-content-lg-start{align-content:flex-start !important}.align-content-lg-end{align-content:flex-end !important}.align-content-lg-center{align-content:center !important}.align-content-lg-between{align-content:space-between !important}.align-content-lg-around{align-content:space-around !important}.align-content-lg-stretch{align-content:stretch !important}.align-self-lg-auto{align-self:auto !important}.align-self-lg-start{align-self:flex-start !important}.align-self-lg-end{align-self:flex-end !important}.align-self-lg-center{align-self:center !important}.align-self-lg-baseline{align-self:baseline !important}.align-self-lg-stretch{align-self:stretch !important}}@media (min-width: 1200px){.flex-xl-row{flex-direction:row !important}.flex-xl-column{flex-direction:column !important}.flex-xl-row-reverse{flex-direction:row-reverse !important}.flex-xl-column-reverse{flex-direction:column-reverse !important}.flex-xl-wrap{flex-wrap:wrap !important}.flex-xl-nowrap{flex-wrap:nowrap !important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse !important}.flex-xl-fill{flex:1 1 auto !important}.flex-xl-grow-0{flex-grow:0 !important}.flex-xl-grow-1{flex-grow:1 !important}.flex-xl-shrink-0{flex-shrink:0 !important}.flex-xl-shrink-1{flex-shrink:1 !important}.justify-content-xl-start{justify-content:flex-start !important}.justify-content-xl-end{justify-content:flex-end !important}.justify-content-xl-center{justify-content:center !important}.justify-content-xl-between{justify-content:space-between !important}.justify-content-xl-around{justify-content:space-around !important}.align-items-xl-start{align-items:flex-start !important}.align-items-xl-end{align-items:flex-end !important}.align-items-xl-center{align-items:center !important}.align-items-xl-baseline{align-items:baseline !important}.align-items-xl-stretch{align-items:stretch !important}.align-content-xl-start{align-content:flex-start !important}.align-content-xl-end{align-content:flex-end !important}.align-content-xl-center{align-content:center !important}.align-content-xl-between{align-content:space-between !important}.align-content-xl-around{align-content:space-around !important}.align-content-xl-stretch{align-content:stretch !important}.align-self-xl-auto{align-self:auto !important}.align-self-xl-start{align-self:flex-start !important}.align-self-xl-end{align-self:flex-end !important}.align-self-xl-center{align-self:center !important}.align-self-xl-baseline{align-self:baseline !important}.align-self-xl-stretch{align-self:stretch !important}}.float-left{float:left !important}.float-right{float:right !important}.float-none{float:none !important}@media (min-width: 576px){.float-sm-left{float:left !important}.float-sm-right{float:right !important}.float-sm-none{float:none !important}}@media (min-width: 768px){.float-md-left{float:left !important}.float-md-right{float:right !important}.float-md-none{float:none !important}}@media (min-width: 992px){.float-lg-left{float:left !important}.float-lg-right{float:right !important}.float-lg-none{float:none !important}}@media (min-width: 1200px){.float-xl-left{float:left !important}.float-xl-right{float:right !important}.float-xl-none{float:none !important}}.user-select-all{user-select:all !important}.user-select-auto{user-select:auto !important}.user-select-none{user-select:none !important}.overflow-auto{overflow:auto !important}.overflow-hidden{overflow:hidden !important}.position-static{position:static !important}.position-relative{position:relative !important}.position-absolute{position:absolute !important}.position-fixed{position:fixed !important}.position-sticky{position:sticky !important}.fixed-top,.navbar-fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom,.navbar-fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports (position: sticky){.sticky-top,.navbar-sticky-top{position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{box-shadow:0 0.125rem 0.25rem rgba(0,0,0,0.075) !important}.shadow{box-shadow:0 0.5rem 1rem rgba(0,0,0,0.15) !important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,0.175) !important}.shadow-none{box-shadow:none !important}.w-25{width:25% !important}.w-50{width:50% !important}.w-75{width:75% !important}.w-100{width:100% !important}.w-auto{width:auto !important}.h-25{height:25% !important}.h-50{height:50% !important}.h-75{height:75% !important}.h-100{height:100% !important}.h-auto{height:auto !important}.mw-100{max-width:100% !important}.mh-100{max-height:100% !important}.min-vw-100{min-width:100vw !important}.min-vh-100{min-height:100vh !important}.vw-100{width:100vw !important}.vh-100{height:100vh !important}.m-0{margin:0 !important}.mt-0,.my-0{margin-top:0 !important}.mr-0,.mx-0{margin-right:0 !important}.mb-0,.my-0{margin-bottom:0 !important}.ml-0,.mx-0{margin-left:0 !important}.m-1{margin:.25rem !important}.mt-1,.my-1{margin-top:.25rem !important}.mr-1,.mx-1{margin-right:.25rem !important}.mb-1,.my-1{margin-bottom:.25rem !important}.ml-1,.mx-1{margin-left:.25rem !important}.m-2{margin:.5rem !important}.mt-2,.my-2{margin-top:.5rem !important}.mr-2,.mx-2{margin-right:.5rem !important}.mb-2,.my-2{margin-bottom:.5rem !important}.ml-2,.mx-2{margin-left:.5rem !important}.m-3{margin:1rem !important}.mt-3,.my-3{margin-top:1rem !important}.mr-3,.mx-3{margin-right:1rem !important}.mb-3,.my-3{margin-bottom:1rem !important}.ml-3,.mx-3{margin-left:1rem !important}.m-4{margin:1.5rem !important}.mt-4,.my-4{margin-top:1.5rem !important}.mr-4,.mx-4{margin-right:1.5rem !important}.mb-4,.my-4{margin-bottom:1.5rem !important}.ml-4,.mx-4{margin-left:1.5rem !important}.m-5{margin:3rem !important}.mt-5,.my-5{margin-top:3rem !important}.mr-5,.mx-5{margin-right:3rem !important}.mb-5,.my-5{margin-bottom:3rem !important}.ml-5,.mx-5{margin-left:3rem !important}.p-0{padding:0 !important}.pt-0,.py-0{padding-top:0 !important}.pr-0,.px-0{padding-right:0 !important}.pb-0,.py-0{padding-bottom:0 !important}.pl-0,.px-0{padding-left:0 !important}.p-1{padding:.25rem !important}.pt-1,.py-1{padding-top:.25rem !important}.pr-1,.px-1{padding-right:.25rem !important}.pb-1,.py-1{padding-bottom:.25rem !important}.pl-1,.px-1{padding-left:.25rem !important}.p-2{padding:.5rem !important}.pt-2,.py-2{padding-top:.5rem !important}.pr-2,.px-2{padding-right:.5rem !important}.pb-2,.py-2{padding-bottom:.5rem !important}.pl-2,.px-2{padding-left:.5rem !important}.p-3{padding:1rem !important}.pt-3,.py-3{padding-top:1rem !important}.pr-3,.px-3{padding-right:1rem !important}.pb-3,.py-3{padding-bottom:1rem !important}.pl-3,.px-3{padding-left:1rem !important}.p-4{padding:1.5rem !important}.pt-4,.py-4{padding-top:1.5rem !important}.pr-4,.px-4{padding-right:1.5rem !important}.pb-4,.py-4{padding-bottom:1.5rem !important}.pl-4,.px-4{padding-left:1.5rem !important}.p-5{padding:3rem !important}.pt-5,.py-5{padding-top:3rem !important}.pr-5,.px-5{padding-right:3rem !important}.pb-5,.py-5{padding-bottom:3rem !important}.pl-5,.px-5{padding-left:3rem !important}.m-n1{margin:-.25rem !important}.mt-n1,.my-n1{margin-top:-.25rem !important}.mr-n1,.mx-n1{margin-right:-.25rem !important}.mb-n1,.my-n1{margin-bottom:-.25rem !important}.ml-n1,.mx-n1{margin-left:-.25rem !important}.m-n2{margin:-.5rem !important}.mt-n2,.my-n2{margin-top:-.5rem !important}.mr-n2,.mx-n2{margin-right:-.5rem !important}.mb-n2,.my-n2{margin-bottom:-.5rem !important}.ml-n2,.mx-n2{margin-left:-.5rem !important}.m-n3{margin:-1rem !important}.mt-n3,.my-n3{margin-top:-1rem !important}.mr-n3,.mx-n3{margin-right:-1rem !important}.mb-n3,.my-n3{margin-bottom:-1rem !important}.ml-n3,.mx-n3{margin-left:-1rem !important}.m-n4{margin:-1.5rem !important}.mt-n4,.my-n4{margin-top:-1.5rem !important}.mr-n4,.mx-n4{margin-right:-1.5rem !important}.mb-n4,.my-n4{margin-bottom:-1.5rem !important}.ml-n4,.mx-n4{margin-left:-1.5rem !important}.m-n5{margin:-3rem !important}.mt-n5,.my-n5{margin-top:-3rem !important}.mr-n5,.mx-n5{margin-right:-3rem !important}.mb-n5,.my-n5{margin-bottom:-3rem !important}.ml-n5,.mx-n5{margin-left:-3rem !important}.m-auto{margin:auto !important}.mt-auto,.my-auto{margin-top:auto !important}.mr-auto,.mx-auto{margin-right:auto !important}.mb-auto,.my-auto{margin-bottom:auto !important}.ml-auto,.mx-auto{margin-left:auto !important}@media (min-width: 576px){.m-sm-0{margin:0 !important}.mt-sm-0,.my-sm-0{margin-top:0 !important}.mr-sm-0,.mx-sm-0{margin-right:0 !important}.mb-sm-0,.my-sm-0{margin-bottom:0 !important}.ml-sm-0,.mx-sm-0{margin-left:0 !important}.m-sm-1{margin:.25rem !important}.mt-sm-1,.my-sm-1{margin-top:.25rem !important}.mr-sm-1,.mx-sm-1{margin-right:.25rem !important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem !important}.ml-sm-1,.mx-sm-1{margin-left:.25rem !important}.m-sm-2{margin:.5rem !important}.mt-sm-2,.my-sm-2{margin-top:.5rem !important}.mr-sm-2,.mx-sm-2{margin-right:.5rem !important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem !important}.ml-sm-2,.mx-sm-2{margin-left:.5rem !important}.m-sm-3{margin:1rem !important}.mt-sm-3,.my-sm-3{margin-top:1rem !important}.mr-sm-3,.mx-sm-3{margin-right:1rem !important}.mb-sm-3,.my-sm-3{margin-bottom:1rem !important}.ml-sm-3,.mx-sm-3{margin-left:1rem !important}.m-sm-4{margin:1.5rem !important}.mt-sm-4,.my-sm-4{margin-top:1.5rem !important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem !important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem !important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem !important}.m-sm-5{margin:3rem !important}.mt-sm-5,.my-sm-5{margin-top:3rem !important}.mr-sm-5,.mx-sm-5{margin-right:3rem !important}.mb-sm-5,.my-sm-5{margin-bottom:3rem !important}.ml-sm-5,.mx-sm-5{margin-left:3rem !important}.p-sm-0{padding:0 !important}.pt-sm-0,.py-sm-0{padding-top:0 !important}.pr-sm-0,.px-sm-0{padding-right:0 !important}.pb-sm-0,.py-sm-0{padding-bottom:0 !important}.pl-sm-0,.px-sm-0{padding-left:0 !important}.p-sm-1{padding:.25rem !important}.pt-sm-1,.py-sm-1{padding-top:.25rem !important}.pr-sm-1,.px-sm-1{padding-right:.25rem !important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem !important}.pl-sm-1,.px-sm-1{padding-left:.25rem !important}.p-sm-2{padding:.5rem !important}.pt-sm-2,.py-sm-2{padding-top:.5rem !important}.pr-sm-2,.px-sm-2{padding-right:.5rem !important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem !important}.pl-sm-2,.px-sm-2{padding-left:.5rem !important}.p-sm-3{padding:1rem !important}.pt-sm-3,.py-sm-3{padding-top:1rem !important}.pr-sm-3,.px-sm-3{padding-right:1rem !important}.pb-sm-3,.py-sm-3{padding-bottom:1rem !important}.pl-sm-3,.px-sm-3{padding-left:1rem !important}.p-sm-4{padding:1.5rem !important}.pt-sm-4,.py-sm-4{padding-top:1.5rem !important}.pr-sm-4,.px-sm-4{padding-right:1.5rem !important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem !important}.pl-sm-4,.px-sm-4{padding-left:1.5rem !important}.p-sm-5{padding:3rem !important}.pt-sm-5,.py-sm-5{padding-top:3rem !important}.pr-sm-5,.px-sm-5{padding-right:3rem !important}.pb-sm-5,.py-sm-5{padding-bottom:3rem !important}.pl-sm-5,.px-sm-5{padding-left:3rem !important}.m-sm-n1{margin:-.25rem !important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem !important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem !important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem !important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem !important}.m-sm-n2{margin:-.5rem !important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem !important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem !important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem !important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem !important}.m-sm-n3{margin:-1rem !important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem !important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem !important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem !important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem !important}.m-sm-n4{margin:-1.5rem !important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem !important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem !important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem !important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem !important}.m-sm-n5{margin:-3rem !important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem !important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem !important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem !important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem !important}.m-sm-auto{margin:auto !important}.mt-sm-auto,.my-sm-auto{margin-top:auto !important}.mr-sm-auto,.mx-sm-auto{margin-right:auto !important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto !important}.ml-sm-auto,.mx-sm-auto{margin-left:auto !important}}@media (min-width: 768px){.m-md-0{margin:0 !important}.mt-md-0,.my-md-0{margin-top:0 !important}.mr-md-0,.mx-md-0{margin-right:0 !important}.mb-md-0,.my-md-0{margin-bottom:0 !important}.ml-md-0,.mx-md-0{margin-left:0 !important}.m-md-1{margin:.25rem !important}.mt-md-1,.my-md-1{margin-top:.25rem !important}.mr-md-1,.mx-md-1{margin-right:.25rem !important}.mb-md-1,.my-md-1{margin-bottom:.25rem !important}.ml-md-1,.mx-md-1{margin-left:.25rem !important}.m-md-2{margin:.5rem !important}.mt-md-2,.my-md-2{margin-top:.5rem !important}.mr-md-2,.mx-md-2{margin-right:.5rem !important}.mb-md-2,.my-md-2{margin-bottom:.5rem !important}.ml-md-2,.mx-md-2{margin-left:.5rem !important}.m-md-3{margin:1rem !important}.mt-md-3,.my-md-3{margin-top:1rem !important}.mr-md-3,.mx-md-3{margin-right:1rem !important}.mb-md-3,.my-md-3{margin-bottom:1rem !important}.ml-md-3,.mx-md-3{margin-left:1rem !important}.m-md-4{margin:1.5rem !important}.mt-md-4,.my-md-4{margin-top:1.5rem !important}.mr-md-4,.mx-md-4{margin-right:1.5rem !important}.mb-md-4,.my-md-4{margin-bottom:1.5rem !important}.ml-md-4,.mx-md-4{margin-left:1.5rem !important}.m-md-5{margin:3rem !important}.mt-md-5,.my-md-5{margin-top:3rem !important}.mr-md-5,.mx-md-5{margin-right:3rem !important}.mb-md-5,.my-md-5{margin-bottom:3rem !important}.ml-md-5,.mx-md-5{margin-left:3rem !important}.p-md-0{padding:0 !important}.pt-md-0,.py-md-0{padding-top:0 !important}.pr-md-0,.px-md-0{padding-right:0 !important}.pb-md-0,.py-md-0{padding-bottom:0 !important}.pl-md-0,.px-md-0{padding-left:0 !important}.p-md-1{padding:.25rem !important}.pt-md-1,.py-md-1{padding-top:.25rem !important}.pr-md-1,.px-md-1{padding-right:.25rem !important}.pb-md-1,.py-md-1{padding-bottom:.25rem !important}.pl-md-1,.px-md-1{padding-left:.25rem !important}.p-md-2{padding:.5rem !important}.pt-md-2,.py-md-2{padding-top:.5rem !important}.pr-md-2,.px-md-2{padding-right:.5rem !important}.pb-md-2,.py-md-2{padding-bottom:.5rem !important}.pl-md-2,.px-md-2{padding-left:.5rem !important}.p-md-3{padding:1rem !important}.pt-md-3,.py-md-3{padding-top:1rem !important}.pr-md-3,.px-md-3{padding-right:1rem !important}.pb-md-3,.py-md-3{padding-bottom:1rem !important}.pl-md-3,.px-md-3{padding-left:1rem !important}.p-md-4{padding:1.5rem !important}.pt-md-4,.py-md-4{padding-top:1.5rem !important}.pr-md-4,.px-md-4{padding-right:1.5rem !important}.pb-md-4,.py-md-4{padding-bottom:1.5rem !important}.pl-md-4,.px-md-4{padding-left:1.5rem !important}.p-md-5{padding:3rem !important}.pt-md-5,.py-md-5{padding-top:3rem !important}.pr-md-5,.px-md-5{padding-right:3rem !important}.pb-md-5,.py-md-5{padding-bottom:3rem !important}.pl-md-5,.px-md-5{padding-left:3rem !important}.m-md-n1{margin:-.25rem !important}.mt-md-n1,.my-md-n1{margin-top:-.25rem !important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem !important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem !important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem !important}.m-md-n2{margin:-.5rem !important}.mt-md-n2,.my-md-n2{margin-top:-.5rem !important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem !important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem !important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem !important}.m-md-n3{margin:-1rem !important}.mt-md-n3,.my-md-n3{margin-top:-1rem !important}.mr-md-n3,.mx-md-n3{margin-right:-1rem !important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem !important}.ml-md-n3,.mx-md-n3{margin-left:-1rem !important}.m-md-n4{margin:-1.5rem !important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem !important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem !important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem !important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem !important}.m-md-n5{margin:-3rem !important}.mt-md-n5,.my-md-n5{margin-top:-3rem !important}.mr-md-n5,.mx-md-n5{margin-right:-3rem !important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem !important}.ml-md-n5,.mx-md-n5{margin-left:-3rem !important}.m-md-auto{margin:auto !important}.mt-md-auto,.my-md-auto{margin-top:auto !important}.mr-md-auto,.mx-md-auto{margin-right:auto !important}.mb-md-auto,.my-md-auto{margin-bottom:auto !important}.ml-md-auto,.mx-md-auto{margin-left:auto !important}}@media (min-width: 992px){.m-lg-0{margin:0 !important}.mt-lg-0,.my-lg-0{margin-top:0 !important}.mr-lg-0,.mx-lg-0{margin-right:0 !important}.mb-lg-0,.my-lg-0{margin-bottom:0 !important}.ml-lg-0,.mx-lg-0{margin-left:0 !important}.m-lg-1{margin:.25rem !important}.mt-lg-1,.my-lg-1{margin-top:.25rem !important}.mr-lg-1,.mx-lg-1{margin-right:.25rem !important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem !important}.ml-lg-1,.mx-lg-1{margin-left:.25rem !important}.m-lg-2{margin:.5rem !important}.mt-lg-2,.my-lg-2{margin-top:.5rem !important}.mr-lg-2,.mx-lg-2{margin-right:.5rem !important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem !important}.ml-lg-2,.mx-lg-2{margin-left:.5rem !important}.m-lg-3{margin:1rem !important}.mt-lg-3,.my-lg-3{margin-top:1rem !important}.mr-lg-3,.mx-lg-3{margin-right:1rem !important}.mb-lg-3,.my-lg-3{margin-bottom:1rem !important}.ml-lg-3,.mx-lg-3{margin-left:1rem !important}.m-lg-4{margin:1.5rem !important}.mt-lg-4,.my-lg-4{margin-top:1.5rem !important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem !important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem !important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem !important}.m-lg-5{margin:3rem !important}.mt-lg-5,.my-lg-5{margin-top:3rem !important}.mr-lg-5,.mx-lg-5{margin-right:3rem !important}.mb-lg-5,.my-lg-5{margin-bottom:3rem !important}.ml-lg-5,.mx-lg-5{margin-left:3rem !important}.p-lg-0{padding:0 !important}.pt-lg-0,.py-lg-0{padding-top:0 !important}.pr-lg-0,.px-lg-0{padding-right:0 !important}.pb-lg-0,.py-lg-0{padding-bottom:0 !important}.pl-lg-0,.px-lg-0{padding-left:0 !important}.p-lg-1{padding:.25rem !important}.pt-lg-1,.py-lg-1{padding-top:.25rem !important}.pr-lg-1,.px-lg-1{padding-right:.25rem !important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem !important}.pl-lg-1,.px-lg-1{padding-left:.25rem !important}.p-lg-2{padding:.5rem !important}.pt-lg-2,.py-lg-2{padding-top:.5rem !important}.pr-lg-2,.px-lg-2{padding-right:.5rem !important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem !important}.pl-lg-2,.px-lg-2{padding-left:.5rem !important}.p-lg-3{padding:1rem !important}.pt-lg-3,.py-lg-3{padding-top:1rem !important}.pr-lg-3,.px-lg-3{padding-right:1rem !important}.pb-lg-3,.py-lg-3{padding-bottom:1rem !important}.pl-lg-3,.px-lg-3{padding-left:1rem !important}.p-lg-4{padding:1.5rem !important}.pt-lg-4,.py-lg-4{padding-top:1.5rem !important}.pr-lg-4,.px-lg-4{padding-right:1.5rem !important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem !important}.pl-lg-4,.px-lg-4{padding-left:1.5rem !important}.p-lg-5{padding:3rem !important}.pt-lg-5,.py-lg-5{padding-top:3rem !important}.pr-lg-5,.px-lg-5{padding-right:3rem !important}.pb-lg-5,.py-lg-5{padding-bottom:3rem !important}.pl-lg-5,.px-lg-5{padding-left:3rem !important}.m-lg-n1{margin:-.25rem !important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem !important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem !important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem !important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem !important}.m-lg-n2{margin:-.5rem !important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem !important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem !important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem !important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem !important}.m-lg-n3{margin:-1rem !important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem !important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem !important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem !important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem !important}.m-lg-n4{margin:-1.5rem !important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem !important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem !important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem !important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem !important}.m-lg-n5{margin:-3rem !important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem !important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem !important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem !important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem !important}.m-lg-auto{margin:auto !important}.mt-lg-auto,.my-lg-auto{margin-top:auto !important}.mr-lg-auto,.mx-lg-auto{margin-right:auto !important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto !important}.ml-lg-auto,.mx-lg-auto{margin-left:auto !important}}@media (min-width: 1200px){.m-xl-0{margin:0 !important}.mt-xl-0,.my-xl-0{margin-top:0 !important}.mr-xl-0,.mx-xl-0{margin-right:0 !important}.mb-xl-0,.my-xl-0{margin-bottom:0 !important}.ml-xl-0,.mx-xl-0{margin-left:0 !important}.m-xl-1{margin:.25rem !important}.mt-xl-1,.my-xl-1{margin-top:.25rem !important}.mr-xl-1,.mx-xl-1{margin-right:.25rem !important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem !important}.ml-xl-1,.mx-xl-1{margin-left:.25rem !important}.m-xl-2{margin:.5rem !important}.mt-xl-2,.my-xl-2{margin-top:.5rem !important}.mr-xl-2,.mx-xl-2{margin-right:.5rem !important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem !important}.ml-xl-2,.mx-xl-2{margin-left:.5rem !important}.m-xl-3{margin:1rem !important}.mt-xl-3,.my-xl-3{margin-top:1rem !important}.mr-xl-3,.mx-xl-3{margin-right:1rem !important}.mb-xl-3,.my-xl-3{margin-bottom:1rem !important}.ml-xl-3,.mx-xl-3{margin-left:1rem !important}.m-xl-4{margin:1.5rem !important}.mt-xl-4,.my-xl-4{margin-top:1.5rem !important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem !important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem !important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem !important}.m-xl-5{margin:3rem !important}.mt-xl-5,.my-xl-5{margin-top:3rem !important}.mr-xl-5,.mx-xl-5{margin-right:3rem !important}.mb-xl-5,.my-xl-5{margin-bottom:3rem !important}.ml-xl-5,.mx-xl-5{margin-left:3rem !important}.p-xl-0{padding:0 !important}.pt-xl-0,.py-xl-0{padding-top:0 !important}.pr-xl-0,.px-xl-0{padding-right:0 !important}.pb-xl-0,.py-xl-0{padding-bottom:0 !important}.pl-xl-0,.px-xl-0{padding-left:0 !important}.p-xl-1{padding:.25rem !important}.pt-xl-1,.py-xl-1{padding-top:.25rem !important}.pr-xl-1,.px-xl-1{padding-right:.25rem !important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem !important}.pl-xl-1,.px-xl-1{padding-left:.25rem !important}.p-xl-2{padding:.5rem !important}.pt-xl-2,.py-xl-2{padding-top:.5rem !important}.pr-xl-2,.px-xl-2{padding-right:.5rem !important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem !important}.pl-xl-2,.px-xl-2{padding-left:.5rem !important}.p-xl-3{padding:1rem !important}.pt-xl-3,.py-xl-3{padding-top:1rem !important}.pr-xl-3,.px-xl-3{padding-right:1rem !important}.pb-xl-3,.py-xl-3{padding-bottom:1rem !important}.pl-xl-3,.px-xl-3{padding-left:1rem !important}.p-xl-4{padding:1.5rem !important}.pt-xl-4,.py-xl-4{padding-top:1.5rem !important}.pr-xl-4,.px-xl-4{padding-right:1.5rem !important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem !important}.pl-xl-4,.px-xl-4{padding-left:1.5rem !important}.p-xl-5{padding:3rem !important}.pt-xl-5,.py-xl-5{padding-top:3rem !important}.pr-xl-5,.px-xl-5{padding-right:3rem !important}.pb-xl-5,.py-xl-5{padding-bottom:3rem !important}.pl-xl-5,.px-xl-5{padding-left:3rem !important}.m-xl-n1{margin:-.25rem !important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem !important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem !important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem !important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem !important}.m-xl-n2{margin:-.5rem !important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem !important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem !important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem !important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem !important}.m-xl-n3{margin:-1rem !important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem !important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem !important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem !important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem !important}.m-xl-n4{margin:-1.5rem !important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem !important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem !important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem !important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem !important}.m-xl-n5{margin:-3rem !important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem !important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem !important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem !important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem !important}.m-xl-auto{margin:auto !important}.mt-xl-auto,.my-xl-auto{margin-top:auto !important}.mr-xl-auto,.mx-xl-auto{margin-right:auto !important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto !important}.ml-xl-auto,.mx-xl-auto{margin-left:auto !important}}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:auto;content:"";background-color:rgba(0,0,0,0)}.text-monospace{font-family:"Source Code Pro" !important}.text-justify{text-align:justify !important}.text-wrap{white-space:normal !important}.text-nowrap{white-space:nowrap !important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left !important}.text-right{text-align:right !important}.text-center{text-align:center !important}@media (min-width: 576px){.text-sm-left{text-align:left !important}.text-sm-right{text-align:right !important}.text-sm-center{text-align:center !important}}@media (min-width: 768px){.text-md-left{text-align:left !important}.text-md-right{text-align:right !important}.text-md-center{text-align:center !important}}@media (min-width: 992px){.text-lg-left{text-align:left !important}.text-lg-right{text-align:right !important}.text-lg-center{text-align:center !important}}@media (min-width: 1200px){.text-xl-left{text-align:left !important}.text-xl-right{text-align:right !important}.text-xl-center{text-align:center !important}}.text-lowercase{text-transform:lowercase !important}.text-uppercase{text-transform:uppercase !important}.text-capitalize{text-transform:capitalize !important}.font-weight-light{font-weight:300 !important}.font-weight-lighter{font-weight:lighter !important}.font-weight-normal{font-weight:400 !important}.font-weight-bold{font-weight:700 !important}.font-weight-bolder{font-weight:bolder !important}.font-italic{font-style:italic !important}.text-white{color:#fff !important}.text-default{color:#dee2e6 !important}a.text-default:hover,a.text-default:focus{color:#b2bcc5 !important}.text-primary{color:#1e90ff !important}a.text-primary:hover,a.text-primary:focus{color:#006ad1 !important}.text-secondary{color:#6c757d !important}a.text-secondary:hover,a.text-secondary:focus{color:#494f54 !important}.text-success{color:#28a745 !important}a.text-success:hover,a.text-success:focus{color:#19692c !important}.text-info{color:#0DCAF0 !important}a.text-info:hover,a.text-info:focus{color:#098da7 !important}.text-warning{color:#ffc107 !important}a.text-warning:hover,a.text-warning:focus{color:#ba8b00 !important}.text-danger{color:#dc3545 !important}a.text-danger:hover,a.text-danger:focus{color:#a71d2a !important}.text-light{color:#f8f9fa !important}a.text-light:hover,a.text-light:focus{color:#cbd3da !important}.text-dark{color:#343a40 !important}a.text-dark:hover,a.text-dark:focus{color:#121416 !important}.text-body{color:#212529 !important}.text-muted,.help-text,.help-block{color:#6c757d !important}.text-black-50{color:rgba(0,0,0,0.5) !important}.text-white-50{color:rgba(255,255,255,0.5) !important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none !important}.text-break{word-break:break-word !important;word-wrap:break-word !important}.text-reset{color:inherit !important}.visible{visibility:visible !important}.invisible{visibility:hidden !important}@media print{*,*::before,*::after{text-shadow:none !important;box-shadow:none !important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap !important}pre,blockquote{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px !important}.container{min-width:992px !important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse !important}.table td,.table th{background-color:#fff !important}.table-bordered th,.table-bordered td{border:1px solid #dee2e6 !important}.table-dark{color:inherit}.table-dark th,.table-dark td,.table-dark thead th,.table-dark tbody+tbody{border-color:#dee2e6}.table .thead-dark th{color:inherit;border-color:#dee2e6}}.table th[align=left]{text-align:left}.table th[align=right]{text-align:right}.table th[align=center]{text-align:center}.well{display:block;background-color:rgba(0,0,0,0.03);color:#212529;padding:1.25rem;border-radius:.25rem}.well-lg{padding:1.5rem;border-radius:.3rem}.well-sm{padding:0.5rem;border-radius:.2rem}.draggable .well{background-color:#f7f7f7}.dropdown-menu>li.active>a{color:#fff;text-decoration:none;background-color:#1e90ff}.navbar .nav.nav-underline{--bs-navbar-nav-link-padding-x: 0}.navbar:not(.fixed-bottom):not(.navbar-fixed-bottom):not(.navbar-fixed-bottom)+div>.tab-content>.tab-pane{--bslib-navbar-margin: 20px;margin-top:var(--bslib-navbar-margin)}ul.nav.navbar-nav{flex:1;-webkit-flex:1}ul.nav.navbar-nav.navbar-right{flex:unset;-webkit-flex:unset;display:flex;display:-webkit-flex;justify-content:flex-end;-webkit-justify-content:flex-end}:where(ul.nav.navbar-nav > li).active>a,:where(ul.nav.navbar-nav > li).show>a,.in:where(ul.nav.navbar-nav > li)>a{color:var(--bs-navbar-active-color)}:where(ul.nav.navbar-nav > li).bslib-nav-item{color:var(--bs-navbar-active-color)}.navbar{--bslib-navbar-default-bg: #f8f9fa;--bslib-navbar-inverse-bg: #343a40}.navbar.navbar-default{background-color:var(--bslib-navbar-default-bg, var(--bs-light)) !important}.navbar.navbar-inverse{background-color:var(--bslib-navbar-inverse-bg, var(--bs-dark)) !important;--bs-emphasis-color: white;--bs-emphasis-color-rgb: 255, 255, 255}.navbar-toggle>.icon-bar{display:none}@media (max-width: 575.98px){.navbar-header{width:100%}.navbar-header .navbar-toggle{float:right}}.nav-tabs>li.active>a{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-pills>li.active>a{color:#fff;background-color:#1e90ff}.nav-stacked{flex-direction:column;-webkit-flex-direction:column}.progress-bar-default{background-color:#dee2e6;color:#000}.progress-bar-primary{background-color:#1e90ff;color:#fff}.progress-bar-secondary{background-color:#6c757d;color:#fff}.progress-bar-success{background-color:#28a745;color:#fff}.progress-bar-info{background-color:#0DCAF0;color:#000}.progress-bar-warning{background-color:#ffc107;color:#000}.progress-bar-danger{background-color:#dc3545;color:#fff}.progress-bar-light{background-color:#f8f9fa;color:#000}.progress-bar-dark{background-color:#343a40;color:#fff}@font-face{font-family:'Glyphicons Halflings';src:url("fonts/bootstrap/glyphicons-halflings-regular.eot");src:url("fonts/bootstrap/glyphicons-halflings-regular.eot?#iefix") format("embedded-opentype"),url("fonts/bootstrap/glyphicons-halflings-regular.woff2") format("woff2"),url("fonts/bootstrap/glyphicons-halflings-regular.woff") format("woff"),url("fonts/bootstrap/glyphicons-halflings-regular.ttf") format("truetype"),url("fonts/bootstrap/glyphicons-halflings-regular.svg#glyphicons_halflingsregular") format("svg")}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-euro:before,.glyphicon-eur:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}.form-group{margin-bottom:1rem}.shiny-input-checkboxgroup .checkbox-inline,.shiny-input-checkboxgroup .radio-inline,.shiny-input-radiogroup .checkbox-inline,.shiny-input-radiogroup .radio-inline{padding-left:0;margin-right:.75rem}.shiny-input-checkboxgroup .checkbox-inline label>input,.shiny-input-checkboxgroup .radio-inline label>input,.shiny-input-radiogroup .checkbox-inline label>input,.shiny-input-radiogroup .radio-inline label>input{margin-top:0;margin-right:.3125rem;margin-bottom:0}.input-daterange .input-group-addon.input-group-prepend.input-group-append{padding:inherit;line-height:inherit;text-shadow:inherit;border-width:0}.input-daterange .input-group-addon.input-group-prepend.input-group-append .input-group-text{border-radius:0}.shiny-input-checkboxgroup .checkbox-inline,.shiny-input-radiogroup .radio-inline{cursor:pointer}pre.shiny-code{padding:0.5rem}h1,h2,h3{margin-top:1.5rem}h4,h5,h6{margin-top:1rem}:root{--bslib-spacer: 1rem;--bslib-mb-spacer: var(--bslib-spacer, 1rem)}.bslib-mb-spacing{margin-bottom:var(--bslib-mb-spacer)}.bslib-gap-spacing{gap:var(--bslib-mb-spacer)}.bslib-gap-spacing>.bslib-mb-spacing,.bslib-gap-spacing>.form-group,.bslib-gap-spacing>p,.bslib-gap-spacing>pre{margin-bottom:0}.html-fill-container>.html-fill-item.bslib-mb-spacing{margin-bottom:0}.tab-content>.tab-pane.html-fill-container{display:none}.tab-content>.active.html-fill-container{display:flex}.tab-content.html-fill-container{padding:0} diff --git a/.r-cache/R/sass/efa802adb72651a608ebef004fcc7eac b/.r-cache/R/sass/efa802adb72651a608ebef004fcc7eac new file mode 100644 index 0000000..91443c8 --- /dev/null +++ b/.r-cache/R/sass/efa802adb72651a608ebef004fcc7eac @@ -0,0 +1 @@ +:root{--bslib-bootstrap-version: 4;--bslib-preset-name: ;--bslib-preset-type: }:root{--blue: #007bff;--indigo: #6610f2;--purple: #6f42c1;--pink: #e83e8c;--red: #dc3545;--orange: #fd7e14;--yellow: #ffc107;--green: #28a745;--teal: #20c997;--cyan: #17a2b8;--white: #fff;--gray: #6c757d;--gray-dark: #343a40;--default: #dee2e6;--primary: #1e90ff;--secondary: #6c757d;--success: #28a745;--info: #0DCAF0;--warning: #ffc107;--danger: #dc3545;--light: #f8f9fa;--dark: #343a40;--breakpoint-xs: 0;--breakpoint-sm: 576px;--breakpoint-md: 768px;--breakpoint-lg: 992px;--breakpoint-xl: 1200px;--font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace}*,*::before,*::after{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0)}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0 !important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[title],abbr[data-original-title]{text-decoration:underline;text-decoration:underline dotted;-webkit-text-decoration:underline dotted;-moz-text-decoration:underline dotted;-ms-text-decoration:underline dotted;-o-text-decoration:underline dotted;cursor:help;border-bottom:0;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul,dl{margin-top:0;margin-bottom:1rem}ol ol,ul ul,ol ul,ul ol{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem;padding:.625rem 1.25rem;border-left:.25rem solid #e9ecef}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#1e90ff;text-decoration:none;-webkit-text-decoration:none;-moz-text-decoration:none;-ms-text-decoration:none;-o-text-decoration:none;background-color:transparent}a:hover{color:#006ad1;text-decoration:underline;-webkit-text-decoration:underline;-moz-text-decoration:underline;-ms-text-decoration:underline;-o-text-decoration:underline}a:not([href]):not([class]){color:inherit;text-decoration:none}a:not([href]):not([class]):hover{color:inherit;text-decoration:none}pre,code,kbd,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit;text-align:-webkit-match-parent}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus:not(:focus-visible){outline:0}input,button,select,optgroup,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[role="button"]{cursor:pointer}select{word-wrap:normal}button,[type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}button:not(:disabled),[type="button"]:not(:disabled),[type="reset"]:not(:disabled),[type="submit"]:not(:disabled){cursor:pointer}button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner{padding:0;border-style:none}input[type="radio"],input[type="checkbox"]{box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto}[type="search"]{outline-offset:-2px;-webkit-appearance:none}[type="search"]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none !important}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{margin-bottom:.5rem;font-weight:500;line-height:1.2}h1,.h1{font-size:2.5rem}h2,.h2{font-size:2rem}h3,.h3{font-size:1.75rem}h4,.h4{font-size:1.5rem}h5,.h5{font-size:1.25rem}h6,.h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,0.1)}small,.small{font-size:80%;font-weight:400}mark,.mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:80%;color:#6c757d}.blockquote-footer::before{content:"\2014\00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#6c757d}code{font-size:87.5%;color:#000;background-color:#f6f6f6;border-radius:.25rem;padding:.125rem .25rem;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#000;background-color:#f6f6f6;padding:.5rem;border:1px solid #dee2e6;border-radius:.25rem}pre code{background-color:transparent;font-size:inherit;color:inherit;word-break:normal;padding:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container,.container-fluid,.container-xl,.container-lg,.container-md,.container-sm{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width: 576px){.container-sm,.container{max-width:540px}}@media (min-width: 768px){.container-md,.container-sm,.container{max-width:720px}}@media (min-width: 992px){.container-lg,.container-md,.container-sm,.container{max-width:960px}}@media (min-width: 1200px){.container-xl,.container-lg,.container-md,.container-sm,.container{max-width:1140px}}.row{display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*="col-"]{padding-right:0;padding-left:0}.col-xl,.col-xl-auto,.col-xl-12,.col-xl-11,.col-xl-10,.col-xl-9,.col-xl-8,.col-xl-7,.col-xl-6,.col-xl-5,.col-xl-4,.col-xl-3,.col-xl-2,.col-xl-1,.col-lg,.col-lg-auto,.col-lg-12,.col-lg-11,.col-lg-10,.col-lg-9,.col-lg-8,.col-lg-7,.col-lg-6,.col-lg-5,.col-lg-4,.col-lg-3,.col-lg-2,.col-lg-1,.col-md,.col-md-auto,.col-md-12,.col-md-11,.col-md-10,.col-md-9,.col-md-8,.col-md-7,.col-md-6,.col-md-5,.col-md-4,.col-md-3,.col-md-2,.col-md-1,.col-sm,.col-sm-auto,.col-sm-12,.col-sm-11,.col-sm-10,.col-sm-9,.col-sm-8,.col-sm-7,.col-sm-6,.col-sm-5,.col-sm-4,.col-sm-3,.col-sm-2,.col-sm-1,.col,.col-auto,.col-12,.col-11,.col-10,.col-9,.col-8,.col-7,.col-6,.col-5,.col-4,.col-3,.col-2,.col-1{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{flex-basis:0;-webkit-flex-basis:0;flex-grow:1;-webkit-flex-grow:1;max-width:100%}.row-cols-1>*{flex:0 0 100%;-webkit-flex:0 0 100%;max-width:100%}.row-cols-2>*{flex:0 0 50%;-webkit-flex:0 0 50%;max-width:50%}.row-cols-3>*{flex:0 0 33.33333%;-webkit-flex:0 0 33.33333%;max-width:33.33333%}.row-cols-4>*{flex:0 0 25%;-webkit-flex:0 0 25%;max-width:25%}.row-cols-5>*{flex:0 0 20%;-webkit-flex:0 0 20%;max-width:20%}.row-cols-6>*{flex:0 0 16.66667%;-webkit-flex:0 0 16.66667%;max-width:16.66667%}.col-auto{flex:0 0 auto;-webkit-flex:0 0 auto;width:auto;max-width:100%}.col-1{flex:0 0 8.33333%;-webkit-flex:0 0 8.33333%;max-width:8.33333%}.col-2{flex:0 0 16.66667%;-webkit-flex:0 0 16.66667%;max-width:16.66667%}.col-3{flex:0 0 25%;-webkit-flex:0 0 25%;max-width:25%}.col-4{flex:0 0 33.33333%;-webkit-flex:0 0 33.33333%;max-width:33.33333%}.col-5{flex:0 0 41.66667%;-webkit-flex:0 0 41.66667%;max-width:41.66667%}.col-6{flex:0 0 50%;-webkit-flex:0 0 50%;max-width:50%}.col-7{flex:0 0 58.33333%;-webkit-flex:0 0 58.33333%;max-width:58.33333%}.col-8{flex:0 0 66.66667%;-webkit-flex:0 0 66.66667%;max-width:66.66667%}.col-9{flex:0 0 75%;-webkit-flex:0 0 75%;max-width:75%}.col-10{flex:0 0 83.33333%;-webkit-flex:0 0 83.33333%;max-width:83.33333%}.col-11{flex:0 0 91.66667%;-webkit-flex:0 0 91.66667%;max-width:91.66667%}.col-12{flex:0 0 100%;-webkit-flex:0 0 100%;max-width:100%}.order-first{order:-1}.order-last{order:13}.order-0{order:0}.order-1{order:1}.order-2{order:2}.order-3{order:3}.order-4{order:4}.order-5{order:5}.order-6{order:6}.order-7{order:7}.order-8{order:8}.order-9{order:9}.order-10{order:10}.order-11{order:11}.order-12{order:12}.offset-1{margin-left:8.33333%}.offset-2{margin-left:16.66667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333%}.offset-5{margin-left:41.66667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333%}.offset-8{margin-left:66.66667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333%}.offset-11{margin-left:91.66667%}@media (min-width: 576px){.col-sm{flex-basis:0;-webkit-flex-basis:0;flex-grow:1;-webkit-flex-grow:1;max-width:100%}.row-cols-sm-1>*{flex:0 0 100%;-webkit-flex:0 0 100%;max-width:100%}.row-cols-sm-2>*{flex:0 0 50%;-webkit-flex:0 0 50%;max-width:50%}.row-cols-sm-3>*{flex:0 0 33.33333%;-webkit-flex:0 0 33.33333%;max-width:33.33333%}.row-cols-sm-4>*{flex:0 0 25%;-webkit-flex:0 0 25%;max-width:25%}.row-cols-sm-5>*{flex:0 0 20%;-webkit-flex:0 0 20%;max-width:20%}.row-cols-sm-6>*{flex:0 0 16.66667%;-webkit-flex:0 0 16.66667%;max-width:16.66667%}.col-sm-auto{flex:0 0 auto;-webkit-flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{flex:0 0 8.33333%;-webkit-flex:0 0 8.33333%;max-width:8.33333%}.col-sm-2{flex:0 0 16.66667%;-webkit-flex:0 0 16.66667%;max-width:16.66667%}.col-sm-3{flex:0 0 25%;-webkit-flex:0 0 25%;max-width:25%}.col-sm-4{flex:0 0 33.33333%;-webkit-flex:0 0 33.33333%;max-width:33.33333%}.col-sm-5{flex:0 0 41.66667%;-webkit-flex:0 0 41.66667%;max-width:41.66667%}.col-sm-6{flex:0 0 50%;-webkit-flex:0 0 50%;max-width:50%}.col-sm-7{flex:0 0 58.33333%;-webkit-flex:0 0 58.33333%;max-width:58.33333%}.col-sm-8{flex:0 0 66.66667%;-webkit-flex:0 0 66.66667%;max-width:66.66667%}.col-sm-9{flex:0 0 75%;-webkit-flex:0 0 75%;max-width:75%}.col-sm-10{flex:0 0 83.33333%;-webkit-flex:0 0 83.33333%;max-width:83.33333%}.col-sm-11{flex:0 0 91.66667%;-webkit-flex:0 0 91.66667%;max-width:91.66667%}.col-sm-12{flex:0 0 100%;-webkit-flex:0 0 100%;max-width:100%}.order-sm-first{order:-1}.order-sm-last{order:13}.order-sm-0{order:0}.order-sm-1{order:1}.order-sm-2{order:2}.order-sm-3{order:3}.order-sm-4{order:4}.order-sm-5{order:5}.order-sm-6{order:6}.order-sm-7{order:7}.order-sm-8{order:8}.order-sm-9{order:9}.order-sm-10{order:10}.order-sm-11{order:11}.order-sm-12{order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333%}.offset-sm-2{margin-left:16.66667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333%}.offset-sm-5{margin-left:41.66667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333%}.offset-sm-8{margin-left:66.66667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333%}.offset-sm-11{margin-left:91.66667%}}@media (min-width: 768px){.col-md{flex-basis:0;-webkit-flex-basis:0;flex-grow:1;-webkit-flex-grow:1;max-width:100%}.row-cols-md-1>*{flex:0 0 100%;-webkit-flex:0 0 100%;max-width:100%}.row-cols-md-2>*{flex:0 0 50%;-webkit-flex:0 0 50%;max-width:50%}.row-cols-md-3>*{flex:0 0 33.33333%;-webkit-flex:0 0 33.33333%;max-width:33.33333%}.row-cols-md-4>*{flex:0 0 25%;-webkit-flex:0 0 25%;max-width:25%}.row-cols-md-5>*{flex:0 0 20%;-webkit-flex:0 0 20%;max-width:20%}.row-cols-md-6>*{flex:0 0 16.66667%;-webkit-flex:0 0 16.66667%;max-width:16.66667%}.col-md-auto{flex:0 0 auto;-webkit-flex:0 0 auto;width:auto;max-width:100%}.col-md-1{flex:0 0 8.33333%;-webkit-flex:0 0 8.33333%;max-width:8.33333%}.col-md-2{flex:0 0 16.66667%;-webkit-flex:0 0 16.66667%;max-width:16.66667%}.col-md-3{flex:0 0 25%;-webkit-flex:0 0 25%;max-width:25%}.col-md-4{flex:0 0 33.33333%;-webkit-flex:0 0 33.33333%;max-width:33.33333%}.col-md-5{flex:0 0 41.66667%;-webkit-flex:0 0 41.66667%;max-width:41.66667%}.col-md-6{flex:0 0 50%;-webkit-flex:0 0 50%;max-width:50%}.col-md-7{flex:0 0 58.33333%;-webkit-flex:0 0 58.33333%;max-width:58.33333%}.col-md-8{flex:0 0 66.66667%;-webkit-flex:0 0 66.66667%;max-width:66.66667%}.col-md-9{flex:0 0 75%;-webkit-flex:0 0 75%;max-width:75%}.col-md-10{flex:0 0 83.33333%;-webkit-flex:0 0 83.33333%;max-width:83.33333%}.col-md-11{flex:0 0 91.66667%;-webkit-flex:0 0 91.66667%;max-width:91.66667%}.col-md-12{flex:0 0 100%;-webkit-flex:0 0 100%;max-width:100%}.order-md-first{order:-1}.order-md-last{order:13}.order-md-0{order:0}.order-md-1{order:1}.order-md-2{order:2}.order-md-3{order:3}.order-md-4{order:4}.order-md-5{order:5}.order-md-6{order:6}.order-md-7{order:7}.order-md-8{order:8}.order-md-9{order:9}.order-md-10{order:10}.order-md-11{order:11}.order-md-12{order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333%}.offset-md-2{margin-left:16.66667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333%}.offset-md-5{margin-left:41.66667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333%}.offset-md-8{margin-left:66.66667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333%}.offset-md-11{margin-left:91.66667%}}@media (min-width: 992px){.col-lg{flex-basis:0;-webkit-flex-basis:0;flex-grow:1;-webkit-flex-grow:1;max-width:100%}.row-cols-lg-1>*{flex:0 0 100%;-webkit-flex:0 0 100%;max-width:100%}.row-cols-lg-2>*{flex:0 0 50%;-webkit-flex:0 0 50%;max-width:50%}.row-cols-lg-3>*{flex:0 0 33.33333%;-webkit-flex:0 0 33.33333%;max-width:33.33333%}.row-cols-lg-4>*{flex:0 0 25%;-webkit-flex:0 0 25%;max-width:25%}.row-cols-lg-5>*{flex:0 0 20%;-webkit-flex:0 0 20%;max-width:20%}.row-cols-lg-6>*{flex:0 0 16.66667%;-webkit-flex:0 0 16.66667%;max-width:16.66667%}.col-lg-auto{flex:0 0 auto;-webkit-flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{flex:0 0 8.33333%;-webkit-flex:0 0 8.33333%;max-width:8.33333%}.col-lg-2{flex:0 0 16.66667%;-webkit-flex:0 0 16.66667%;max-width:16.66667%}.col-lg-3{flex:0 0 25%;-webkit-flex:0 0 25%;max-width:25%}.col-lg-4{flex:0 0 33.33333%;-webkit-flex:0 0 33.33333%;max-width:33.33333%}.col-lg-5{flex:0 0 41.66667%;-webkit-flex:0 0 41.66667%;max-width:41.66667%}.col-lg-6{flex:0 0 50%;-webkit-flex:0 0 50%;max-width:50%}.col-lg-7{flex:0 0 58.33333%;-webkit-flex:0 0 58.33333%;max-width:58.33333%}.col-lg-8{flex:0 0 66.66667%;-webkit-flex:0 0 66.66667%;max-width:66.66667%}.col-lg-9{flex:0 0 75%;-webkit-flex:0 0 75%;max-width:75%}.col-lg-10{flex:0 0 83.33333%;-webkit-flex:0 0 83.33333%;max-width:83.33333%}.col-lg-11{flex:0 0 91.66667%;-webkit-flex:0 0 91.66667%;max-width:91.66667%}.col-lg-12{flex:0 0 100%;-webkit-flex:0 0 100%;max-width:100%}.order-lg-first{order:-1}.order-lg-last{order:13}.order-lg-0{order:0}.order-lg-1{order:1}.order-lg-2{order:2}.order-lg-3{order:3}.order-lg-4{order:4}.order-lg-5{order:5}.order-lg-6{order:6}.order-lg-7{order:7}.order-lg-8{order:8}.order-lg-9{order:9}.order-lg-10{order:10}.order-lg-11{order:11}.order-lg-12{order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333%}.offset-lg-2{margin-left:16.66667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333%}.offset-lg-5{margin-left:41.66667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333%}.offset-lg-8{margin-left:66.66667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333%}.offset-lg-11{margin-left:91.66667%}}@media (min-width: 1200px){.col-xl{flex-basis:0;-webkit-flex-basis:0;flex-grow:1;-webkit-flex-grow:1;max-width:100%}.row-cols-xl-1>*{flex:0 0 100%;-webkit-flex:0 0 100%;max-width:100%}.row-cols-xl-2>*{flex:0 0 50%;-webkit-flex:0 0 50%;max-width:50%}.row-cols-xl-3>*{flex:0 0 33.33333%;-webkit-flex:0 0 33.33333%;max-width:33.33333%}.row-cols-xl-4>*{flex:0 0 25%;-webkit-flex:0 0 25%;max-width:25%}.row-cols-xl-5>*{flex:0 0 20%;-webkit-flex:0 0 20%;max-width:20%}.row-cols-xl-6>*{flex:0 0 16.66667%;-webkit-flex:0 0 16.66667%;max-width:16.66667%}.col-xl-auto{flex:0 0 auto;-webkit-flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{flex:0 0 8.33333%;-webkit-flex:0 0 8.33333%;max-width:8.33333%}.col-xl-2{flex:0 0 16.66667%;-webkit-flex:0 0 16.66667%;max-width:16.66667%}.col-xl-3{flex:0 0 25%;-webkit-flex:0 0 25%;max-width:25%}.col-xl-4{flex:0 0 33.33333%;-webkit-flex:0 0 33.33333%;max-width:33.33333%}.col-xl-5{flex:0 0 41.66667%;-webkit-flex:0 0 41.66667%;max-width:41.66667%}.col-xl-6{flex:0 0 50%;-webkit-flex:0 0 50%;max-width:50%}.col-xl-7{flex:0 0 58.33333%;-webkit-flex:0 0 58.33333%;max-width:58.33333%}.col-xl-8{flex:0 0 66.66667%;-webkit-flex:0 0 66.66667%;max-width:66.66667%}.col-xl-9{flex:0 0 75%;-webkit-flex:0 0 75%;max-width:75%}.col-xl-10{flex:0 0 83.33333%;-webkit-flex:0 0 83.33333%;max-width:83.33333%}.col-xl-11{flex:0 0 91.66667%;-webkit-flex:0 0 91.66667%;max-width:91.66667%}.col-xl-12{flex:0 0 100%;-webkit-flex:0 0 100%;max-width:100%}.order-xl-first{order:-1}.order-xl-last{order:13}.order-xl-0{order:0}.order-xl-1{order:1}.order-xl-2{order:2}.order-xl-3{order:3}.order-xl-4{order:4}.order-xl-5{order:5}.order-xl-6{order:6}.order-xl-7{order:7}.order-xl-8{order:8}.order-xl-9{order:9}.order-xl-10{order:10}.order-xl-11{order:11}.order-xl-12{order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333%}.offset-xl-2{margin-left:16.66667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333%}.offset-xl-5{margin-left:41.66667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333%}.offset-xl-8{margin-left:66.66667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333%}.offset-xl-11{margin-left:91.66667%}}.table{width:100%;margin-bottom:1rem;color:#212529}.table th,.table td{padding:.75rem;vertical-align:top;border-top:1px solid #dee2e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dee2e6}.table tbody+tbody{border-top:2px solid #dee2e6}.table-sm th,.table-sm td{padding:.3rem}.table-bordered{border:1px solid #dee2e6}.table-bordered th,.table-bordered td{border:1px solid #dee2e6}.table-bordered thead th,.table-bordered thead td{border-bottom-width:2px}.table-borderless th,.table-borderless td,.table-borderless thead th,.table-borderless tbody+tbody{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,0.05)}.table-hover tbody tr:hover{color:#212529;background-color:rgba(0,0,0,0.075)}.table-default,.table-default>th,.table-default>td{background-color:#f6f7f8}.table-default th,.table-default td,.table-default thead th,.table-default tbody+tbody{border-color:#eef0f2}.table-hover .table-default:hover{background-color:#e8eaed}.table-hover .table-default:hover>td,.table-hover .table-default:hover>th{background-color:#e8eaed}.table-primary,.table-primary>th,.table-primary>td{background-color:#c0e0ff}.table-primary th,.table-primary td,.table-primary thead th,.table-primary tbody+tbody{border-color:#8ac5ff}.table-hover .table-primary:hover{background-color:#a7d3ff}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#a7d3ff}.table-secondary,.table-secondary>th,.table-secondary>td{background-color:#d6d8db}.table-secondary th,.table-secondary td,.table-secondary thead th,.table-secondary tbody+tbody{border-color:#b3b7bb}.table-hover .table-secondary:hover{background-color:#c8cbcf}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#c8cbcf}.table-success,.table-success>th,.table-success>td{background-color:#c3e6cb}.table-success th,.table-success td,.table-success thead th,.table-success tbody+tbody{border-color:#8fd19e}.table-hover .table-success:hover{background-color:#b1dfbb}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b1dfbb}.table-info,.table-info>th,.table-info>td{background-color:#bbf0fb}.table-info th,.table-info td,.table-info thead th,.table-info tbody+tbody{border-color:#81e3f7}.table-hover .table-info:hover{background-color:#a3ebfa}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#a3ebfa}.table-warning,.table-warning>th,.table-warning>td{background-color:#ffeeba}.table-warning th,.table-warning td,.table-warning thead th,.table-warning tbody+tbody{border-color:#ffdf7e}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>th,.table-danger>td{background-color:#f5c6cb}.table-danger th,.table-danger td,.table-danger thead th,.table-danger tbody+tbody{border-color:#ed969e}.table-hover .table-danger:hover{background-color:#f1b0b7}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f1b0b7}.table-light,.table-light>th,.table-light>td{background-color:#fdfdfe}.table-light th,.table-light td,.table-light thead th,.table-light tbody+tbody{border-color:#fbfcfc}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>th,.table-dark>td{background-color:#c6c8ca}.table-dark th,.table-dark td,.table-dark thead th,.table-dark tbody+tbody{border-color:#95999c}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>th,.table-active>td{background-color:rgba(0,0,0,0.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,0.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,0.075)}.table .thead-dark th{color:#fff;background-color:#343a40;border-color:#454d55}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#dee2e6}.table-dark{color:#fff;background-color:#343a40}.table-dark th,.table-dark td,.table-dark thead th{border-color:#454d55}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,0.05)}.table-dark.table-hover tbody tr:hover{color:#fff;background-color:rgba(255,255,255,0.075)}@media (max-width: 575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-sm>.table-bordered{border:0}}@media (max-width: 767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-md>.table-bordered{border:0}}@media (max-width: 991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-lg>.table-bordered{border:0}}@media (max-width: 1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}.form-control:focus{color:#495057;background-color:#fff;border-color:#9ecfff;outline:0;box-shadow:0 0 0 .2rem rgba(30,144,255,0.25)}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}input[type="date"].form-control,input[type="time"].form-control,input[type="datetime-local"].form-control,input[type="month"].form-control{appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;font-size:1rem;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-sm,.form-control-plaintext.form-control-lg{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.form-control-lg{height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}select.form-control[size],select.form-control[multiple]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text,.help-text,.help-block{display:block;margin-top:.25rem}.form-row{display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*="col-"]{padding-right:5px;padding-left:5px}.form-check,.shiny-input-checkboxgroup .checkbox,.shiny-input-checkboxgroup .radio,.shiny-input-radiogroup .checkbox,.shiny-input-radiogroup .radio{position:relative;display:block;padding-left:1.25rem}.form-check-input,.shiny-input-checkboxgroup .checkbox label>input,.shiny-input-checkboxgroup .radio label>input,.shiny-input-radiogroup .checkbox label>input,.shiny-input-radiogroup .radio label>input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input[disabled]~.form-check-label,.shiny-input-checkboxgroup .checkbox label>input[disabled]~.form-check-label,.shiny-input-checkboxgroup .radio label>input[disabled]~.form-check-label,.shiny-input-radiogroup .checkbox label>input[disabled]~.form-check-label,.shiny-input-radiogroup .radio label>input[disabled]~.form-check-label,.shiny-input-checkboxgroup .checkbox .form-check-input[disabled]~label,.shiny-input-checkboxgroup .checkbox label>input[disabled]~label,.shiny-input-checkboxgroup .radio .form-check-input[disabled]~label,.shiny-input-checkboxgroup .radio label>input[disabled]~label,.shiny-input-radiogroup .checkbox .form-check-input[disabled]~label,.shiny-input-radiogroup .checkbox label>input[disabled]~label,.shiny-input-radiogroup .radio .form-check-input[disabled]~label,.shiny-input-radiogroup .radio label>input[disabled]~label,.form-check-input:disabled~.form-check-label,.shiny-input-checkboxgroup .checkbox label>input:disabled~.form-check-label,.shiny-input-checkboxgroup .radio label>input:disabled~.form-check-label,.shiny-input-radiogroup .checkbox label>input:disabled~.form-check-label,.shiny-input-radiogroup .radio label>input:disabled~.form-check-label,.shiny-input-checkboxgroup .checkbox .form-check-input:disabled~label,.shiny-input-checkboxgroup .checkbox label>input:disabled~label,.shiny-input-checkboxgroup .radio .form-check-input:disabled~label,.shiny-input-checkboxgroup .radio label>input:disabled~label,.shiny-input-radiogroup .checkbox .form-check-input:disabled~label,.shiny-input-radiogroup .checkbox label>input:disabled~label,.shiny-input-radiogroup .radio .form-check-input:disabled~label,.shiny-input-radiogroup .radio label>input:disabled~label{color:#6c757d}.form-check-label,.shiny-input-checkboxgroup .checkbox label,.shiny-input-checkboxgroup .radio label,.shiny-input-radiogroup .checkbox label,.shiny-input-radiogroup .radio label{margin-bottom:0}.form-check-inline{display:inline-flex;align-items:center;-webkit-align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input,.form-check-inline .shiny-input-checkboxgroup .checkbox label>input,.shiny-input-checkboxgroup .checkbox .form-check-inline label>input,.form-check-inline .shiny-input-checkboxgroup .radio label>input,.shiny-input-checkboxgroup .radio .form-check-inline label>input,.form-check-inline .shiny-input-radiogroup .checkbox label>input,.shiny-input-radiogroup .checkbox .form-check-inline label>input,.form-check-inline .shiny-input-radiogroup .radio label>input,.shiny-input-radiogroup .radio .form-check-inline label>input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#28a745}.valid-tooltip{position:absolute;top:100%;left:0;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(40,167,69,0.9);border-radius:.25rem}.form-row>.col>.valid-tooltip,.form-row>[class*="col-"]>.valid-tooltip{left:5px}.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip,.is-valid~.valid-feedback,.is-valid~.valid-tooltip{display:block}.was-validated .form-control:valid,.form-control.is-valid{border-color:#28a745;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.was-validated .form-control:valid:focus,.form-control.is-valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,0.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.was-validated .custom-select:valid,.custom-select.is-valid{border-color:#28a745;padding-right:calc(.75em + 2.3125rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") right .75rem center/8px 10px no-repeat,#fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem) no-repeat}.was-validated .custom-select:valid:focus,.custom-select.is-valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,0.25)}.was-validated .form-check-input:valid~.form-check-label,.was-validated .shiny-input-checkboxgroup .checkbox label>input:valid~.form-check-label,.shiny-input-checkboxgroup .checkbox .was-validated label>input:valid~.form-check-label,.was-validated .shiny-input-checkboxgroup .radio label>input:valid~.form-check-label,.shiny-input-checkboxgroup .radio .was-validated label>input:valid~.form-check-label,.was-validated .shiny-input-radiogroup .checkbox label>input:valid~.form-check-label,.shiny-input-radiogroup .checkbox .was-validated label>input:valid~.form-check-label,.was-validated .shiny-input-radiogroup .radio label>input:valid~.form-check-label,.shiny-input-radiogroup .radio .was-validated label>input:valid~.form-check-label,.was-validated .shiny-input-checkboxgroup .checkbox .form-check-input:valid~label,.was-validated .shiny-input-checkboxgroup .checkbox label>input:valid~label,.was-validated .shiny-input-checkboxgroup .checkbox .radio label>input:valid~label,.was-validated .shiny-input-checkboxgroup .radio .checkbox label>input:valid~label,.was-validated .shiny-input-checkboxgroup .shiny-input-radiogroup .checkbox label>input:valid~label,.shiny-input-radiogroup .was-validated .shiny-input-checkboxgroup .checkbox label>input:valid~label,.was-validated .shiny-input-checkboxgroup .checkbox .shiny-input-radiogroup .radio label>input:valid~label,.shiny-input-radiogroup .radio .was-validated .shiny-input-checkboxgroup .checkbox label>input:valid~label,.shiny-input-checkboxgroup .checkbox .was-validated .form-check-input:valid~label,.shiny-input-checkboxgroup .checkbox .was-validated label>input:valid~label,.shiny-input-checkboxgroup .checkbox .was-validated .radio label>input:valid~label,.shiny-input-checkboxgroup .radio .checkbox .was-validated label>input:valid~label,.shiny-input-checkboxgroup .shiny-input-radiogroup .checkbox .was-validated label>input:valid~label,.shiny-input-radiogroup .shiny-input-checkboxgroup .checkbox .was-validated label>input:valid~label,.shiny-input-checkboxgroup .checkbox .was-validated .shiny-input-radiogroup .radio label>input:valid~label,.shiny-input-radiogroup .radio .shiny-input-checkboxgroup .checkbox .was-validated label>input:valid~label,.was-validated .shiny-input-checkboxgroup .radio .form-check-input:valid~label,.was-validated .shiny-input-checkboxgroup .radio .checkbox label>input:valid~label,.was-validated .shiny-input-checkboxgroup .checkbox .radio label>input:valid~label,.was-validated .shiny-input-checkboxgroup .radio label>input:valid~label,.was-validated .shiny-input-checkboxgroup .radio .shiny-input-radiogroup .checkbox label>input:valid~label,.shiny-input-radiogroup .checkbox .was-validated .shiny-input-checkboxgroup .radio label>input:valid~label,.was-validated .shiny-input-checkboxgroup .shiny-input-radiogroup .radio label>input:valid~label,.shiny-input-radiogroup .was-validated .shiny-input-checkboxgroup .radio label>input:valid~label,.shiny-input-checkboxgroup .radio .was-validated .form-check-input:valid~label,.shiny-input-checkboxgroup .radio .was-validated .checkbox label>input:valid~label,.shiny-input-checkboxgroup .checkbox .radio .was-validated label>input:valid~label,.shiny-input-checkboxgroup .radio .was-validated label>input:valid~label,.shiny-input-checkboxgroup .radio .was-validated .shiny-input-radiogroup .checkbox label>input:valid~label,.shiny-input-radiogroup .checkbox .shiny-input-checkboxgroup .radio .was-validated label>input:valid~label,.shiny-input-checkboxgroup .shiny-input-radiogroup .radio .was-validated label>input:valid~label,.shiny-input-radiogroup .shiny-input-checkboxgroup .radio .was-validated label>input:valid~label,.was-validated .shiny-input-radiogroup .checkbox .form-check-input:valid~label,.was-validated .shiny-input-radiogroup .shiny-input-checkboxgroup .checkbox label>input:valid~label,.shiny-input-checkboxgroup .was-validated .shiny-input-radiogroup .checkbox label>input:valid~label,.was-validated .shiny-input-radiogroup .checkbox .shiny-input-checkboxgroup .radio label>input:valid~label,.shiny-input-checkboxgroup .radio .was-validated .shiny-input-radiogroup .checkbox label>input:valid~label,.was-validated .shiny-input-radiogroup .checkbox label>input:valid~label,.was-validated .shiny-input-radiogroup .checkbox .radio label>input:valid~label,.was-validated .shiny-input-radiogroup .radio .checkbox label>input:valid~label,.shiny-input-radiogroup .checkbox .was-validated .form-check-input:valid~label,.shiny-input-radiogroup .shiny-input-checkboxgroup .checkbox .was-validated label>input:valid~label,.shiny-input-checkboxgroup .shiny-input-radiogroup .checkbox .was-validated label>input:valid~label,.shiny-input-radiogroup .checkbox .was-validated .shiny-input-checkboxgroup .radio label>input:valid~label,.shiny-input-checkboxgroup .radio .shiny-input-radiogroup .checkbox .was-validated label>input:valid~label,.shiny-input-radiogroup .checkbox .was-validated label>input:valid~label,.shiny-input-radiogroup .checkbox .was-validated .radio label>input:valid~label,.shiny-input-radiogroup .radio .checkbox .was-validated label>input:valid~label,.was-validated .shiny-input-radiogroup .radio .form-check-input:valid~label,.was-validated .shiny-input-radiogroup .radio .shiny-input-checkboxgroup .checkbox label>input:valid~label,.shiny-input-checkboxgroup .checkbox .was-validated .shiny-input-radiogroup .radio label>input:valid~label,.was-validated .shiny-input-radiogroup .shiny-input-checkboxgroup .radio label>input:valid~label,.shiny-input-checkboxgroup .was-validated .shiny-input-radiogroup .radio label>input:valid~label,.was-validated .shiny-input-radiogroup .radio .checkbox label>input:valid~label,.was-validated .shiny-input-radiogroup .checkbox .radio label>input:valid~label,.was-validated .shiny-input-radiogroup .radio label>input:valid~label,.shiny-input-radiogroup .radio .was-validated .form-check-input:valid~label,.shiny-input-radiogroup .radio .was-validated .shiny-input-checkboxgroup .checkbox label>input:valid~label,.shiny-input-checkboxgroup .checkbox .shiny-input-radiogroup .radio .was-validated label>input:valid~label,.shiny-input-radiogroup .shiny-input-checkboxgroup .radio .was-validated label>input:valid~label,.shiny-input-checkboxgroup .shiny-input-radiogroup .radio .was-validated label>input:valid~label,.shiny-input-radiogroup .radio .was-validated .checkbox label>input:valid~label,.shiny-input-radiogroup .checkbox .radio .was-validated label>input:valid~label,.shiny-input-radiogroup .radio .was-validated label>input:valid~label,.form-check-input.is-valid~.form-check-label,.shiny-input-checkboxgroup .checkbox label>input.is-valid~.form-check-label,.shiny-input-checkboxgroup .radio label>input.is-valid~.form-check-label,.shiny-input-radiogroup .checkbox label>input.is-valid~.form-check-label,.shiny-input-radiogroup .radio label>input.is-valid~.form-check-label,.shiny-input-checkboxgroup .checkbox .form-check-input.is-valid~label,.shiny-input-checkboxgroup .checkbox label>input.is-valid~label,.shiny-input-checkboxgroup .checkbox .radio label>input.is-valid~label,.shiny-input-checkboxgroup .radio .checkbox label>input.is-valid~label,.shiny-input-checkboxgroup .shiny-input-radiogroup .checkbox label>input.is-valid~label,.shiny-input-radiogroup .shiny-input-checkboxgroup .checkbox label>input.is-valid~label,.shiny-input-checkboxgroup .checkbox .shiny-input-radiogroup .radio label>input.is-valid~label,.shiny-input-radiogroup .radio .shiny-input-checkboxgroup .checkbox label>input.is-valid~label,.shiny-input-checkboxgroup .radio .form-check-input.is-valid~label,.shiny-input-checkboxgroup .radio .checkbox label>input.is-valid~label,.shiny-input-checkboxgroup .checkbox .radio label>input.is-valid~label,.shiny-input-checkboxgroup .radio label>input.is-valid~label,.shiny-input-checkboxgroup .radio .shiny-input-radiogroup .checkbox label>input.is-valid~label,.shiny-input-radiogroup .checkbox .shiny-input-checkboxgroup .radio label>input.is-valid~label,.shiny-input-checkboxgroup .shiny-input-radiogroup .radio label>input.is-valid~label,.shiny-input-radiogroup .shiny-input-checkboxgroup .radio label>input.is-valid~label,.shiny-input-radiogroup .checkbox .form-check-input.is-valid~label,.shiny-input-radiogroup .shiny-input-checkboxgroup .checkbox label>input.is-valid~label,.shiny-input-checkboxgroup .shiny-input-radiogroup .checkbox label>input.is-valid~label,.shiny-input-radiogroup .checkbox .shiny-input-checkboxgroup .radio label>input.is-valid~label,.shiny-input-checkboxgroup .radio .shiny-input-radiogroup .checkbox label>input.is-valid~label,.shiny-input-radiogroup .checkbox label>input.is-valid~label,.shiny-input-radiogroup .checkbox .radio label>input.is-valid~label,.shiny-input-radiogroup .radio .checkbox label>input.is-valid~label,.shiny-input-radiogroup .radio .form-check-input.is-valid~label,.shiny-input-radiogroup .radio .shiny-input-checkboxgroup .checkbox label>input.is-valid~label,.shiny-input-checkboxgroup .checkbox .shiny-input-radiogroup .radio label>input.is-valid~label,.shiny-input-radiogroup .shiny-input-checkboxgroup .radio label>input.is-valid~label,.shiny-input-checkboxgroup .shiny-input-radiogroup .radio label>input.is-valid~label,.shiny-input-radiogroup .radio .checkbox label>input.is-valid~label,.shiny-input-radiogroup .checkbox .radio label>input.is-valid~label,.shiny-input-radiogroup .radio label>input.is-valid~label{color:#28a745}.was-validated .form-check-input:valid~.valid-feedback,.was-validated .shiny-input-checkboxgroup .checkbox label>input:valid~.valid-feedback,.shiny-input-checkboxgroup .checkbox .was-validated label>input:valid~.valid-feedback,.was-validated .shiny-input-checkboxgroup .radio label>input:valid~.valid-feedback,.shiny-input-checkboxgroup .radio .was-validated label>input:valid~.valid-feedback,.was-validated .shiny-input-radiogroup .checkbox label>input:valid~.valid-feedback,.shiny-input-radiogroup .checkbox .was-validated label>input:valid~.valid-feedback,.was-validated .shiny-input-radiogroup .radio label>input:valid~.valid-feedback,.shiny-input-radiogroup .radio .was-validated label>input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip,.was-validated .shiny-input-checkboxgroup .checkbox label>input:valid~.valid-tooltip,.shiny-input-checkboxgroup .checkbox .was-validated label>input:valid~.valid-tooltip,.was-validated .shiny-input-checkboxgroup .radio label>input:valid~.valid-tooltip,.shiny-input-checkboxgroup .radio .was-validated label>input:valid~.valid-tooltip,.was-validated .shiny-input-radiogroup .checkbox label>input:valid~.valid-tooltip,.shiny-input-radiogroup .checkbox .was-validated label>input:valid~.valid-tooltip,.was-validated .shiny-input-radiogroup .radio label>input:valid~.valid-tooltip,.shiny-input-radiogroup .radio .was-validated label>input:valid~.valid-tooltip,.form-check-input.is-valid~.valid-feedback,.shiny-input-checkboxgroup .checkbox label>input.is-valid~.valid-feedback,.shiny-input-checkboxgroup .radio label>input.is-valid~.valid-feedback,.shiny-input-radiogroup .checkbox label>input.is-valid~.valid-feedback,.shiny-input-radiogroup .radio label>input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.shiny-input-checkboxgroup .checkbox label>input.is-valid~.valid-tooltip,.shiny-input-checkboxgroup .radio label>input.is-valid~.valid-tooltip,.shiny-input-radiogroup .checkbox label>input.is-valid~.valid-tooltip,.shiny-input-radiogroup .radio label>input.is-valid~.valid-tooltip{display:block}.was-validated .custom-control-input:valid~.custom-control-label,.custom-control-input.is-valid~.custom-control-label{color:#28a745}.was-validated .custom-control-input:valid~.custom-control-label::before,.custom-control-input.is-valid~.custom-control-label::before{border-color:#28a745}.was-validated .custom-control-input:valid:checked~.custom-control-label::before,.custom-control-input.is-valid:checked~.custom-control-label::before{border-color:#34ce57;background-color:#34ce57}.was-validated .custom-control-input:valid:focus~.custom-control-label::before,.custom-control-input.is-valid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(40,167,69,0.25)}.was-validated .custom-control-input:valid:focus:not(:checked)~.custom-control-label::before,.custom-control-input.is-valid:focus:not(:checked)~.custom-control-label::before{border-color:#28a745}.was-validated .custom-file-input:valid~.custom-file-label,.custom-file-input.is-valid~.custom-file-label{border-color:#28a745}.was-validated .custom-file-input:valid:focus~.custom-file-label,.custom-file-input.is-valid:focus~.custom-file-label{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,0.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;left:0;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(220,53,69,0.9);border-radius:.25rem}.form-row>.col>.invalid-tooltip,.form-row>[class*="col-"]>.invalid-tooltip{left:5px}.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip,.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip{display:block}.was-validated .form-control:invalid,.form-control.is-invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.was-validated .form-control:invalid:focus,.form-control.is-invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,0.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.was-validated .custom-select:invalid,.custom-select.is-invalid{border-color:#dc3545;padding-right:calc(.75em + 2.3125rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") right .75rem center/8px 10px no-repeat,#fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e") center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem) no-repeat}.was-validated .custom-select:invalid:focus,.custom-select.is-invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,0.25)}.was-validated .form-check-input:invalid~.form-check-label,.was-validated .shiny-input-checkboxgroup .checkbox label>input:invalid~.form-check-label,.shiny-input-checkboxgroup .checkbox .was-validated label>input:invalid~.form-check-label,.was-validated .shiny-input-checkboxgroup .radio label>input:invalid~.form-check-label,.shiny-input-checkboxgroup .radio .was-validated label>input:invalid~.form-check-label,.was-validated .shiny-input-radiogroup .checkbox label>input:invalid~.form-check-label,.shiny-input-radiogroup .checkbox .was-validated label>input:invalid~.form-check-label,.was-validated .shiny-input-radiogroup .radio label>input:invalid~.form-check-label,.shiny-input-radiogroup .radio .was-validated label>input:invalid~.form-check-label,.was-validated .shiny-input-checkboxgroup .checkbox .form-check-input:invalid~label,.was-validated .shiny-input-checkboxgroup .checkbox label>input:invalid~label,.was-validated .shiny-input-checkboxgroup .checkbox .radio label>input:invalid~label,.was-validated .shiny-input-checkboxgroup .radio .checkbox label>input:invalid~label,.was-validated .shiny-input-checkboxgroup .shiny-input-radiogroup .checkbox label>input:invalid~label,.shiny-input-radiogroup .was-validated .shiny-input-checkboxgroup .checkbox label>input:invalid~label,.was-validated .shiny-input-checkboxgroup .checkbox .shiny-input-radiogroup .radio label>input:invalid~label,.shiny-input-radiogroup .radio .was-validated .shiny-input-checkboxgroup .checkbox label>input:invalid~label,.shiny-input-checkboxgroup .checkbox .was-validated .form-check-input:invalid~label,.shiny-input-checkboxgroup .checkbox .was-validated label>input:invalid~label,.shiny-input-checkboxgroup .checkbox .was-validated .radio label>input:invalid~label,.shiny-input-checkboxgroup .radio .checkbox .was-validated label>input:invalid~label,.shiny-input-checkboxgroup .shiny-input-radiogroup .checkbox .was-validated label>input:invalid~label,.shiny-input-radiogroup .shiny-input-checkboxgroup .checkbox .was-validated label>input:invalid~label,.shiny-input-checkboxgroup .checkbox .was-validated .shiny-input-radiogroup .radio label>input:invalid~label,.shiny-input-radiogroup .radio .shiny-input-checkboxgroup .checkbox .was-validated label>input:invalid~label,.was-validated .shiny-input-checkboxgroup .radio .form-check-input:invalid~label,.was-validated .shiny-input-checkboxgroup .radio .checkbox label>input:invalid~label,.was-validated .shiny-input-checkboxgroup .checkbox .radio label>input:invalid~label,.was-validated .shiny-input-checkboxgroup .radio label>input:invalid~label,.was-validated .shiny-input-checkboxgroup .radio .shiny-input-radiogroup .checkbox label>input:invalid~label,.shiny-input-radiogroup .checkbox .was-validated .shiny-input-checkboxgroup .radio label>input:invalid~label,.was-validated .shiny-input-checkboxgroup .shiny-input-radiogroup .radio label>input:invalid~label,.shiny-input-radiogroup .was-validated .shiny-input-checkboxgroup .radio label>input:invalid~label,.shiny-input-checkboxgroup .radio .was-validated .form-check-input:invalid~label,.shiny-input-checkboxgroup .radio .was-validated .checkbox label>input:invalid~label,.shiny-input-checkboxgroup .checkbox .radio .was-validated label>input:invalid~label,.shiny-input-checkboxgroup .radio .was-validated label>input:invalid~label,.shiny-input-checkboxgroup .radio .was-validated .shiny-input-radiogroup .checkbox label>input:invalid~label,.shiny-input-radiogroup .checkbox .shiny-input-checkboxgroup .radio .was-validated label>input:invalid~label,.shiny-input-checkboxgroup .shiny-input-radiogroup .radio .was-validated label>input:invalid~label,.shiny-input-radiogroup .shiny-input-checkboxgroup .radio .was-validated label>input:invalid~label,.was-validated .shiny-input-radiogroup .checkbox .form-check-input:invalid~label,.was-validated .shiny-input-radiogroup .shiny-input-checkboxgroup .checkbox label>input:invalid~label,.shiny-input-checkboxgroup .was-validated .shiny-input-radiogroup .checkbox label>input:invalid~label,.was-validated .shiny-input-radiogroup .checkbox .shiny-input-checkboxgroup .radio label>input:invalid~label,.shiny-input-checkboxgroup .radio .was-validated .shiny-input-radiogroup .checkbox label>input:invalid~label,.was-validated .shiny-input-radiogroup .checkbox label>input:invalid~label,.was-validated .shiny-input-radiogroup .checkbox .radio label>input:invalid~label,.was-validated .shiny-input-radiogroup .radio .checkbox label>input:invalid~label,.shiny-input-radiogroup .checkbox .was-validated .form-check-input:invalid~label,.shiny-input-radiogroup .shiny-input-checkboxgroup .checkbox .was-validated label>input:invalid~label,.shiny-input-checkboxgroup .shiny-input-radiogroup .checkbox .was-validated label>input:invalid~label,.shiny-input-radiogroup .checkbox .was-validated .shiny-input-checkboxgroup .radio label>input:invalid~label,.shiny-input-checkboxgroup .radio .shiny-input-radiogroup .checkbox .was-validated label>input:invalid~label,.shiny-input-radiogroup .checkbox .was-validated label>input:invalid~label,.shiny-input-radiogroup .checkbox .was-validated .radio label>input:invalid~label,.shiny-input-radiogroup .radio .checkbox .was-validated label>input:invalid~label,.was-validated .shiny-input-radiogroup .radio .form-check-input:invalid~label,.was-validated .shiny-input-radiogroup .radio .shiny-input-checkboxgroup .checkbox label>input:invalid~label,.shiny-input-checkboxgroup .checkbox .was-validated .shiny-input-radiogroup .radio label>input:invalid~label,.was-validated .shiny-input-radiogroup .shiny-input-checkboxgroup .radio label>input:invalid~label,.shiny-input-checkboxgroup .was-validated .shiny-input-radiogroup .radio label>input:invalid~label,.was-validated .shiny-input-radiogroup .radio .checkbox label>input:invalid~label,.was-validated .shiny-input-radiogroup .checkbox .radio label>input:invalid~label,.was-validated .shiny-input-radiogroup .radio label>input:invalid~label,.shiny-input-radiogroup .radio .was-validated .form-check-input:invalid~label,.shiny-input-radiogroup .radio .was-validated .shiny-input-checkboxgroup .checkbox label>input:invalid~label,.shiny-input-checkboxgroup .checkbox .shiny-input-radiogroup .radio .was-validated label>input:invalid~label,.shiny-input-radiogroup .shiny-input-checkboxgroup .radio .was-validated label>input:invalid~label,.shiny-input-checkboxgroup .shiny-input-radiogroup .radio .was-validated label>input:invalid~label,.shiny-input-radiogroup .radio .was-validated .checkbox label>input:invalid~label,.shiny-input-radiogroup .checkbox .radio .was-validated label>input:invalid~label,.shiny-input-radiogroup .radio .was-validated label>input:invalid~label,.form-check-input.is-invalid~.form-check-label,.shiny-input-checkboxgroup .checkbox label>input.is-invalid~.form-check-label,.shiny-input-checkboxgroup .radio label>input.is-invalid~.form-check-label,.shiny-input-radiogroup .checkbox label>input.is-invalid~.form-check-label,.shiny-input-radiogroup .radio label>input.is-invalid~.form-check-label,.shiny-input-checkboxgroup .checkbox .form-check-input.is-invalid~label,.shiny-input-checkboxgroup .checkbox label>input.is-invalid~label,.shiny-input-checkboxgroup .checkbox .radio label>input.is-invalid~label,.shiny-input-checkboxgroup .radio .checkbox label>input.is-invalid~label,.shiny-input-checkboxgroup .shiny-input-radiogroup .checkbox label>input.is-invalid~label,.shiny-input-radiogroup .shiny-input-checkboxgroup .checkbox label>input.is-invalid~label,.shiny-input-checkboxgroup .checkbox .shiny-input-radiogroup .radio label>input.is-invalid~label,.shiny-input-radiogroup .radio .shiny-input-checkboxgroup .checkbox label>input.is-invalid~label,.shiny-input-checkboxgroup .radio .form-check-input.is-invalid~label,.shiny-input-checkboxgroup .radio .checkbox label>input.is-invalid~label,.shiny-input-checkboxgroup .checkbox .radio label>input.is-invalid~label,.shiny-input-checkboxgroup .radio label>input.is-invalid~label,.shiny-input-checkboxgroup .radio .shiny-input-radiogroup .checkbox label>input.is-invalid~label,.shiny-input-radiogroup .checkbox .shiny-input-checkboxgroup .radio label>input.is-invalid~label,.shiny-input-checkboxgroup .shiny-input-radiogroup .radio label>input.is-invalid~label,.shiny-input-radiogroup .shiny-input-checkboxgroup .radio label>input.is-invalid~label,.shiny-input-radiogroup .checkbox .form-check-input.is-invalid~label,.shiny-input-radiogroup .shiny-input-checkboxgroup .checkbox label>input.is-invalid~label,.shiny-input-checkboxgroup .shiny-input-radiogroup .checkbox label>input.is-invalid~label,.shiny-input-radiogroup .checkbox .shiny-input-checkboxgroup .radio label>input.is-invalid~label,.shiny-input-checkboxgroup .radio .shiny-input-radiogroup .checkbox label>input.is-invalid~label,.shiny-input-radiogroup .checkbox label>input.is-invalid~label,.shiny-input-radiogroup .checkbox .radio label>input.is-invalid~label,.shiny-input-radiogroup .radio .checkbox label>input.is-invalid~label,.shiny-input-radiogroup .radio .form-check-input.is-invalid~label,.shiny-input-radiogroup .radio .shiny-input-checkboxgroup .checkbox label>input.is-invalid~label,.shiny-input-checkboxgroup .checkbox .shiny-input-radiogroup .radio label>input.is-invalid~label,.shiny-input-radiogroup .shiny-input-checkboxgroup .radio label>input.is-invalid~label,.shiny-input-checkboxgroup .shiny-input-radiogroup .radio label>input.is-invalid~label,.shiny-input-radiogroup .radio .checkbox label>input.is-invalid~label,.shiny-input-radiogroup .checkbox .radio label>input.is-invalid~label,.shiny-input-radiogroup .radio label>input.is-invalid~label{color:#dc3545}.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .shiny-input-checkboxgroup .checkbox label>input:invalid~.invalid-feedback,.shiny-input-checkboxgroup .checkbox .was-validated label>input:invalid~.invalid-feedback,.was-validated .shiny-input-checkboxgroup .radio label>input:invalid~.invalid-feedback,.shiny-input-checkboxgroup .radio .was-validated label>input:invalid~.invalid-feedback,.was-validated .shiny-input-radiogroup .checkbox label>input:invalid~.invalid-feedback,.shiny-input-radiogroup .checkbox .was-validated label>input:invalid~.invalid-feedback,.was-validated .shiny-input-radiogroup .radio label>input:invalid~.invalid-feedback,.shiny-input-radiogroup .radio .was-validated label>input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip,.was-validated .shiny-input-checkboxgroup .checkbox label>input:invalid~.invalid-tooltip,.shiny-input-checkboxgroup .checkbox .was-validated label>input:invalid~.invalid-tooltip,.was-validated .shiny-input-checkboxgroup .radio label>input:invalid~.invalid-tooltip,.shiny-input-checkboxgroup .radio .was-validated label>input:invalid~.invalid-tooltip,.was-validated .shiny-input-radiogroup .checkbox label>input:invalid~.invalid-tooltip,.shiny-input-radiogroup .checkbox .was-validated label>input:invalid~.invalid-tooltip,.was-validated .shiny-input-radiogroup .radio label>input:invalid~.invalid-tooltip,.shiny-input-radiogroup .radio .was-validated label>input:invalid~.invalid-tooltip,.form-check-input.is-invalid~.invalid-feedback,.shiny-input-checkboxgroup .checkbox label>input.is-invalid~.invalid-feedback,.shiny-input-checkboxgroup .radio label>input.is-invalid~.invalid-feedback,.shiny-input-radiogroup .checkbox label>input.is-invalid~.invalid-feedback,.shiny-input-radiogroup .radio label>input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.shiny-input-checkboxgroup .checkbox label>input.is-invalid~.invalid-tooltip,.shiny-input-checkboxgroup .radio label>input.is-invalid~.invalid-tooltip,.shiny-input-radiogroup .checkbox label>input.is-invalid~.invalid-tooltip,.shiny-input-radiogroup .radio label>input.is-invalid~.invalid-tooltip{display:block}.was-validated .custom-control-input:invalid~.custom-control-label,.custom-control-input.is-invalid~.custom-control-label{color:#dc3545}.was-validated .custom-control-input:invalid~.custom-control-label::before,.custom-control-input.is-invalid~.custom-control-label::before{border-color:#dc3545}.was-validated .custom-control-input:invalid:checked~.custom-control-label::before,.custom-control-input.is-invalid:checked~.custom-control-label::before{border-color:#e4606d;background-color:#e4606d}.was-validated .custom-control-input:invalid:focus~.custom-control-label::before,.custom-control-input.is-invalid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(220,53,69,0.25)}.was-validated .custom-control-input:invalid:focus:not(:checked)~.custom-control-label::before,.custom-control-input.is-invalid:focus:not(:checked)~.custom-control-label::before{border-color:#dc3545}.was-validated .custom-file-input:invalid~.custom-file-label,.custom-file-input.is-invalid~.custom-file-label{border-color:#dc3545}.was-validated .custom-file-input:invalid:focus~.custom-file-label,.custom-file-input.is-invalid:focus~.custom-file-label{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,0.25)}.form-inline{display:flex;display:-webkit-flex;flex-flow:row wrap;-webkit-flex-flow:row wrap;align-items:center;-webkit-align-items:center}.form-inline .form-check,.form-inline .shiny-input-checkboxgroup .checkbox,.shiny-input-checkboxgroup .form-inline .checkbox,.form-inline .shiny-input-checkboxgroup .radio,.shiny-input-checkboxgroup .form-inline .radio,.form-inline .shiny-input-radiogroup .checkbox,.shiny-input-radiogroup .form-inline .checkbox,.form-inline .shiny-input-radiogroup .radio,.shiny-input-radiogroup .form-inline .radio{width:100%}@media (min-width: 576px){.form-inline label{display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;justify-content:center;-webkit-justify-content:center;margin-bottom:0}.form-inline .form-group{display:flex;display:-webkit-flex;flex:0 0 auto;-webkit-flex:0 0 auto;flex-flow:row wrap;-webkit-flex-flow:row wrap;align-items:center;-webkit-align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .input-group,.form-inline .custom-select{width:auto}.form-inline .form-check,.form-inline .shiny-input-checkboxgroup .checkbox,.shiny-input-checkboxgroup .form-inline .checkbox,.form-inline .shiny-input-checkboxgroup .radio,.shiny-input-checkboxgroup .form-inline .radio,.form-inline .shiny-input-radiogroup .checkbox,.shiny-input-radiogroup .form-inline .checkbox,.form-inline .shiny-input-radiogroup .radio,.shiny-input-radiogroup .form-inline .radio{display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;justify-content:center;-webkit-justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input,.form-inline .shiny-input-checkboxgroup .checkbox label>input,.shiny-input-checkboxgroup .checkbox .form-inline label>input,.form-inline .shiny-input-checkboxgroup .radio label>input,.shiny-input-checkboxgroup .radio .form-inline label>input,.form-inline .shiny-input-radiogroup .checkbox label>input,.shiny-input-radiogroup .checkbox .form-inline label>input,.form-inline .shiny-input-radiogroup .radio label>input,.shiny-input-radiogroup .radio .form-inline label>input{position:relative;flex-shrink:0;-webkit-flex-shrink:0;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{align-items:center;-webkit-align-items:center;justify-content:center;-webkit-justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#212529;text-align:center;vertical-align:middle;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.btn{transition:none}}.btn:hover{color:#212529;text-decoration:none}.btn:focus,.btn.focus{outline:0;box-shadow:0 0 0 .2rem rgba(30,144,255,0.25)}.btn.disabled,.btn:disabled{opacity:.65}.btn:not(:disabled):not(.disabled){cursor:pointer}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-default{color:#000;background-color:#dee2e6;border-color:#dee2e6}.btn-default:hover{color:#000;background-color:#c8cfd6;border-color:#c1c9d0}.btn-default:focus,.btn-default.focus{color:#000;background-color:#c8cfd6;border-color:#c1c9d0;box-shadow:0 0 0 .2rem rgba(189,192,196,0.5)}.btn-default.disabled,.btn-default:disabled{color:#000;background-color:#dee2e6;border-color:#dee2e6}.btn-default:not(:disabled):not(.disabled):active,.btn-default:not(:disabled):not(.disabled).active,.show>.btn-default.dropdown-toggle,.in>.btn-default.dropdown-toggle{color:#000;background-color:#c1c9d0;border-color:#bac2cb}.btn-default:not(:disabled):not(.disabled):active:focus,.btn-default:not(:disabled):not(.disabled).active:focus,.show>.btn-default.dropdown-toggle:focus,.in>.btn-default.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(189,192,196,0.5)}.btn-primary{color:#fff;background-color:#1e90ff;border-color:#1e90ff}.btn-primary:hover{color:#fff;background-color:#007df7;border-color:#0077ea}.btn-primary:focus,.btn-primary.focus{color:#fff;background-color:#007df7;border-color:#0077ea;box-shadow:0 0 0 .2rem rgba(64,161,255,0.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#1e90ff;border-color:#1e90ff}.btn-primary:not(:disabled):not(.disabled):active,.btn-primary:not(:disabled):not(.disabled).active,.show>.btn-primary.dropdown-toggle,.in>.btn-primary.dropdown-toggle{color:#fff;background-color:#0077ea;border-color:#0070dd}.btn-primary:not(:disabled):not(.disabled):active:focus,.btn-primary:not(:disabled):not(.disabled).active:focus,.show>.btn-primary.dropdown-toggle:focus,.in>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(64,161,255,0.5)}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5a6268;border-color:#545b62}.btn-secondary:focus,.btn-secondary.focus{color:#fff;background-color:#5a6268;border-color:#545b62;box-shadow:0 0 0 .2rem rgba(130,138,145,0.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:not(:disabled):not(.disabled):active,.btn-secondary:not(:disabled):not(.disabled).active,.show>.btn-secondary.dropdown-toggle,.in>.btn-secondary.dropdown-toggle{color:#fff;background-color:#545b62;border-color:#4e555b}.btn-secondary:not(:disabled):not(.disabled):active:focus,.btn-secondary:not(:disabled):not(.disabled).active:focus,.show>.btn-secondary.dropdown-toggle:focus,.in>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(130,138,145,0.5)}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{color:#fff;background-color:#218838;border-color:#1e7e34}.btn-success:focus,.btn-success.focus{color:#fff;background-color:#218838;border-color:#1e7e34;box-shadow:0 0 0 .2rem rgba(72,180,97,0.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:not(:disabled):not(.disabled):active,.btn-success:not(:disabled):not(.disabled).active,.show>.btn-success.dropdown-toggle,.in>.btn-success.dropdown-toggle{color:#fff;background-color:#1e7e34;border-color:#1c7430}.btn-success:not(:disabled):not(.disabled):active:focus,.btn-success:not(:disabled):not(.disabled).active:focus,.show>.btn-success.dropdown-toggle:focus,.in>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(72,180,97,0.5)}.btn-info{color:#000;background-color:#0DCAF0;border-color:#0DCAF0}.btn-info:hover{color:#000;background-color:#0babcc;border-color:#0aa1c0}.btn-info:focus,.btn-info.focus{color:#000;background-color:#0babcc;border-color:#0aa1c0;box-shadow:0 0 0 .2rem rgba(11,172,204,0.5)}.btn-info.disabled,.btn-info:disabled{color:#000;background-color:#0DCAF0;border-color:#0DCAF0}.btn-info:not(:disabled):not(.disabled):active,.btn-info:not(:disabled):not(.disabled).active,.show>.btn-info.dropdown-toggle,.in>.btn-info.dropdown-toggle{color:#fff;background-color:#0aa1c0;border-color:#0a97b4}.btn-info:not(:disabled):not(.disabled):active:focus,.btn-info:not(:disabled):not(.disabled).active:focus,.show>.btn-info.dropdown-toggle:focus,.in>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(11,172,204,0.5)}.btn-warning{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#000;background-color:#e0a800;border-color:#d39e00}.btn-warning:focus,.btn-warning.focus{color:#000;background-color:#e0a800;border-color:#d39e00;box-shadow:0 0 0 .2rem rgba(217,164,6,0.5)}.btn-warning.disabled,.btn-warning:disabled{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled):active,.btn-warning:not(:disabled):not(.disabled).active,.show>.btn-warning.dropdown-toggle,.in>.btn-warning.dropdown-toggle{color:#000;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled):active:focus,.btn-warning:not(:disabled):not(.disabled).active:focus,.show>.btn-warning.dropdown-toggle:focus,.in>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(217,164,6,0.5)}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#c82333;border-color:#bd2130}.btn-danger:focus,.btn-danger.focus{color:#fff;background-color:#c82333;border-color:#bd2130;box-shadow:0 0 0 .2rem rgba(225,83,97,0.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:not(:disabled):not(.disabled):active,.btn-danger:not(:disabled):not(.disabled).active,.show>.btn-danger.dropdown-toggle,.in>.btn-danger.dropdown-toggle{color:#fff;background-color:#bd2130;border-color:#b21f2d}.btn-danger:not(:disabled):not(.disabled):active:focus,.btn-danger:not(:disabled):not(.disabled).active:focus,.show>.btn-danger.dropdown-toggle:focus,.in>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(225,83,97,0.5)}.btn-light{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#000;background-color:#e2e6ea;border-color:#dae0e5}.btn-light:focus,.btn-light.focus{color:#000;background-color:#e2e6ea;border-color:#dae0e5;box-shadow:0 0 0 .2rem rgba(211,212,213,0.5)}.btn-light.disabled,.btn-light:disabled{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:not(:disabled):not(.disabled):active,.btn-light:not(:disabled):not(.disabled).active,.show>.btn-light.dropdown-toggle,.in>.btn-light.dropdown-toggle{color:#000;background-color:#dae0e5;border-color:#d3d9df}.btn-light:not(:disabled):not(.disabled):active:focus,.btn-light:not(:disabled):not(.disabled).active:focus,.show>.btn-light.dropdown-toggle:focus,.in>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(211,212,213,0.5)}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark:focus,.btn-dark.focus{color:#fff;background-color:#23272b;border-color:#1d2124;box-shadow:0 0 0 .2rem rgba(82,88,93,0.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled):active,.btn-dark:not(:disabled):not(.disabled).active,.show>.btn-dark.dropdown-toggle,.in>.btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled):active:focus,.btn-dark:not(:disabled):not(.disabled).active:focus,.show>.btn-dark.dropdown-toggle:focus,.in>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(82,88,93,0.5)}.btn-outline-default{color:#dee2e6;border-color:#dee2e6;background-color:transparent}.btn-outline-default:hover{color:#000;background-color:#dee2e6;border-color:#dee2e6}.btn-outline-default:focus,.btn-outline-default.focus{box-shadow:0 0 0 .2rem rgba(222,226,230,0.5)}.btn-outline-default.disabled,.btn-outline-default:disabled{color:#dee2e6;background-color:transparent}.btn-outline-default:not(:disabled):not(.disabled):active,.btn-outline-default:not(:disabled):not(.disabled).active,.show>.btn-outline-default.dropdown-toggle,.in>.btn-outline-default.dropdown-toggle{color:#000;background-color:#dee2e6;border-color:#dee2e6}.btn-outline-default:not(:disabled):not(.disabled):active:focus,.btn-outline-default:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-default.dropdown-toggle:focus,.in>.btn-outline-default.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(222,226,230,0.5)}.btn-outline-primary{color:#1e90ff;border-color:#1e90ff;background-color:transparent}.btn-outline-primary:hover{color:#fff;background-color:#1e90ff;border-color:#1e90ff}.btn-outline-primary:focus,.btn-outline-primary.focus{box-shadow:0 0 0 .2rem rgba(30,144,255,0.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#1e90ff;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled):active,.btn-outline-primary:not(:disabled):not(.disabled).active,.show>.btn-outline-primary.dropdown-toggle,.in>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#1e90ff;border-color:#1e90ff}.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-primary.dropdown-toggle:focus,.in>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(30,144,255,0.5)}.btn-outline-secondary{color:#6c757d;border-color:#6c757d;background-color:transparent}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary:focus,.btn-outline-secondary.focus{box-shadow:0 0 0 .2rem rgba(108,117,125,0.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled):active,.btn-outline-secondary:not(:disabled):not(.disabled).active,.show>.btn-outline-secondary.dropdown-toggle,.in>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus,.in>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,0.5)}.btn-outline-success{color:#28a745;border-color:#28a745;background-color:transparent}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success:focus,.btn-outline-success.focus{box-shadow:0 0 0 .2rem rgba(40,167,69,0.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled):active,.btn-outline-success:not(:disabled):not(.disabled).active,.show>.btn-outline-success.dropdown-toggle,.in>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success:not(:disabled):not(.disabled):active:focus,.btn-outline-success:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-success.dropdown-toggle:focus,.in>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,0.5)}.btn-outline-info{color:#0DCAF0;border-color:#0DCAF0;background-color:transparent}.btn-outline-info:hover{color:#000;background-color:#0DCAF0;border-color:#0DCAF0}.btn-outline-info:focus,.btn-outline-info.focus{box-shadow:0 0 0 .2rem rgba(13,202,240,0.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#0DCAF0;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled):active,.btn-outline-info:not(:disabled):not(.disabled).active,.show>.btn-outline-info.dropdown-toggle,.in>.btn-outline-info.dropdown-toggle{color:#000;background-color:#0DCAF0;border-color:#0DCAF0}.btn-outline-info:not(:disabled):not(.disabled):active:focus,.btn-outline-info:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-info.dropdown-toggle:focus,.in>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(13,202,240,0.5)}.btn-outline-warning{color:#ffc107;border-color:#ffc107;background-color:transparent}.btn-outline-warning:hover{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:focus,.btn-outline-warning.focus{box-shadow:0 0 0 .2rem rgba(255,193,7,0.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled):active,.btn-outline-warning:not(:disabled):not(.disabled).active,.show>.btn-outline-warning.dropdown-toggle,.in>.btn-outline-warning.dropdown-toggle{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-warning.dropdown-toggle:focus,.in>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,0.5)}.btn-outline-danger{color:#dc3545;border-color:#dc3545;background-color:transparent}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:focus,.btn-outline-danger.focus{box-shadow:0 0 0 .2rem rgba(220,53,69,0.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled):active,.btn-outline-danger:not(:disabled):not(.disabled).active,.show>.btn-outline-danger.dropdown-toggle,.in>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-danger.dropdown-toggle:focus,.in>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,0.5)}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa;background-color:transparent}.btn-outline-light:hover{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:focus,.btn-outline-light.focus{box-shadow:0 0 0 .2rem rgba(248,249,250,0.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled):active,.btn-outline-light:not(:disabled):not(.disabled).active,.show>.btn-outline-light.dropdown-toggle,.in>.btn-outline-light.dropdown-toggle{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:not(:disabled):not(.disabled):active:focus,.btn-outline-light:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-light.dropdown-toggle:focus,.in>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,0.5)}.btn-outline-dark{color:#343a40;border-color:#343a40;background-color:transparent}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:focus,.btn-outline-dark.focus{box-shadow:0 0 0 .2rem rgba(52,58,64,0.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled):active,.btn-outline-dark:not(:disabled):not(.disabled).active,.show>.btn-outline-dark.dropdown-toggle,.in>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-dark.dropdown-toggle:focus,.in>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,0.5)}.btn-link{font-weight:400;color:#1e90ff;text-decoration:none;-webkit-text-decoration:none;-moz-text-decoration:none;-ms-text-decoration:none;-o-text-decoration:none}.btn-link:hover{color:#006ad1;text-decoration:underline;-webkit-text-decoration:underline;-moz-text-decoration:underline;-ms-text-decoration:underline;-o-text-decoration:underline}.btn-link:focus,.btn-link.focus{text-decoration:underline;-webkit-text-decoration:underline;-moz-text-decoration:underline;-ms-text-decoration:underline;-o-text-decoration:underline}.btn-link:disabled,.btn-link.disabled{color:#6c757d;pointer-events:none}.btn-lg,.btn-group-lg>.btn{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-sm,.btn-group-sm>.btn{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{transition:opacity 0.15s linear}@media (prefers-reduced-motion: reduce){.fade{transition:none}}.fade:not(.show):not(.in){opacity:0}.collapse:not(.show):not(.in){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height 0.35s ease}@media (prefers-reduced-motion: reduce){.collapsing{transition:none}}.dropup,.dropright,.dropdown,.dropleft{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,0.15);border-radius:.25rem}.dropdown-menu-left{right:auto;left:0}.dropdown-menu-right{right:0;left:auto}@media (min-width: 576px){.dropdown-menu-sm-left{right:auto;left:0}.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width: 768px){.dropdown-menu-md-left{right:auto;left:0}.dropdown-menu-md-right{right:0;left:auto}}@media (min-width: 992px){.dropdown-menu-lg-left{right:auto;left:0}.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width: 1200px){.dropdown-menu-xl-left{right:auto;left:0}.dropdown-menu-xl-right{right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^="top"],.dropdown-menu[x-placement^="right"],.dropdown-menu[x-placement^="bottom"],.dropdown-menu[x-placement^="left"]{right:auto;bottom:auto}.dropdown-divider,.dropdown-menu>li.divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item,.dropdown-menu>li>a{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:hover,.dropdown-menu>li>a:hover,.dropdown-item:focus,.dropdown-menu>li>a:focus{color:#16181b;text-decoration:none;background-color:#e9ecef}.dropdown-item.active,.dropdown-menu>li>a.active,.dropdown-item:active,.dropdown-menu>li>a:active{color:#fff;text-decoration:none;background-color:#1e90ff}.dropdown-item.disabled,.dropdown-menu>li>a.disabled,.dropdown-item:disabled,.dropdown-menu>li>a:disabled{color:#adb5bd;pointer-events:none;background-color:transparent}.dropdown-menu.show,.dropdown-menu.in{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1.5rem;color:#212529}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;flex:1 1 auto;-webkit-flex:1 1 auto}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover{z-index:1}.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn.active{z-index:1}.btn-toolbar{display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;justify-content:flex-start;-webkit-justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn:not(:first-child),.btn-group>.btn-group:not(:first-child){margin-left:-1px}.btn-group>.btn:not(:last-child):not(.dropdown-toggle),.btn-group>.btn-group:not(:last-child)>.btn{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:not(:first-child),.btn-group>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-sm+.dropdown-toggle-split,.btn-group-sm>.btn+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-lg+.dropdown-toggle-split,.btn-group-lg>.btn+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;-webkit-flex-direction:column;align-items:flex-start;-webkit-align-items:flex-start;justify-content:center;-webkit-justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn:not(:first-child),.btn-group-vertical>.btn-group:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle),.btn-group-vertical>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:not(:first-child),.btn-group-vertical>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type="radio"],.btn-group-toggle>.btn input[type="checkbox"],.btn-group-toggle>.btn-group>.btn input[type="radio"],.btn-group-toggle>.btn-group>.btn input[type="checkbox"]{position:absolute;clip:rect(0, 0, 0, 0);pointer-events:none}.input-group{position:relative;display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;align-items:stretch;-webkit-align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-control-plaintext,.input-group>.custom-select,.input-group>.custom-file{position:relative;flex:1 1 auto;-webkit-flex:1 1 auto;width:1%;min-width:0;margin-bottom:0}.input-group>.form-control+.form-control,.input-group>.form-control+.custom-select,.input-group>.form-control+.custom-file,.input-group>.form-control-plaintext+.form-control,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.custom-file,.input-group>.custom-select+.form-control,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.custom-file,.input-group>.custom-file+.form-control,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.custom-file{margin-left:-1px}.input-group>.form-control:focus,.input-group>.custom-select:focus,.input-group>.custom-file .custom-file-input:focus~.custom-file-label{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.form-control:not(:first-child),.input-group>.custom-select:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group:not(.has-validation)>.form-control:not(:last-child),.input-group:not(.has-validation)>.custom-select:not(:last-child),.input-group:not(.has-validation)>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.form-control:nth-last-child(n + 3),.input-group.has-validation>.custom-select:nth-last-child(n + 3),.input-group.has-validation>.custom-file:nth-last-child(n + 3) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group-prepend,.input-group-append{display:flex;display:-webkit-flex}.input-group-prepend .btn,.input-group-append .btn{position:relative;z-index:2}.input-group-prepend .btn:focus,.input-group-append .btn:focus{z-index:3}.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.input-group-text,.input-group-append .input-group-text+.btn{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-text input[type="radio"],.input-group-text input[type="checkbox"]{margin-top:0}.input-group-lg>.form-control:not(textarea),.input-group-lg>.custom-select{height:calc(1.5em + 1rem + 2px)}.input-group-lg>.form-control,.input-group-lg>.custom-select,.input-group-lg>.input-group-prepend>.input-group-text,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-append>.btn{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-sm>.form-control:not(textarea),.input-group-sm>.custom-select{height:calc(1.5em + .5rem + 2px)}.input-group-sm>.form-control,.input-group-sm>.custom-select,.input-group-sm>.input-group-prepend>.input-group-text,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-append>.btn{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text,.input-group:not(.has-validation)>.input-group-append:not(:last-child)>.btn,.input-group:not(.has-validation)>.input-group-append:not(:last-child)>.input-group-text,.input-group.has-validation>.input-group-append:nth-last-child(n + 3)>.btn,.input-group.has-validation>.input-group-append:nth-last-child(n + 3)>.input-group-text,.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;z-index:1;display:block;min-height:1.5rem;padding-left:1.5rem;color-adjust:exact;-webkit-print-color-adjust:exact}.custom-control-inline{display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;left:0;z-index:-1;width:1rem;height:1.25rem;opacity:0}.custom-control-input:checked~.custom-control-label::before{color:#fff;border-color:#1e90ff;background-color:#1e90ff}.custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(30,144,255,0.25)}.custom-control-input:focus:not(:checked)~.custom-control-label::before{border-color:#9ecfff}.custom-control-input:not(:disabled):active~.custom-control-label::before{color:#000;background-color:#d1e8ff;border-color:#d1e8ff}.custom-control-input[disabled]~.custom-control-label,.custom-control-input:disabled~.custom-control-label{color:#6c757d}.custom-control-input[disabled]~.custom-control-label::before,.custom-control-input:disabled~.custom-control-label::before{background-color:#e9ecef}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label::before{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";background-color:#fff;border:#adb5bd solid 1px}.custom-control-label::after{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background:50% / 50% 50% no-repeat}.custom-checkbox .custom-control-label::before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::before{border-color:#1e90ff;background-color:#1e90ff}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(30,144,255,0.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label::before{background-color:rgba(30,144,255,0.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(30,144,255,0.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label::before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:.5rem}.custom-switch .custom-control-label::after{top:calc(.25rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#adb5bd;border-radius:.5rem;transition:transform 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.custom-switch .custom-control-label::after{transition:none}}.custom-switch .custom-control-input:checked~.custom-control-label::after{background-color:#fff;transform:translateX(.75rem)}.custom-switch .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(30,144,255,0.5)}.custom-select{display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem 1.75rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;vertical-align:middle;background:#fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") right .75rem center/8px 10px no-repeat;border:1px solid #ced4da;border-radius:.25rem;appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none}.custom-select:focus{border-color:#9ecfff;outline:0;box-shadow:0 0 0 .2rem rgba(30,144,255,0.25)}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{display:none}.custom-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}.custom-select-sm{height:calc(1.5em + .5rem + 2px);padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.custom-select-lg{height:calc(1.5em + 1rem + 2px);padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.custom-file{position:relative;display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(1.5em + .75rem + 2px);margin:0;overflow:hidden;opacity:0}.custom-file-input:focus~.custom-file-label{border-color:#9ecfff;box-shadow:0 0 0 .2rem rgba(30,144,255,0.25)}.custom-file-input[disabled]~.custom-file-label,.custom-file-input:disabled~.custom-file-label{background-color:#e9ecef}.custom-file-input:lang(en)~.custom-file-label::after{content:"Browse"}.custom-file-input~.custom-file-label[data-browse]::after{content:attr(data-browse)}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;overflow:hidden;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:calc(1.5em + .75rem);padding:.375rem .75rem;line-height:1.5;color:#495057;content:"Browse";background-color:#e9ecef;border-left:inherit;border-radius:0 .25rem .25rem 0}.custom-range{width:100%;height:1.4rem;padding:0;background-color:transparent;appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none}.custom-range:focus{outline:0}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(30,144,255,0.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(30,144,255,0.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(30,144,255,0.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#1e90ff;border:0;border-radius:1rem;transition:background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out;appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none}@media (prefers-reduced-motion: reduce){.custom-range::-webkit-slider-thumb{transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#d1e8ff}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#1e90ff;border:0;border-radius:1rem;transition:background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out;appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none}@media (prefers-reduced-motion: reduce){.custom-range::-moz-range-thumb{transition:none}}.custom-range::-moz-range-thumb:active{background-color:#d1e8ff}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:.2rem;margin-left:.2rem;background-color:#1e90ff;border:0;border-radius:1rem;transition:background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out;appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none}@media (prefers-reduced-motion: reduce){.custom-range::-ms-thumb{transition:none}}.custom-range::-ms-thumb:active{background-color:#d1e8ff}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#adb5bd}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#adb5bd}.custom-control-label::before,.custom-file-label,.custom-select{transition:background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.custom-control-label::before,.custom-file-label,.custom-select{transition:none}}.nav{display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link,.nav-tabs>li>a,.nav-pills>li>a,:where(ul.nav.navbar-nav > li)>a{display:block;padding:.5rem 1rem}.nav-link:hover,.nav-tabs>li>a:hover,.nav-pills>li>a:hover,:where(ul.nav.navbar-nav > li)>a:hover,.nav-link:focus,.nav-tabs>li>a:focus,.nav-pills>li>a:focus,:where(ul.nav.navbar-nav > li)>a:focus{text-decoration:none}.nav-link.disabled,.nav-tabs>li>a.disabled,.nav-pills>li>a.disabled,:where(ul.nav.navbar-nav > li)>a.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-link,.nav-tabs>li>a,.nav-tabs .nav-pills>li>a,.nav-tabs :where(ul.nav.navbar-nav > li)>a{margin-bottom:-1px;border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:hover,.nav-tabs>li>a:hover,.nav-tabs .nav-pills>li>a:hover,.nav-tabs :where(ul.nav.navbar-nav > li)>a:hover,.nav-tabs .nav-link:focus,.nav-tabs>li>a:focus,.nav-tabs .nav-pills>li>a:focus,.nav-tabs :where(ul.nav.navbar-nav > li)>a:focus{border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled,.nav-tabs>li>a.disabled,.nav-tabs .nav-pills>li>a.disabled,.nav-tabs :where(ul.nav.navbar-nav > li)>a.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-link.active,.nav-tabs>li>a.active,.nav-tabs .nav-pills>li>a.active,.nav-tabs :where(ul.nav.navbar-nav > li)>a.active,.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-item.in .nav-link,.nav-tabs .nav-item.show .nav-tabs>li>a,.nav-tabs .nav-item.in .nav-tabs>li>a,.nav-tabs .nav-item.show .nav-pills>li>a,.nav-tabs .nav-item.in .nav-pills>li>a,.nav-tabs>li.show .nav-link,.nav-tabs>li.in .nav-link,.nav-tabs>li.show .nav-tabs>li>a,.nav-tabs>li.in .nav-tabs>li>a,.nav-tabs>li.show .nav-pills>li>a,.nav-tabs>li.in .nav-pills>li>a,.nav-tabs .nav-pills>li.show .nav-link,.nav-tabs .nav-pills>li.in .nav-link,.nav-tabs .nav-pills>li.show .nav-tabs>li>a,.nav-tabs .nav-pills>li.in .nav-tabs>li>a,.nav-tabs .nav-pills>li.show .nav-pills>li>a,.nav-tabs .nav-pills>li.in .nav-pills>li>a,.nav-tabs .nav-item.show :where(ul.nav.navbar-nav > li)>a,.nav-tabs .nav-item.in :where(ul.nav.navbar-nav > li)>a,.nav-tabs>li.show :where(ul.nav.navbar-nav > li)>a,.nav-tabs>li.in :where(ul.nav.navbar-nav > li)>a,.nav-tabs .nav-pills>li.show :where(ul.nav.navbar-nav > li)>a,.nav-tabs .nav-pills>li.in :where(ul.nav.navbar-nav > li)>a,.nav-tabs .show:where(ul.nav.navbar-nav > li):not(.dropdown) .nav-link,.nav-tabs .in:where(ul.nav.navbar-nav > li):not(.dropdown) .nav-link,.nav-tabs .show:where(ul.nav.navbar-nav > li):not(.dropdown) .nav-tabs>li>a,.nav-tabs .in:where(ul.nav.navbar-nav > li):not(.dropdown) .nav-tabs>li>a,.nav-tabs .show:where(ul.nav.navbar-nav > li):not(.dropdown) .nav-pills>li>a,.nav-tabs .in:where(ul.nav.navbar-nav > li):not(.dropdown) .nav-pills>li>a,.nav-tabs .show:where(ul.nav.navbar-nav > li):not(.dropdown) :where(ul.nav.navbar-nav > li)>a,.nav-tabs .in:where(ul.nav.navbar-nav > li):not(.dropdown) :where(ul.nav.navbar-nav > li)>a{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link,.nav-pills .nav-tabs>li>a,.nav-pills>li>a,.nav-pills :where(ul.nav.navbar-nav > li)>a{border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .nav-tabs>li>a.active,.nav-pills>li>a.active,.nav-pills :where(ul.nav.navbar-nav > li)>a.active,.nav-pills .show>.nav-link,.nav-pills .in>.nav-link,.nav-pills .nav-tabs>li.show>a,.nav-pills .nav-tabs>li.in>a,.nav-pills>li.show>a,.nav-pills>li.in>a,.nav-pills .show:where(ul.nav.navbar-nav > li)>a,.nav-pills .in:where(ul.nav.navbar-nav > li)>a{color:#fff;background-color:#1e90ff}.nav-fill>.nav-link,.nav-tabs>li.nav-fill>a,.nav-pills>li.nav-fill>a,.nav-fill:where(ul.nav.navbar-nav > li)>a,.nav-fill .nav-item,.nav-fill .nav-tabs>li,.nav-fill .nav-pills>li,.nav-fill :where(ul.nav.navbar-nav > li):not(.dropdown){flex:1 1 auto;-webkit-flex:1 1 auto;text-align:center}.nav-justified>.nav-link,.nav-tabs>li.nav-justified>a,.nav-pills>li.nav-justified>a,.nav-justified:where(ul.nav.navbar-nav > li)>a,.nav-justified .nav-item,.nav-justified .nav-tabs>li,.nav-justified .nav-pills>li,.nav-justified :where(ul.nav.navbar-nav > li):not(.dropdown){flex-basis:0;-webkit-flex-basis:0;flex-grow:1;-webkit-flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;align-items:center;-webkit-align-items:center;justify-content:space-between;-webkit-justify-content:space-between;padding:.5rem 1rem}.navbar .container,.navbar .container-fluid,.navbar .container-sm,.navbar .container-md,.navbar .container-lg,.navbar .container-xl{display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;align-items:center;-webkit-align-items:center;justify-content:space-between;-webkit-justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}.navbar-nav{display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link,.navbar-nav .nav-tabs>li>a,.navbar-nav .nav-pills>li>a,.navbar-nav :where(ul.nav.navbar-nav > li)>a{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{flex-basis:100%;-webkit-flex-basis:100%;flex-grow:1;-webkit-flex-grow:1;align-items:center;-webkit-align-items:center}.navbar-toggler,.navbar-toggle{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:hover,.navbar-toggle:hover,.navbar-toggler:focus,.navbar-toggle:focus{text-decoration:none}.navbar-toggler-icon,.navbar-toggle>.icon-bar:last-child{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:50% / 100% 100% no-repeat}.navbar-nav-scroll{max-height:75vh;overflow-y:auto}@media (max-width: 575.98px){.navbar-expand-sm>.container,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl)>.container,.navbar-expand-sm>.container-fluid,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl)>.container-fluid,.navbar-expand-sm>.container-sm,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl)>.container-sm,.navbar-expand-sm>.container-md,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl)>.container-md,.navbar-expand-sm>.container-lg,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl)>.container-lg,.navbar-expand-sm>.container-xl,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl)>.container-xl{padding-right:0;padding-left:0}}@media (min-width: 576px){.navbar-expand-sm,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl){flex-flow:row nowrap;-webkit-flex-flow:row nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand-sm .navbar-nav,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl) .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl) .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link,.navbar-expand-sm .navbar-nav .nav-tabs>li>a,.navbar-expand-sm .navbar-nav .nav-pills>li>a,.navbar-expand-sm .navbar-nav :where(ul.nav.navbar-nav > li)>a,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl) .navbar-nav .nav-link,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl) .navbar-nav .nav-tabs>li>a,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl) .navbar-nav .nav-pills>li>a,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl) .navbar-nav :where(ul.nav.navbar-nav > li)>a{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl)>.container,.navbar-expand-sm>.container-fluid,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl)>.container-fluid,.navbar-expand-sm>.container-sm,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl)>.container-sm,.navbar-expand-sm>.container-md,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl)>.container-md,.navbar-expand-sm>.container-lg,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl)>.container-lg,.navbar-expand-sm>.container-xl,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl)>.container-xl{flex-wrap:nowrap;-webkit-flex-wrap:nowrap}.navbar-expand-sm .navbar-nav-scroll,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl) .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl) .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand-sm .navbar-toggler,.navbar-expand-sm .navbar-toggle,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl) .navbar-toggler,.navbar:not(.navbar-expand):not(.navbar-expand-sm):not(.navbar-expand-md):not(.navbar-expand-lg):not(.navbar-expand-xl) .navbar-toggle{display:none}}@media (max-width: 767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-md,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-xl{padding-right:0;padding-left:0}}@media (min-width: 768px){.navbar-expand-md{flex-flow:row nowrap;-webkit-flex-flow:row nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link,.navbar-expand-md .navbar-nav .nav-tabs>li>a,.navbar-expand-md .navbar-nav .nav-pills>li>a,.navbar-expand-md .navbar-nav :where(ul.nav.navbar-nav > li)>a{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-md,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-xl{flex-wrap:nowrap;-webkit-flex-wrap:nowrap}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand-md .navbar-toggler,.navbar-expand-md .navbar-toggle{display:none}}@media (max-width: 991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-xl{padding-right:0;padding-left:0}}@media (min-width: 992px){.navbar-expand-lg{flex-flow:row nowrap;-webkit-flex-flow:row nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link,.navbar-expand-lg .navbar-nav .nav-tabs>li>a,.navbar-expand-lg .navbar-nav .nav-pills>li>a,.navbar-expand-lg .navbar-nav :where(ul.nav.navbar-nav > li)>a{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-xl{flex-wrap:nowrap;-webkit-flex-wrap:nowrap}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand-lg .navbar-toggler,.navbar-expand-lg .navbar-toggle{display:none}}@media (max-width: 1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-xl{padding-right:0;padding-left:0}}@media (min-width: 1200px){.navbar-expand-xl{flex-flow:row nowrap;-webkit-flex-flow:row nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link,.navbar-expand-xl .navbar-nav .nav-tabs>li>a,.navbar-expand-xl .navbar-nav .nav-pills>li>a,.navbar-expand-xl .navbar-nav :where(ul.nav.navbar-nav > li)>a{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-xl{flex-wrap:nowrap;-webkit-flex-wrap:nowrap}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand-xl .navbar-toggler,.navbar-expand-xl .navbar-toggle{display:none}}.navbar-expand{flex-flow:row nowrap;-webkit-flex-flow:row nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-sm,.navbar-expand>.container-md,.navbar-expand>.container-lg,.navbar-expand>.container-xl{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link,.navbar-expand .navbar-nav .nav-tabs>li>a,.navbar-expand .navbar-nav .nav-pills>li>a,.navbar-expand .navbar-nav :where(ul.nav.navbar-nav > li)>a{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-sm,.navbar-expand>.container-md,.navbar-expand>.container-lg,.navbar-expand>.container-xl{flex-wrap:nowrap;-webkit-flex-wrap:nowrap}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand .navbar-toggler,.navbar-expand .navbar-toggle{display:none}.navbar-light,.navbar.navbar-default{background-color:#f8f9fa}.navbar-light .navbar-brand,.navbar.navbar-default .navbar-brand{color:#000}.navbar-light .navbar-brand:hover,.navbar.navbar-default .navbar-brand:hover,.navbar-light .navbar-brand:focus,.navbar.navbar-default .navbar-brand:focus{color:#000}.navbar-light .navbar-nav .nav-link,.navbar-light .navbar-nav .nav-tabs>li>a,.navbar-light .navbar-nav .nav-pills>li>a,.navbar.navbar-default .navbar-nav .nav-link,.navbar.navbar-default .navbar-nav .nav-tabs>li>a,.navbar.navbar-default .navbar-nav .nav-pills>li>a,.navbar-light .navbar-nav :where(ul.nav.navbar-nav > li)>a,.navbar.navbar-default .navbar-nav :where(ul.nav.navbar-nav > li)>a{color:rgba(0,0,0,0.5)}.navbar-light .navbar-nav .nav-link:hover,.navbar-light .navbar-nav .nav-tabs>li>a:hover,.navbar-light .navbar-nav .nav-pills>li>a:hover,.navbar.navbar-default .navbar-nav .nav-link:hover,.navbar.navbar-default .navbar-nav .nav-tabs>li>a:hover,.navbar.navbar-default .navbar-nav .nav-pills>li>a:hover,.navbar-light .navbar-nav :where(ul.nav.navbar-nav > li)>a:hover,.navbar.navbar-default .navbar-nav :where(ul.nav.navbar-nav > li)>a:hover,.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-tabs>li>a:focus,.navbar-light .navbar-nav .nav-pills>li>a:focus,.navbar.navbar-default .navbar-nav .nav-link:focus,.navbar.navbar-default .navbar-nav .nav-tabs>li>a:focus,.navbar.navbar-default .navbar-nav .nav-pills>li>a:focus,.navbar-light .navbar-nav :where(ul.nav.navbar-nav > li)>a:focus,.navbar.navbar-default .navbar-nav :where(ul.nav.navbar-nav > li)>a:focus{color:rgba(0,0,0,0.75)}.navbar-light .navbar-nav .nav-link.disabled,.navbar-light .navbar-nav .nav-tabs>li>a.disabled,.navbar-light .navbar-nav .nav-pills>li>a.disabled,.navbar.navbar-default .navbar-nav .nav-link.disabled,.navbar.navbar-default .navbar-nav .nav-tabs>li>a.disabled,.navbar.navbar-default .navbar-nav .nav-pills>li>a.disabled,.navbar-light .navbar-nav :where(ul.nav.navbar-nav > li)>a.disabled,.navbar.navbar-default .navbar-nav :where(ul.nav.navbar-nav > li)>a.disabled{color:rgba(0,0,0,0.25)}.navbar-light .navbar-nav .show>.nav-link,.navbar-light .navbar-nav .in>.nav-link,.navbar-light .navbar-nav .nav-tabs>li.show>a,.navbar-light .navbar-nav .nav-tabs>li.in>a,.navbar-light .navbar-nav .nav-pills>li.show>a,.navbar-light .navbar-nav .nav-pills>li.in>a,.navbar.navbar-default .navbar-nav .show>.nav-link,.navbar.navbar-default .navbar-nav .in>.nav-link,.navbar.navbar-default .navbar-nav .nav-tabs>li.show>a,.navbar.navbar-default .navbar-nav .nav-tabs>li.in>a,.navbar.navbar-default .navbar-nav .nav-pills>li.show>a,.navbar.navbar-default .navbar-nav .nav-pills>li.in>a,.navbar-light .navbar-nav .show:where(ul.nav.navbar-nav > li)>a,.navbar-light .navbar-nav .in:where(ul.nav.navbar-nav > li)>a,.navbar.navbar-default .navbar-nav .show:where(ul.nav.navbar-nav > li)>a,.navbar.navbar-default .navbar-nav .in:where(ul.nav.navbar-nav > li)>a,.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-tabs>li.active>a,.navbar-light .navbar-nav .nav-pills>li.active>a,.navbar.navbar-default .navbar-nav .active>.nav-link,.navbar.navbar-default .navbar-nav .nav-tabs>li.active>a,.navbar.navbar-default .navbar-nav .nav-pills>li.active>a,.navbar-light .navbar-nav .active:where(ul.nav.navbar-nav > li)>a,.navbar.navbar-default .navbar-nav .active:where(ul.nav.navbar-nav > li)>a,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .nav-link.in,.navbar-light .navbar-nav .nav-tabs>li>a.show,.navbar-light .navbar-nav .nav-tabs>li>a.in,.navbar-light .navbar-nav .nav-pills>li>a.show,.navbar-light .navbar-nav .nav-pills>li>a.in,.navbar.navbar-default .navbar-nav .nav-link.show,.navbar.navbar-default .navbar-nav .nav-link.in,.navbar.navbar-default .navbar-nav .nav-tabs>li>a.show,.navbar.navbar-default .navbar-nav .nav-tabs>li>a.in,.navbar.navbar-default .navbar-nav .nav-pills>li>a.show,.navbar.navbar-default .navbar-nav .nav-pills>li>a.in,.navbar-light .navbar-nav :where(ul.nav.navbar-nav > li)>a.show,.navbar-light .navbar-nav :where(ul.nav.navbar-nav > li)>a.in,.navbar.navbar-default .navbar-nav :where(ul.nav.navbar-nav > li)>a.show,.navbar.navbar-default .navbar-nav :where(ul.nav.navbar-nav > li)>a.in,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-tabs>li>a.active,.navbar-light .navbar-nav .nav-pills>li>a.active,.navbar.navbar-default .navbar-nav .nav-link.active,.navbar.navbar-default .navbar-nav .nav-tabs>li>a.active,.navbar.navbar-default .navbar-nav .nav-pills>li>a.active,.navbar-light .navbar-nav :where(ul.nav.navbar-nav > li)>a.active,.navbar.navbar-default .navbar-nav :where(ul.nav.navbar-nav > li)>a.active{color:#000}.navbar-light .navbar-toggler,.navbar-light .navbar-toggle,.navbar.navbar-default .navbar-toggler,.navbar.navbar-default .navbar-toggle{color:rgba(0,0,0,0.5);border-color:rgba(0,0,0,0.1)}.navbar-light .navbar-toggler-icon,.navbar-light .navbar-toggle>.icon-bar:last-child,.navbar.navbar-default .navbar-toggler-icon,.navbar.navbar-default .navbar-toggle>.icon-bar:last-child{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280,0,0,0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text,.navbar.navbar-default .navbar-text{color:rgba(0,0,0,0.5)}.navbar-light .navbar-text a,.navbar.navbar-default .navbar-text a{color:#000}.navbar-light .navbar-text a:hover,.navbar.navbar-default .navbar-text a:hover,.navbar-light .navbar-text a:focus,.navbar.navbar-default .navbar-text a:focus{color:#000}.navbar-dark,.navbar.navbar-inverse{background-color:#343a40}.navbar-dark .navbar-brand,.navbar.navbar-inverse .navbar-brand{color:#fff}.navbar-dark .navbar-brand:hover,.navbar.navbar-inverse .navbar-brand:hover,.navbar-dark .navbar-brand:focus,.navbar.navbar-inverse .navbar-brand:focus{color:#fff}.navbar-dark .navbar-nav .nav-link,.navbar-dark .navbar-nav .nav-tabs>li>a,.navbar-dark .navbar-nav .nav-pills>li>a,.navbar.navbar-inverse .navbar-nav .nav-link,.navbar.navbar-inverse .navbar-nav .nav-tabs>li>a,.navbar.navbar-inverse .navbar-nav .nav-pills>li>a,.navbar-dark .navbar-nav :where(ul.nav.navbar-nav > li)>a,.navbar.navbar-inverse .navbar-nav :where(ul.nav.navbar-nav > li)>a{color:rgba(255,255,255,0.5)}.navbar-dark .navbar-nav .nav-link:hover,.navbar-dark .navbar-nav .nav-tabs>li>a:hover,.navbar-dark .navbar-nav .nav-pills>li>a:hover,.navbar.navbar-inverse .navbar-nav .nav-link:hover,.navbar.navbar-inverse .navbar-nav .nav-tabs>li>a:hover,.navbar.navbar-inverse .navbar-nav .nav-pills>li>a:hover,.navbar-dark .navbar-nav :where(ul.nav.navbar-nav > li)>a:hover,.navbar.navbar-inverse .navbar-nav :where(ul.nav.navbar-nav > li)>a:hover,.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-tabs>li>a:focus,.navbar-dark .navbar-nav .nav-pills>li>a:focus,.navbar.navbar-inverse .navbar-nav .nav-link:focus,.navbar.navbar-inverse .navbar-nav .nav-tabs>li>a:focus,.navbar.navbar-inverse .navbar-nav .nav-pills>li>a:focus,.navbar-dark .navbar-nav :where(ul.nav.navbar-nav > li)>a:focus,.navbar.navbar-inverse .navbar-nav :where(ul.nav.navbar-nav > li)>a:focus{color:rgba(255,255,255,0.75)}.navbar-dark .navbar-nav .nav-link.disabled,.navbar-dark .navbar-nav .nav-tabs>li>a.disabled,.navbar-dark .navbar-nav .nav-pills>li>a.disabled,.navbar.navbar-inverse .navbar-nav .nav-link.disabled,.navbar.navbar-inverse .navbar-nav .nav-tabs>li>a.disabled,.navbar.navbar-inverse .navbar-nav .nav-pills>li>a.disabled,.navbar-dark .navbar-nav :where(ul.nav.navbar-nav > li)>a.disabled,.navbar.navbar-inverse .navbar-nav :where(ul.nav.navbar-nav > li)>a.disabled{color:rgba(255,255,255,0.25)}.navbar-dark .navbar-nav .show>.nav-link,.navbar-dark .navbar-nav .in>.nav-link,.navbar-dark .navbar-nav .nav-tabs>li.show>a,.navbar-dark .navbar-nav .nav-tabs>li.in>a,.navbar-dark .navbar-nav .nav-pills>li.show>a,.navbar-dark .navbar-nav .nav-pills>li.in>a,.navbar.navbar-inverse .navbar-nav .show>.nav-link,.navbar.navbar-inverse .navbar-nav .in>.nav-link,.navbar.navbar-inverse .navbar-nav .nav-tabs>li.show>a,.navbar.navbar-inverse .navbar-nav .nav-tabs>li.in>a,.navbar.navbar-inverse .navbar-nav .nav-pills>li.show>a,.navbar.navbar-inverse .navbar-nav .nav-pills>li.in>a,.navbar-dark .navbar-nav .show:where(ul.nav.navbar-nav > li)>a,.navbar-dark .navbar-nav .in:where(ul.nav.navbar-nav > li)>a,.navbar.navbar-inverse .navbar-nav .show:where(ul.nav.navbar-nav > li)>a,.navbar.navbar-inverse .navbar-nav .in:where(ul.nav.navbar-nav > li)>a,.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-tabs>li.active>a,.navbar-dark .navbar-nav .nav-pills>li.active>a,.navbar.navbar-inverse .navbar-nav .active>.nav-link,.navbar.navbar-inverse .navbar-nav .nav-tabs>li.active>a,.navbar.navbar-inverse .navbar-nav .nav-pills>li.active>a,.navbar-dark .navbar-nav .active:where(ul.nav.navbar-nav > li)>a,.navbar.navbar-inverse .navbar-nav .active:where(ul.nav.navbar-nav > li)>a,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .nav-link.in,.navbar-dark .navbar-nav .nav-tabs>li>a.show,.navbar-dark .navbar-nav .nav-tabs>li>a.in,.navbar-dark .navbar-nav .nav-pills>li>a.show,.navbar-dark .navbar-nav .nav-pills>li>a.in,.navbar.navbar-inverse .navbar-nav .nav-link.show,.navbar.navbar-inverse .navbar-nav .nav-link.in,.navbar.navbar-inverse .navbar-nav .nav-tabs>li>a.show,.navbar.navbar-inverse .navbar-nav .nav-tabs>li>a.in,.navbar.navbar-inverse .navbar-nav .nav-pills>li>a.show,.navbar.navbar-inverse .navbar-nav .nav-pills>li>a.in,.navbar-dark .navbar-nav :where(ul.nav.navbar-nav > li)>a.show,.navbar-dark .navbar-nav :where(ul.nav.navbar-nav > li)>a.in,.navbar.navbar-inverse .navbar-nav :where(ul.nav.navbar-nav > li)>a.show,.navbar.navbar-inverse .navbar-nav :where(ul.nav.navbar-nav > li)>a.in,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-tabs>li>a.active,.navbar-dark .navbar-nav .nav-pills>li>a.active,.navbar.navbar-inverse .navbar-nav .nav-link.active,.navbar.navbar-inverse .navbar-nav .nav-tabs>li>a.active,.navbar.navbar-inverse .navbar-nav .nav-pills>li>a.active,.navbar-dark .navbar-nav :where(ul.nav.navbar-nav > li)>a.active,.navbar.navbar-inverse .navbar-nav :where(ul.nav.navbar-nav > li)>a.active{color:#fff}.navbar-dark .navbar-toggler,.navbar-dark .navbar-toggle,.navbar.navbar-inverse .navbar-toggler,.navbar.navbar-inverse .navbar-toggle{color:rgba(255,255,255,0.5);border-color:rgba(255,255,255,0.1)}.navbar-dark .navbar-toggler-icon,.navbar-dark .navbar-toggle>.icon-bar:last-child,.navbar.navbar-inverse .navbar-toggler-icon,.navbar.navbar-inverse .navbar-toggle>.icon-bar:last-child{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255,255,255,0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text,.navbar.navbar-inverse .navbar-text{color:rgba(255,255,255,0.5)}.navbar-dark .navbar-text a,.navbar.navbar-inverse .navbar-text a{color:#fff}.navbar-dark .navbar-text a:hover,.navbar.navbar-inverse .navbar-text a:hover,.navbar-dark .navbar-text a:focus,.navbar.navbar-inverse .navbar-text a:focus{color:#fff}.card,.well{position:relative;display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,0.125);border-radius:.25rem}.card>hr,.well>hr{margin-right:0;margin-left:0}.card>.list-group,.well>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child,.well>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child,.well>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card>.card-header+.list-group,.well>.card-header+.list-group,.card>.list-group+.card-footer,.well>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;-webkit-flex:1 1 auto;min-height:1px;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:rgba(0,0,0,0.03);border-bottom:1px solid rgba(0,0,0,0.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(0,0,0,0.03);border-top:1px solid rgba(0,0,0,0.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem;border-radius:calc(.25rem - 1px)}.card-img,.card-img-top,.card-img-bottom{flex-shrink:0;-webkit-flex-shrink:0;width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-deck .card,.card-deck .well{margin-bottom:15px}@media (min-width: 576px){.card-deck{display:flex;display:-webkit-flex;flex-flow:row wrap;-webkit-flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card,.card-deck .well{flex:1 0 0%;-webkit-flex:1 0 0%;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group>.card,.card-group>.well{margin-bottom:15px}@media (min-width: 576px){.card-group{display:flex;display:-webkit-flex;flex-flow:row wrap;-webkit-flex-flow:row wrap}.card-group>.card,.card-group>.well{flex:1 0 0%;-webkit-flex:1 0 0%;margin-bottom:0}.card-group>.card+.card,.card-group>.well+.card,.card-group>.card+.well,.card-group>.well+.well{margin-left:0;border-left:0}.card-group>.card:not(:last-child),.card-group>.well:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-img-top,.card-group>.well:not(:last-child) .card-img-top,.card-group>.card:not(:last-child) .card-header,.card-group>.well:not(:last-child) .card-header{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-img-bottom,.card-group>.well:not(:last-child) .card-img-bottom,.card-group>.card:not(:last-child) .card-footer,.card-group>.well:not(:last-child) .card-footer{border-bottom-right-radius:0}.card-group>.card:not(:first-child),.card-group>.well:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-img-top,.card-group>.well:not(:first-child) .card-img-top,.card-group>.card:not(:first-child) .card-header,.card-group>.well:not(:first-child) .card-header{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-img-bottom,.card-group>.well:not(:first-child) .card-img-bottom,.card-group>.card:not(:first-child) .card-footer,.card-group>.well:not(:first-child) .card-footer{border-bottom-left-radius:0}}.card-columns .card,.card-columns .well{margin-bottom:.75rem}@media (min-width: 576px){.card-columns{column-count:3;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card,.card-columns .well{display:inline-block;width:100%}}.accordion{overflow-anchor:none}.accordion>.card,.accordion>.well{overflow:hidden}.accordion>.card:not(:last-of-type),.accordion>.well:not(:last-of-type){border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion>.card:not(:first-of-type),.accordion>.well:not(:first-of-type){border-top-left-radius:0;border-top-right-radius:0}.accordion>.card>.card-header,.accordion>.well>.card-header{border-radius:0;margin-bottom:-1px}.breadcrumb{display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:.25rem}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:.5rem;color:#6c757d;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:flex;display:-webkit-flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#1e90ff;background-color:#fff;border:1px solid #dee2e6}.page-link:hover{z-index:2;color:#006ad1;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;outline:0;box-shadow:0 0 0 .2rem rgba(30,144,255,0.25)}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:3;color:#fff;background-color:#1e90ff;border-color:#1e90ff}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dee2e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem;transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.badge{transition:none}}a.badge:hover,a.badge:focus{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-default{color:#000;background-color:#dee2e6}a.badge-default:hover,a.badge-default:focus{color:#000;background-color:#c1c9d0}a.badge-default:focus,a.badge-default.focus{outline:0;box-shadow:0 0 0 .2rem rgba(222,226,230,0.5)}.badge-primary{color:#fff;background-color:#1e90ff}a.badge-primary:hover,a.badge-primary:focus{color:#fff;background-color:#0077ea}a.badge-primary:focus,a.badge-primary.focus{outline:0;box-shadow:0 0 0 .2rem rgba(30,144,255,0.5)}.badge-secondary{color:#fff;background-color:#6c757d}a.badge-secondary:hover,a.badge-secondary:focus{color:#fff;background-color:#545b62}a.badge-secondary:focus,a.badge-secondary.focus{outline:0;box-shadow:0 0 0 .2rem rgba(108,117,125,0.5)}.badge-success{color:#fff;background-color:#28a745}a.badge-success:hover,a.badge-success:focus{color:#fff;background-color:#1e7e34}a.badge-success:focus,a.badge-success.focus{outline:0;box-shadow:0 0 0 .2rem rgba(40,167,69,0.5)}.badge-info{color:#000;background-color:#0DCAF0}a.badge-info:hover,a.badge-info:focus{color:#000;background-color:#0aa1c0}a.badge-info:focus,a.badge-info.focus{outline:0;box-shadow:0 0 0 .2rem rgba(13,202,240,0.5)}.badge-warning{color:#000;background-color:#ffc107}a.badge-warning:hover,a.badge-warning:focus{color:#000;background-color:#d39e00}a.badge-warning:focus,a.badge-warning.focus{outline:0;box-shadow:0 0 0 .2rem rgba(255,193,7,0.5)}.badge-danger{color:#fff;background-color:#dc3545}a.badge-danger:hover,a.badge-danger:focus{color:#fff;background-color:#bd2130}a.badge-danger:focus,a.badge-danger.focus{outline:0;box-shadow:0 0 0 .2rem rgba(220,53,69,0.5)}.badge-light{color:#000;background-color:#f8f9fa}a.badge-light:hover,a.badge-light:focus{color:#000;background-color:#dae0e5}a.badge-light:focus,a.badge-light.focus{outline:0;box-shadow:0 0 0 .2rem rgba(248,249,250,0.5)}.badge-dark{color:#fff;background-color:#343a40}a.badge-dark:hover,a.badge-dark:focus{color:#fff;background-color:#1d2124}a.badge-dark:focus,a.badge-dark.focus{outline:0;box-shadow:0 0 0 .2rem rgba(52,58,64,0.5)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.3rem}@media (min-width: 576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;z-index:2;padding:.75rem 1.25rem;color:inherit}.alert-default{color:#737678;background-color:#f8f9fa;border-color:#f6f7f8}.alert-default hr{border-top-color:#e8eaed}.alert-default .alert-link{color:#5a5c5e}.alert-primary{color:#104b85;background-color:#d2e9ff;border-color:#c0e0ff}.alert-primary hr{border-top-color:#a7d3ff}.alert-primary .alert-link{color:#0b3157}.alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-secondary hr{border-top-color:#c8cbcf}.alert-secondary .alert-link{color:#202326}.alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-success hr{border-top-color:#b1dfbb}.alert-success .alert-link{color:#0b2e13}.alert-info{color:#07697d;background-color:#cff4fc;border-color:#bbf0fb}.alert-info hr{border-top-color:#a3ebfa}.alert-info .alert-link{color:#04404d}.alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#533f03}.alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-danger hr{border-top-color:#f1b0b7}.alert-danger .alert-link{color:#491217}.alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#686868}.alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.alert-dark .alert-link{color:#040505}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:flex;display:-webkit-flex;height:1rem;overflow:hidden;line-height:0;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;justify-content:center;-webkit-justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#1e90ff;transition:width 0.6s ease}@media (prefers-reduced-motion: reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-size:1rem 1rem}.progress-bar-animated{animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion: reduce){.progress-bar-animated{animation:none}}.media{display:flex;display:-webkit-flex;align-items:flex-start;-webkit-align-items:flex-start}.media-body{flex:1;-webkit-flex:1}.list-group{display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:hover,.list-group-item-action:focus{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;background-color:#fff;border:1px solid rgba(0,0,0,0.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#1e90ff;border-color:#1e90ff}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width: 576px){.list-group-horizontal-sm{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width: 768px){.list-group-horizontal-md{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width: 992px){.list-group-horizontal-lg{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width: 1200px){.list-group-horizontal-xl{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-default{color:#737678;background-color:#f6f7f8}.list-group-item-default.list-group-item-action:hover,.list-group-item-default.list-group-item-action:focus{color:#737678;background-color:#e8eaed}.list-group-item-default.list-group-item-action.active{color:#fff;background-color:#737678;border-color:#737678}.list-group-item-primary{color:#104b85;background-color:#c0e0ff}.list-group-item-primary.list-group-item-action:hover,.list-group-item-primary.list-group-item-action:focus{color:#104b85;background-color:#a7d3ff}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#104b85;border-color:#104b85}.list-group-item-secondary{color:#383d41;background-color:#d6d8db}.list-group-item-secondary.list-group-item-action:hover,.list-group-item-secondary.list-group-item-action:focus{color:#383d41;background-color:#c8cbcf}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#383d41;border-color:#383d41}.list-group-item-success{color:#155724;background-color:#c3e6cb}.list-group-item-success.list-group-item-action:hover,.list-group-item-success.list-group-item-action:focus{color:#155724;background-color:#b1dfbb}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#155724;border-color:#155724}.list-group-item-info{color:#07697d;background-color:#bbf0fb}.list-group-item-info.list-group-item-action:hover,.list-group-item-info.list-group-item-action:focus{color:#07697d;background-color:#a3ebfa}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#07697d;border-color:#07697d}.list-group-item-warning{color:#856404;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:hover,.list-group-item-warning.list-group-item-action:focus{color:#856404;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#856404;border-color:#856404}.list-group-item-danger{color:#721c24;background-color:#f5c6cb}.list-group-item-danger.list-group-item-action:hover,.list-group-item-danger.list-group-item-action:focus{color:#721c24;background-color:#f1b0b7}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#721c24;border-color:#721c24}.list-group-item-light{color:#818182;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:hover,.list-group-item-light.list-group-item-action:focus{color:#818182;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#818182;border-color:#818182}.list-group-item-dark{color:#1b1e21;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:hover,.list-group-item-dark.list-group-item-action:focus{color:#1b1e21;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1b1e21;border-color:#1b1e21}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:hover{color:#000;text-decoration:none}.close:not(:disabled):not(.disabled):hover,.close:not(:disabled):not(.disabled):focus{opacity:.75}button.close{padding:0;background-color:transparent;border:0}a.close.disabled{pointer-events:none}.toast{flex-basis:350px;-webkit-flex-basis:350px;max-width:350px;font-size:.875rem;background-color:rgba(255,255,255,0.85);background-clip:padding-box;border:1px solid rgba(0,0,0,0.1);box-shadow:0 0.25rem 0.75rem rgba(0,0,0,0.1);opacity:0;border-radius:.25rem}.toast:not(:last-child){margin-bottom:.75rem}.toast.showing{opacity:1}.toast.show,.toast.in{display:block;opacity:1}.toast.hide{display:none}.toast-header{display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;padding:.25rem .75rem;color:#6c757d;background-color:rgba(255,255,255,0.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,0.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.toast-body{padding:.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform 0.3s ease-out;transform:translate(0, -50px)}@media (prefers-reduced-motion: reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog,.modal.in .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{display:flex;display:-webkit-flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-header,.modal-dialog-scrollable .modal-footer{flex-shrink:0;-webkit-flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;min-height:calc(100% - 1rem)}.modal-dialog-centered::before{display:block;height:calc(100vh - 1rem);height:min-content;height:-webkit-min-content;height:-moz-min-content;height:-ms-min-content;height:-o-min-content;content:""}.modal-dialog-centered.modal-dialog-scrollable{flex-direction:column;-webkit-flex-direction:column;justify-content:center;-webkit-justify-content:center;height:100%}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable::before{content:none}.modal-content{position:relative;display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,0.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show,.modal-backdrop.in{opacity:.5}.modal-header{display:flex;display:-webkit-flex;align-items:flex-start;-webkit-align-items:flex-start;justify-content:space-between;-webkit-justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .close{padding:1rem 1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;-webkit-flex:1 1 auto;padding:1rem}.modal-footer{display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;align-items:center;-webkit-align-items:center;justify-content:flex-end;-webkit-justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width: 576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered::before{height:calc(100vh - 3.5rem);height:min-content;height:-webkit-min-content;height:-moz-min-content;height:-ms-min-content;height:-o-min-content}.modal-sm{max-width:300px}}@media (min-width: 992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width: 1200px){.modal-xl{max-width:1140px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show,.tooltip.in{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-top,.bs-tooltip-auto[x-placement^="top"]{padding:.4rem 0}.bs-tooltip-top .arrow,.bs-tooltip-auto[x-placement^="top"] .arrow{bottom:0}.bs-tooltip-top .arrow::before,.bs-tooltip-auto[x-placement^="top"] .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-right,.bs-tooltip-auto[x-placement^="right"]{padding:0 .4rem}.bs-tooltip-right .arrow,.bs-tooltip-auto[x-placement^="right"] .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-right .arrow::before,.bs-tooltip-auto[x-placement^="right"] .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-bottom,.bs-tooltip-auto[x-placement^="bottom"]{padding:.4rem 0}.bs-tooltip-bottom .arrow,.bs-tooltip-auto[x-placement^="bottom"] .arrow{top:0}.bs-tooltip-bottom .arrow::before,.bs-tooltip-auto[x-placement^="bottom"] .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-left,.bs-tooltip-auto[x-placement^="left"]{padding:0 .4rem}.bs-tooltip-left .arrow,.bs-tooltip-auto[x-placement^="left"] .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-left .arrow::before,.bs-tooltip-auto[x-placement^="left"] .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,0.2);border-radius:.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::before,.popover .arrow::after{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-top,.bs-popover-auto[x-placement^="top"]{margin-bottom:.5rem}.bs-popover-top>.arrow,.bs-popover-auto[x-placement^="top"]>.arrow{bottom:calc(-.5rem - 1px)}.bs-popover-top>.arrow::before,.bs-popover-auto[x-placement^="top"]>.arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,0.25)}.bs-popover-top>.arrow::after,.bs-popover-auto[x-placement^="top"]>.arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-right,.bs-popover-auto[x-placement^="right"]{margin-left:.5rem}.bs-popover-right>.arrow,.bs-popover-auto[x-placement^="right"]>.arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-right>.arrow::before,.bs-popover-auto[x-placement^="right"]>.arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,0.25)}.bs-popover-right>.arrow::after,.bs-popover-auto[x-placement^="right"]>.arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-bottom,.bs-popover-auto[x-placement^="bottom"]{margin-top:.5rem}.bs-popover-bottom>.arrow,.bs-popover-auto[x-placement^="bottom"]>.arrow{top:calc(-.5rem - 1px)}.bs-popover-bottom>.arrow::before,.bs-popover-auto[x-placement^="bottom"]>.arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,0.25)}.bs-popover-bottom>.arrow::after,.bs-popover-auto[x-placement^="bottom"]>.arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-bottom .popover-header::before,.bs-popover-auto[x-placement^="bottom"] .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-left,.bs-popover-auto[x-placement^="left"]{margin-right:.5rem}.bs-popover-left>.arrow,.bs-popover-auto[x-placement^="left"]>.arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-left>.arrow::before,.bs-popover-auto[x-placement^="left"]>.arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,0.25)}.bs-popover-left>.arrow::after,.bs-popover-auto[x-placement^="left"]>.arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y;-webkit-touch-action:pan-y;-moz-touch-action:pan-y;-ms-touch-action:pan-y;-o-touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;backface-visibility:hidden;-webkit-backface-visibility:hidden;-moz-backface-visibility:hidden;-ms-backface-visibility:hidden;-o-backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion: reduce){.carousel-item{transition:none}}.carousel-item.active,.carousel-item-next,.carousel-item-prev{display:block}.carousel-item-next:not(.carousel-item-left),.active.carousel-item-right{transform:translateX(100%)}.carousel-item-prev:not(.carousel-item-right),.active.carousel-item-left{transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item.active,.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right{z-index:1;opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion: reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{transition:none}}.carousel-control-prev,.carousel-control-next{position:absolute;top:0;bottom:0;z-index:1;display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;justify-content:center;-webkit-justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5;transition:opacity 0.15s ease}@media (prefers-reduced-motion: reduce){.carousel-control-prev,.carousel-control-next{transition:none}}.carousel-control-prev:hover,.carousel-control-prev:focus,.carousel-control-next:hover,.carousel-control-next:focus{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-prev-icon,.carousel-control-next-icon{display:inline-block;width:20px;height:20px;background:50% / 100% 100% no-repeat}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:15;display:flex;display:-webkit-flex;justify-content:center;-webkit-justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{box-sizing:content-box;flex:0 1 auto;-webkit-flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity 0.6s ease}@media (prefers-reduced-motion: reduce){.carousel-indicators li{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion: reduce){.spinner-border,.spinner-grow{animation-duration:1.5s;-webkit-animation-duration:1.5s;-moz-animation-duration:1.5s;-ms-animation-duration:1.5s;-o-animation-duration:1.5s}}.align-baseline{vertical-align:baseline !important}.align-top{vertical-align:top !important}.align-middle{vertical-align:middle !important}.align-bottom{vertical-align:bottom !important}.align-text-bottom{vertical-align:text-bottom !important}.align-text-top{vertical-align:text-top !important}.bg-default{background-color:#dee2e6 !important;color:#000}a.bg-default:hover,a.bg-default:focus,button.bg-default:hover,button.bg-default:focus{background-color:#c1c9d0 !important}.bg-primary{background-color:#1e90ff !important;color:#fff}a.bg-primary:hover,a.bg-primary:focus,button.bg-primary:hover,button.bg-primary:focus{background-color:#0077ea !important}.bg-secondary{background-color:#6c757d !important;color:#fff}a.bg-secondary:hover,a.bg-secondary:focus,button.bg-secondary:hover,button.bg-secondary:focus{background-color:#545b62 !important}.bg-success{background-color:#28a745 !important;color:#fff}a.bg-success:hover,a.bg-success:focus,button.bg-success:hover,button.bg-success:focus{background-color:#1e7e34 !important}.bg-info{background-color:#0DCAF0 !important;color:#000}a.bg-info:hover,a.bg-info:focus,button.bg-info:hover,button.bg-info:focus{background-color:#0aa1c0 !important}.bg-warning{background-color:#ffc107 !important;color:#000}a.bg-warning:hover,a.bg-warning:focus,button.bg-warning:hover,button.bg-warning:focus{background-color:#d39e00 !important}.bg-danger{background-color:#dc3545 !important;color:#fff}a.bg-danger:hover,a.bg-danger:focus,button.bg-danger:hover,button.bg-danger:focus{background-color:#bd2130 !important}.bg-light{background-color:#f8f9fa !important;color:#000}a.bg-light:hover,a.bg-light:focus,button.bg-light:hover,button.bg-light:focus{background-color:#dae0e5 !important}.bg-dark{background-color:#343a40 !important;color:#fff}a.bg-dark:hover,a.bg-dark:focus,button.bg-dark:hover,button.bg-dark:focus{background-color:#1d2124 !important}.bg-white{background-color:#fff !important;color:#000}.bg-transparent{background-color:transparent !important}.border{border:1px solid #dee2e6 !important}.border-top{border-top:1px solid #dee2e6 !important}.border-right{border-right:1px solid #dee2e6 !important}.border-bottom{border-bottom:1px solid #dee2e6 !important}.border-left{border-left:1px solid #dee2e6 !important}.border-0{border:0 !important}.border-top-0{border-top:0 !important}.border-right-0{border-right:0 !important}.border-bottom-0{border-bottom:0 !important}.border-left-0{border-left:0 !important}.border-default{border-color:#dee2e6 !important}.border-primary{border-color:#1e90ff !important}.border-secondary{border-color:#6c757d !important}.border-success{border-color:#28a745 !important}.border-info{border-color:#0DCAF0 !important}.border-warning{border-color:#ffc107 !important}.border-danger{border-color:#dc3545 !important}.border-light{border-color:#f8f9fa !important}.border-dark{border-color:#343a40 !important}.border-white{border-color:#fff !important}.rounded-sm{border-radius:.2rem !important}.rounded{border-radius:.25rem !important}.rounded-top{border-top-left-radius:.25rem !important;border-top-right-radius:.25rem !important}.rounded-right{border-top-right-radius:.25rem !important;border-bottom-right-radius:.25rem !important}.rounded-bottom{border-bottom-right-radius:.25rem !important;border-bottom-left-radius:.25rem !important}.rounded-left{border-top-left-radius:.25rem !important;border-bottom-left-radius:.25rem !important}.rounded-lg{border-radius:.3rem !important}.rounded-circle{border-radius:50% !important}.rounded-pill{border-radius:50rem !important}.rounded-0{border-radius:0 !important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none !important}.d-inline{display:inline !important}.d-inline-block{display:inline-block !important}.d-block{display:block !important}.d-table{display:table !important}.d-table-row{display:table-row !important}.d-table-cell{display:table-cell !important}.d-flex{display:flex !important}.d-inline-flex{display:inline-flex !important}@media (min-width: 576px){.d-sm-none{display:none !important}.d-sm-inline{display:inline !important}.d-sm-inline-block{display:inline-block !important}.d-sm-block{display:block !important}.d-sm-table{display:table !important}.d-sm-table-row{display:table-row !important}.d-sm-table-cell{display:table-cell !important}.d-sm-flex{display:flex !important}.d-sm-inline-flex{display:inline-flex !important}}@media (min-width: 768px){.d-md-none{display:none !important}.d-md-inline{display:inline !important}.d-md-inline-block{display:inline-block !important}.d-md-block{display:block !important}.d-md-table{display:table !important}.d-md-table-row{display:table-row !important}.d-md-table-cell{display:table-cell !important}.d-md-flex{display:flex !important}.d-md-inline-flex{display:inline-flex !important}}@media (min-width: 992px){.d-lg-none{display:none !important}.d-lg-inline{display:inline !important}.d-lg-inline-block{display:inline-block !important}.d-lg-block{display:block !important}.d-lg-table{display:table !important}.d-lg-table-row{display:table-row !important}.d-lg-table-cell{display:table-cell !important}.d-lg-flex{display:flex !important}.d-lg-inline-flex{display:inline-flex !important}}@media (min-width: 1200px){.d-xl-none{display:none !important}.d-xl-inline{display:inline !important}.d-xl-inline-block{display:inline-block !important}.d-xl-block{display:block !important}.d-xl-table{display:table !important}.d-xl-table-row{display:table-row !important}.d-xl-table-cell{display:table-cell !important}.d-xl-flex{display:flex !important}.d-xl-inline-flex{display:inline-flex !important}}@media print{.d-print-none{display:none !important}.d-print-inline{display:inline !important}.d-print-inline-block{display:inline-block !important}.d-print-block{display:block !important}.d-print-table{display:table !important}.d-print-table-row{display:table-row !important}.d-print-table-cell{display:table-cell !important}.d-print-flex{display:flex !important}.d-print-inline-flex{display:inline-flex !important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive iframe,.embed-responsive embed,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.85714%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{flex-direction:row !important}.flex-column{flex-direction:column !important}.flex-row-reverse{flex-direction:row-reverse !important}.flex-column-reverse{flex-direction:column-reverse !important}.flex-wrap{flex-wrap:wrap !important}.flex-nowrap{flex-wrap:nowrap !important}.flex-wrap-reverse{flex-wrap:wrap-reverse !important}.flex-fill{flex:1 1 auto !important}.flex-grow-0{flex-grow:0 !important}.flex-grow-1{flex-grow:1 !important}.flex-shrink-0{flex-shrink:0 !important}.flex-shrink-1{flex-shrink:1 !important}.justify-content-start{justify-content:flex-start !important}.justify-content-end{justify-content:flex-end !important}.justify-content-center{justify-content:center !important}.justify-content-between{justify-content:space-between !important}.justify-content-around{justify-content:space-around !important}.align-items-start{align-items:flex-start !important}.align-items-end{align-items:flex-end !important}.align-items-center{align-items:center !important}.align-items-baseline{align-items:baseline !important}.align-items-stretch{align-items:stretch !important}.align-content-start{align-content:flex-start !important}.align-content-end{align-content:flex-end !important}.align-content-center{align-content:center !important}.align-content-between{align-content:space-between !important}.align-content-around{align-content:space-around !important}.align-content-stretch{align-content:stretch !important}.align-self-auto{align-self:auto !important}.align-self-start{align-self:flex-start !important}.align-self-end{align-self:flex-end !important}.align-self-center{align-self:center !important}.align-self-baseline{align-self:baseline !important}.align-self-stretch{align-self:stretch !important}@media (min-width: 576px){.flex-sm-row{flex-direction:row !important}.flex-sm-column{flex-direction:column !important}.flex-sm-row-reverse{flex-direction:row-reverse !important}.flex-sm-column-reverse{flex-direction:column-reverse !important}.flex-sm-wrap{flex-wrap:wrap !important}.flex-sm-nowrap{flex-wrap:nowrap !important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse !important}.flex-sm-fill{flex:1 1 auto !important}.flex-sm-grow-0{flex-grow:0 !important}.flex-sm-grow-1{flex-grow:1 !important}.flex-sm-shrink-0{flex-shrink:0 !important}.flex-sm-shrink-1{flex-shrink:1 !important}.justify-content-sm-start{justify-content:flex-start !important}.justify-content-sm-end{justify-content:flex-end !important}.justify-content-sm-center{justify-content:center !important}.justify-content-sm-between{justify-content:space-between !important}.justify-content-sm-around{justify-content:space-around !important}.align-items-sm-start{align-items:flex-start !important}.align-items-sm-end{align-items:flex-end !important}.align-items-sm-center{align-items:center !important}.align-items-sm-baseline{align-items:baseline !important}.align-items-sm-stretch{align-items:stretch !important}.align-content-sm-start{align-content:flex-start !important}.align-content-sm-end{align-content:flex-end !important}.align-content-sm-center{align-content:center !important}.align-content-sm-between{align-content:space-between !important}.align-content-sm-around{align-content:space-around !important}.align-content-sm-stretch{align-content:stretch !important}.align-self-sm-auto{align-self:auto !important}.align-self-sm-start{align-self:flex-start !important}.align-self-sm-end{align-self:flex-end !important}.align-self-sm-center{align-self:center !important}.align-self-sm-baseline{align-self:baseline !important}.align-self-sm-stretch{align-self:stretch !important}}@media (min-width: 768px){.flex-md-row{flex-direction:row !important}.flex-md-column{flex-direction:column !important}.flex-md-row-reverse{flex-direction:row-reverse !important}.flex-md-column-reverse{flex-direction:column-reverse !important}.flex-md-wrap{flex-wrap:wrap !important}.flex-md-nowrap{flex-wrap:nowrap !important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse !important}.flex-md-fill{flex:1 1 auto !important}.flex-md-grow-0{flex-grow:0 !important}.flex-md-grow-1{flex-grow:1 !important}.flex-md-shrink-0{flex-shrink:0 !important}.flex-md-shrink-1{flex-shrink:1 !important}.justify-content-md-start{justify-content:flex-start !important}.justify-content-md-end{justify-content:flex-end !important}.justify-content-md-center{justify-content:center !important}.justify-content-md-between{justify-content:space-between !important}.justify-content-md-around{justify-content:space-around !important}.align-items-md-start{align-items:flex-start !important}.align-items-md-end{align-items:flex-end !important}.align-items-md-center{align-items:center !important}.align-items-md-baseline{align-items:baseline !important}.align-items-md-stretch{align-items:stretch !important}.align-content-md-start{align-content:flex-start !important}.align-content-md-end{align-content:flex-end !important}.align-content-md-center{align-content:center !important}.align-content-md-between{align-content:space-between !important}.align-content-md-around{align-content:space-around !important}.align-content-md-stretch{align-content:stretch !important}.align-self-md-auto{align-self:auto !important}.align-self-md-start{align-self:flex-start !important}.align-self-md-end{align-self:flex-end !important}.align-self-md-center{align-self:center !important}.align-self-md-baseline{align-self:baseline !important}.align-self-md-stretch{align-self:stretch !important}}@media (min-width: 992px){.flex-lg-row{flex-direction:row !important}.flex-lg-column{flex-direction:column !important}.flex-lg-row-reverse{flex-direction:row-reverse !important}.flex-lg-column-reverse{flex-direction:column-reverse !important}.flex-lg-wrap{flex-wrap:wrap !important}.flex-lg-nowrap{flex-wrap:nowrap !important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse !important}.flex-lg-fill{flex:1 1 auto !important}.flex-lg-grow-0{flex-grow:0 !important}.flex-lg-grow-1{flex-grow:1 !important}.flex-lg-shrink-0{flex-shrink:0 !important}.flex-lg-shrink-1{flex-shrink:1 !important}.justify-content-lg-start{justify-content:flex-start !important}.justify-content-lg-end{justify-content:flex-end !important}.justify-content-lg-center{justify-content:center !important}.justify-content-lg-between{justify-content:space-between !important}.justify-content-lg-around{justify-content:space-around !important}.align-items-lg-start{align-items:flex-start !important}.align-items-lg-end{align-items:flex-end !important}.align-items-lg-center{align-items:center !important}.align-items-lg-baseline{align-items:baseline !important}.align-items-lg-stretch{align-items:stretch !important}.align-content-lg-start{align-content:flex-start !important}.align-content-lg-end{align-content:flex-end !important}.align-content-lg-center{align-content:center !important}.align-content-lg-between{align-content:space-between !important}.align-content-lg-around{align-content:space-around !important}.align-content-lg-stretch{align-content:stretch !important}.align-self-lg-auto{align-self:auto !important}.align-self-lg-start{align-self:flex-start !important}.align-self-lg-end{align-self:flex-end !important}.align-self-lg-center{align-self:center !important}.align-self-lg-baseline{align-self:baseline !important}.align-self-lg-stretch{align-self:stretch !important}}@media (min-width: 1200px){.flex-xl-row{flex-direction:row !important}.flex-xl-column{flex-direction:column !important}.flex-xl-row-reverse{flex-direction:row-reverse !important}.flex-xl-column-reverse{flex-direction:column-reverse !important}.flex-xl-wrap{flex-wrap:wrap !important}.flex-xl-nowrap{flex-wrap:nowrap !important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse !important}.flex-xl-fill{flex:1 1 auto !important}.flex-xl-grow-0{flex-grow:0 !important}.flex-xl-grow-1{flex-grow:1 !important}.flex-xl-shrink-0{flex-shrink:0 !important}.flex-xl-shrink-1{flex-shrink:1 !important}.justify-content-xl-start{justify-content:flex-start !important}.justify-content-xl-end{justify-content:flex-end !important}.justify-content-xl-center{justify-content:center !important}.justify-content-xl-between{justify-content:space-between !important}.justify-content-xl-around{justify-content:space-around !important}.align-items-xl-start{align-items:flex-start !important}.align-items-xl-end{align-items:flex-end !important}.align-items-xl-center{align-items:center !important}.align-items-xl-baseline{align-items:baseline !important}.align-items-xl-stretch{align-items:stretch !important}.align-content-xl-start{align-content:flex-start !important}.align-content-xl-end{align-content:flex-end !important}.align-content-xl-center{align-content:center !important}.align-content-xl-between{align-content:space-between !important}.align-content-xl-around{align-content:space-around !important}.align-content-xl-stretch{align-content:stretch !important}.align-self-xl-auto{align-self:auto !important}.align-self-xl-start{align-self:flex-start !important}.align-self-xl-end{align-self:flex-end !important}.align-self-xl-center{align-self:center !important}.align-self-xl-baseline{align-self:baseline !important}.align-self-xl-stretch{align-self:stretch !important}}.float-left{float:left !important}.float-right{float:right !important}.float-none{float:none !important}@media (min-width: 576px){.float-sm-left{float:left !important}.float-sm-right{float:right !important}.float-sm-none{float:none !important}}@media (min-width: 768px){.float-md-left{float:left !important}.float-md-right{float:right !important}.float-md-none{float:none !important}}@media (min-width: 992px){.float-lg-left{float:left !important}.float-lg-right{float:right !important}.float-lg-none{float:none !important}}@media (min-width: 1200px){.float-xl-left{float:left !important}.float-xl-right{float:right !important}.float-xl-none{float:none !important}}.user-select-all{user-select:all !important}.user-select-auto{user-select:auto !important}.user-select-none{user-select:none !important}.overflow-auto{overflow:auto !important}.overflow-hidden{overflow:hidden !important}.position-static{position:static !important}.position-relative{position:relative !important}.position-absolute{position:absolute !important}.position-fixed{position:fixed !important}.position-sticky{position:sticky !important}.fixed-top,.navbar-fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom,.navbar-fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports (position: sticky){.sticky-top,.navbar-sticky-top{position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{box-shadow:0 0.125rem 0.25rem rgba(0,0,0,0.075) !important}.shadow{box-shadow:0 0.5rem 1rem rgba(0,0,0,0.15) !important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,0.175) !important}.shadow-none{box-shadow:none !important}.w-25{width:25% !important}.w-50{width:50% !important}.w-75{width:75% !important}.w-100{width:100% !important}.w-auto{width:auto !important}.h-25{height:25% !important}.h-50{height:50% !important}.h-75{height:75% !important}.h-100{height:100% !important}.h-auto{height:auto !important}.mw-100{max-width:100% !important}.mh-100{max-height:100% !important}.min-vw-100{min-width:100vw !important}.min-vh-100{min-height:100vh !important}.vw-100{width:100vw !important}.vh-100{height:100vh !important}.m-0{margin:0 !important}.mt-0,.my-0{margin-top:0 !important}.mr-0,.mx-0{margin-right:0 !important}.mb-0,.my-0{margin-bottom:0 !important}.ml-0,.mx-0{margin-left:0 !important}.m-1{margin:.25rem !important}.mt-1,.my-1{margin-top:.25rem !important}.mr-1,.mx-1{margin-right:.25rem !important}.mb-1,.my-1{margin-bottom:.25rem !important}.ml-1,.mx-1{margin-left:.25rem !important}.m-2{margin:.5rem !important}.mt-2,.my-2{margin-top:.5rem !important}.mr-2,.mx-2{margin-right:.5rem !important}.mb-2,.my-2{margin-bottom:.5rem !important}.ml-2,.mx-2{margin-left:.5rem !important}.m-3{margin:1rem !important}.mt-3,.my-3{margin-top:1rem !important}.mr-3,.mx-3{margin-right:1rem !important}.mb-3,.my-3{margin-bottom:1rem !important}.ml-3,.mx-3{margin-left:1rem !important}.m-4{margin:1.5rem !important}.mt-4,.my-4{margin-top:1.5rem !important}.mr-4,.mx-4{margin-right:1.5rem !important}.mb-4,.my-4{margin-bottom:1.5rem !important}.ml-4,.mx-4{margin-left:1.5rem !important}.m-5{margin:3rem !important}.mt-5,.my-5{margin-top:3rem !important}.mr-5,.mx-5{margin-right:3rem !important}.mb-5,.my-5{margin-bottom:3rem !important}.ml-5,.mx-5{margin-left:3rem !important}.p-0{padding:0 !important}.pt-0,.py-0{padding-top:0 !important}.pr-0,.px-0{padding-right:0 !important}.pb-0,.py-0{padding-bottom:0 !important}.pl-0,.px-0{padding-left:0 !important}.p-1{padding:.25rem !important}.pt-1,.py-1{padding-top:.25rem !important}.pr-1,.px-1{padding-right:.25rem !important}.pb-1,.py-1{padding-bottom:.25rem !important}.pl-1,.px-1{padding-left:.25rem !important}.p-2{padding:.5rem !important}.pt-2,.py-2{padding-top:.5rem !important}.pr-2,.px-2{padding-right:.5rem !important}.pb-2,.py-2{padding-bottom:.5rem !important}.pl-2,.px-2{padding-left:.5rem !important}.p-3{padding:1rem !important}.pt-3,.py-3{padding-top:1rem !important}.pr-3,.px-3{padding-right:1rem !important}.pb-3,.py-3{padding-bottom:1rem !important}.pl-3,.px-3{padding-left:1rem !important}.p-4{padding:1.5rem !important}.pt-4,.py-4{padding-top:1.5rem !important}.pr-4,.px-4{padding-right:1.5rem !important}.pb-4,.py-4{padding-bottom:1.5rem !important}.pl-4,.px-4{padding-left:1.5rem !important}.p-5{padding:3rem !important}.pt-5,.py-5{padding-top:3rem !important}.pr-5,.px-5{padding-right:3rem !important}.pb-5,.py-5{padding-bottom:3rem !important}.pl-5,.px-5{padding-left:3rem !important}.m-n1{margin:-.25rem !important}.mt-n1,.my-n1{margin-top:-.25rem !important}.mr-n1,.mx-n1{margin-right:-.25rem !important}.mb-n1,.my-n1{margin-bottom:-.25rem !important}.ml-n1,.mx-n1{margin-left:-.25rem !important}.m-n2{margin:-.5rem !important}.mt-n2,.my-n2{margin-top:-.5rem !important}.mr-n2,.mx-n2{margin-right:-.5rem !important}.mb-n2,.my-n2{margin-bottom:-.5rem !important}.ml-n2,.mx-n2{margin-left:-.5rem !important}.m-n3{margin:-1rem !important}.mt-n3,.my-n3{margin-top:-1rem !important}.mr-n3,.mx-n3{margin-right:-1rem !important}.mb-n3,.my-n3{margin-bottom:-1rem !important}.ml-n3,.mx-n3{margin-left:-1rem !important}.m-n4{margin:-1.5rem !important}.mt-n4,.my-n4{margin-top:-1.5rem !important}.mr-n4,.mx-n4{margin-right:-1.5rem !important}.mb-n4,.my-n4{margin-bottom:-1.5rem !important}.ml-n4,.mx-n4{margin-left:-1.5rem !important}.m-n5{margin:-3rem !important}.mt-n5,.my-n5{margin-top:-3rem !important}.mr-n5,.mx-n5{margin-right:-3rem !important}.mb-n5,.my-n5{margin-bottom:-3rem !important}.ml-n5,.mx-n5{margin-left:-3rem !important}.m-auto{margin:auto !important}.mt-auto,.my-auto{margin-top:auto !important}.mr-auto,.mx-auto{margin-right:auto !important}.mb-auto,.my-auto{margin-bottom:auto !important}.ml-auto,.mx-auto{margin-left:auto !important}@media (min-width: 576px){.m-sm-0{margin:0 !important}.mt-sm-0,.my-sm-0{margin-top:0 !important}.mr-sm-0,.mx-sm-0{margin-right:0 !important}.mb-sm-0,.my-sm-0{margin-bottom:0 !important}.ml-sm-0,.mx-sm-0{margin-left:0 !important}.m-sm-1{margin:.25rem !important}.mt-sm-1,.my-sm-1{margin-top:.25rem !important}.mr-sm-1,.mx-sm-1{margin-right:.25rem !important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem !important}.ml-sm-1,.mx-sm-1{margin-left:.25rem !important}.m-sm-2{margin:.5rem !important}.mt-sm-2,.my-sm-2{margin-top:.5rem !important}.mr-sm-2,.mx-sm-2{margin-right:.5rem !important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem !important}.ml-sm-2,.mx-sm-2{margin-left:.5rem !important}.m-sm-3{margin:1rem !important}.mt-sm-3,.my-sm-3{margin-top:1rem !important}.mr-sm-3,.mx-sm-3{margin-right:1rem !important}.mb-sm-3,.my-sm-3{margin-bottom:1rem !important}.ml-sm-3,.mx-sm-3{margin-left:1rem !important}.m-sm-4{margin:1.5rem !important}.mt-sm-4,.my-sm-4{margin-top:1.5rem !important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem !important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem !important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem !important}.m-sm-5{margin:3rem !important}.mt-sm-5,.my-sm-5{margin-top:3rem !important}.mr-sm-5,.mx-sm-5{margin-right:3rem !important}.mb-sm-5,.my-sm-5{margin-bottom:3rem !important}.ml-sm-5,.mx-sm-5{margin-left:3rem !important}.p-sm-0{padding:0 !important}.pt-sm-0,.py-sm-0{padding-top:0 !important}.pr-sm-0,.px-sm-0{padding-right:0 !important}.pb-sm-0,.py-sm-0{padding-bottom:0 !important}.pl-sm-0,.px-sm-0{padding-left:0 !important}.p-sm-1{padding:.25rem !important}.pt-sm-1,.py-sm-1{padding-top:.25rem !important}.pr-sm-1,.px-sm-1{padding-right:.25rem !important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem !important}.pl-sm-1,.px-sm-1{padding-left:.25rem !important}.p-sm-2{padding:.5rem !important}.pt-sm-2,.py-sm-2{padding-top:.5rem !important}.pr-sm-2,.px-sm-2{padding-right:.5rem !important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem !important}.pl-sm-2,.px-sm-2{padding-left:.5rem !important}.p-sm-3{padding:1rem !important}.pt-sm-3,.py-sm-3{padding-top:1rem !important}.pr-sm-3,.px-sm-3{padding-right:1rem !important}.pb-sm-3,.py-sm-3{padding-bottom:1rem !important}.pl-sm-3,.px-sm-3{padding-left:1rem !important}.p-sm-4{padding:1.5rem !important}.pt-sm-4,.py-sm-4{padding-top:1.5rem !important}.pr-sm-4,.px-sm-4{padding-right:1.5rem !important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem !important}.pl-sm-4,.px-sm-4{padding-left:1.5rem !important}.p-sm-5{padding:3rem !important}.pt-sm-5,.py-sm-5{padding-top:3rem !important}.pr-sm-5,.px-sm-5{padding-right:3rem !important}.pb-sm-5,.py-sm-5{padding-bottom:3rem !important}.pl-sm-5,.px-sm-5{padding-left:3rem !important}.m-sm-n1{margin:-.25rem !important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem !important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem !important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem !important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem !important}.m-sm-n2{margin:-.5rem !important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem !important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem !important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem !important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem !important}.m-sm-n3{margin:-1rem !important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem !important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem !important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem !important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem !important}.m-sm-n4{margin:-1.5rem !important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem !important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem !important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem !important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem !important}.m-sm-n5{margin:-3rem !important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem !important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem !important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem !important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem !important}.m-sm-auto{margin:auto !important}.mt-sm-auto,.my-sm-auto{margin-top:auto !important}.mr-sm-auto,.mx-sm-auto{margin-right:auto !important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto !important}.ml-sm-auto,.mx-sm-auto{margin-left:auto !important}}@media (min-width: 768px){.m-md-0{margin:0 !important}.mt-md-0,.my-md-0{margin-top:0 !important}.mr-md-0,.mx-md-0{margin-right:0 !important}.mb-md-0,.my-md-0{margin-bottom:0 !important}.ml-md-0,.mx-md-0{margin-left:0 !important}.m-md-1{margin:.25rem !important}.mt-md-1,.my-md-1{margin-top:.25rem !important}.mr-md-1,.mx-md-1{margin-right:.25rem !important}.mb-md-1,.my-md-1{margin-bottom:.25rem !important}.ml-md-1,.mx-md-1{margin-left:.25rem !important}.m-md-2{margin:.5rem !important}.mt-md-2,.my-md-2{margin-top:.5rem !important}.mr-md-2,.mx-md-2{margin-right:.5rem !important}.mb-md-2,.my-md-2{margin-bottom:.5rem !important}.ml-md-2,.mx-md-2{margin-left:.5rem !important}.m-md-3{margin:1rem !important}.mt-md-3,.my-md-3{margin-top:1rem !important}.mr-md-3,.mx-md-3{margin-right:1rem !important}.mb-md-3,.my-md-3{margin-bottom:1rem !important}.ml-md-3,.mx-md-3{margin-left:1rem !important}.m-md-4{margin:1.5rem !important}.mt-md-4,.my-md-4{margin-top:1.5rem !important}.mr-md-4,.mx-md-4{margin-right:1.5rem !important}.mb-md-4,.my-md-4{margin-bottom:1.5rem !important}.ml-md-4,.mx-md-4{margin-left:1.5rem !important}.m-md-5{margin:3rem !important}.mt-md-5,.my-md-5{margin-top:3rem !important}.mr-md-5,.mx-md-5{margin-right:3rem !important}.mb-md-5,.my-md-5{margin-bottom:3rem !important}.ml-md-5,.mx-md-5{margin-left:3rem !important}.p-md-0{padding:0 !important}.pt-md-0,.py-md-0{padding-top:0 !important}.pr-md-0,.px-md-0{padding-right:0 !important}.pb-md-0,.py-md-0{padding-bottom:0 !important}.pl-md-0,.px-md-0{padding-left:0 !important}.p-md-1{padding:.25rem !important}.pt-md-1,.py-md-1{padding-top:.25rem !important}.pr-md-1,.px-md-1{padding-right:.25rem !important}.pb-md-1,.py-md-1{padding-bottom:.25rem !important}.pl-md-1,.px-md-1{padding-left:.25rem !important}.p-md-2{padding:.5rem !important}.pt-md-2,.py-md-2{padding-top:.5rem !important}.pr-md-2,.px-md-2{padding-right:.5rem !important}.pb-md-2,.py-md-2{padding-bottom:.5rem !important}.pl-md-2,.px-md-2{padding-left:.5rem !important}.p-md-3{padding:1rem !important}.pt-md-3,.py-md-3{padding-top:1rem !important}.pr-md-3,.px-md-3{padding-right:1rem !important}.pb-md-3,.py-md-3{padding-bottom:1rem !important}.pl-md-3,.px-md-3{padding-left:1rem !important}.p-md-4{padding:1.5rem !important}.pt-md-4,.py-md-4{padding-top:1.5rem !important}.pr-md-4,.px-md-4{padding-right:1.5rem !important}.pb-md-4,.py-md-4{padding-bottom:1.5rem !important}.pl-md-4,.px-md-4{padding-left:1.5rem !important}.p-md-5{padding:3rem !important}.pt-md-5,.py-md-5{padding-top:3rem !important}.pr-md-5,.px-md-5{padding-right:3rem !important}.pb-md-5,.py-md-5{padding-bottom:3rem !important}.pl-md-5,.px-md-5{padding-left:3rem !important}.m-md-n1{margin:-.25rem !important}.mt-md-n1,.my-md-n1{margin-top:-.25rem !important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem !important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem !important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem !important}.m-md-n2{margin:-.5rem !important}.mt-md-n2,.my-md-n2{margin-top:-.5rem !important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem !important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem !important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem !important}.m-md-n3{margin:-1rem !important}.mt-md-n3,.my-md-n3{margin-top:-1rem !important}.mr-md-n3,.mx-md-n3{margin-right:-1rem !important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem !important}.ml-md-n3,.mx-md-n3{margin-left:-1rem !important}.m-md-n4{margin:-1.5rem !important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem !important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem !important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem !important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem !important}.m-md-n5{margin:-3rem !important}.mt-md-n5,.my-md-n5{margin-top:-3rem !important}.mr-md-n5,.mx-md-n5{margin-right:-3rem !important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem !important}.ml-md-n5,.mx-md-n5{margin-left:-3rem !important}.m-md-auto{margin:auto !important}.mt-md-auto,.my-md-auto{margin-top:auto !important}.mr-md-auto,.mx-md-auto{margin-right:auto !important}.mb-md-auto,.my-md-auto{margin-bottom:auto !important}.ml-md-auto,.mx-md-auto{margin-left:auto !important}}@media (min-width: 992px){.m-lg-0{margin:0 !important}.mt-lg-0,.my-lg-0{margin-top:0 !important}.mr-lg-0,.mx-lg-0{margin-right:0 !important}.mb-lg-0,.my-lg-0{margin-bottom:0 !important}.ml-lg-0,.mx-lg-0{margin-left:0 !important}.m-lg-1{margin:.25rem !important}.mt-lg-1,.my-lg-1{margin-top:.25rem !important}.mr-lg-1,.mx-lg-1{margin-right:.25rem !important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem !important}.ml-lg-1,.mx-lg-1{margin-left:.25rem !important}.m-lg-2{margin:.5rem !important}.mt-lg-2,.my-lg-2{margin-top:.5rem !important}.mr-lg-2,.mx-lg-2{margin-right:.5rem !important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem !important}.ml-lg-2,.mx-lg-2{margin-left:.5rem !important}.m-lg-3{margin:1rem !important}.mt-lg-3,.my-lg-3{margin-top:1rem !important}.mr-lg-3,.mx-lg-3{margin-right:1rem !important}.mb-lg-3,.my-lg-3{margin-bottom:1rem !important}.ml-lg-3,.mx-lg-3{margin-left:1rem !important}.m-lg-4{margin:1.5rem !important}.mt-lg-4,.my-lg-4{margin-top:1.5rem !important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem !important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem !important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem !important}.m-lg-5{margin:3rem !important}.mt-lg-5,.my-lg-5{margin-top:3rem !important}.mr-lg-5,.mx-lg-5{margin-right:3rem !important}.mb-lg-5,.my-lg-5{margin-bottom:3rem !important}.ml-lg-5,.mx-lg-5{margin-left:3rem !important}.p-lg-0{padding:0 !important}.pt-lg-0,.py-lg-0{padding-top:0 !important}.pr-lg-0,.px-lg-0{padding-right:0 !important}.pb-lg-0,.py-lg-0{padding-bottom:0 !important}.pl-lg-0,.px-lg-0{padding-left:0 !important}.p-lg-1{padding:.25rem !important}.pt-lg-1,.py-lg-1{padding-top:.25rem !important}.pr-lg-1,.px-lg-1{padding-right:.25rem !important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem !important}.pl-lg-1,.px-lg-1{padding-left:.25rem !important}.p-lg-2{padding:.5rem !important}.pt-lg-2,.py-lg-2{padding-top:.5rem !important}.pr-lg-2,.px-lg-2{padding-right:.5rem !important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem !important}.pl-lg-2,.px-lg-2{padding-left:.5rem !important}.p-lg-3{padding:1rem !important}.pt-lg-3,.py-lg-3{padding-top:1rem !important}.pr-lg-3,.px-lg-3{padding-right:1rem !important}.pb-lg-3,.py-lg-3{padding-bottom:1rem !important}.pl-lg-3,.px-lg-3{padding-left:1rem !important}.p-lg-4{padding:1.5rem !important}.pt-lg-4,.py-lg-4{padding-top:1.5rem !important}.pr-lg-4,.px-lg-4{padding-right:1.5rem !important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem !important}.pl-lg-4,.px-lg-4{padding-left:1.5rem !important}.p-lg-5{padding:3rem !important}.pt-lg-5,.py-lg-5{padding-top:3rem !important}.pr-lg-5,.px-lg-5{padding-right:3rem !important}.pb-lg-5,.py-lg-5{padding-bottom:3rem !important}.pl-lg-5,.px-lg-5{padding-left:3rem !important}.m-lg-n1{margin:-.25rem !important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem !important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem !important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem !important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem !important}.m-lg-n2{margin:-.5rem !important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem !important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem !important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem !important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem !important}.m-lg-n3{margin:-1rem !important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem !important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem !important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem !important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem !important}.m-lg-n4{margin:-1.5rem !important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem !important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem !important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem !important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem !important}.m-lg-n5{margin:-3rem !important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem !important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem !important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem !important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem !important}.m-lg-auto{margin:auto !important}.mt-lg-auto,.my-lg-auto{margin-top:auto !important}.mr-lg-auto,.mx-lg-auto{margin-right:auto !important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto !important}.ml-lg-auto,.mx-lg-auto{margin-left:auto !important}}@media (min-width: 1200px){.m-xl-0{margin:0 !important}.mt-xl-0,.my-xl-0{margin-top:0 !important}.mr-xl-0,.mx-xl-0{margin-right:0 !important}.mb-xl-0,.my-xl-0{margin-bottom:0 !important}.ml-xl-0,.mx-xl-0{margin-left:0 !important}.m-xl-1{margin:.25rem !important}.mt-xl-1,.my-xl-1{margin-top:.25rem !important}.mr-xl-1,.mx-xl-1{margin-right:.25rem !important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem !important}.ml-xl-1,.mx-xl-1{margin-left:.25rem !important}.m-xl-2{margin:.5rem !important}.mt-xl-2,.my-xl-2{margin-top:.5rem !important}.mr-xl-2,.mx-xl-2{margin-right:.5rem !important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem !important}.ml-xl-2,.mx-xl-2{margin-left:.5rem !important}.m-xl-3{margin:1rem !important}.mt-xl-3,.my-xl-3{margin-top:1rem !important}.mr-xl-3,.mx-xl-3{margin-right:1rem !important}.mb-xl-3,.my-xl-3{margin-bottom:1rem !important}.ml-xl-3,.mx-xl-3{margin-left:1rem !important}.m-xl-4{margin:1.5rem !important}.mt-xl-4,.my-xl-4{margin-top:1.5rem !important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem !important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem !important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem !important}.m-xl-5{margin:3rem !important}.mt-xl-5,.my-xl-5{margin-top:3rem !important}.mr-xl-5,.mx-xl-5{margin-right:3rem !important}.mb-xl-5,.my-xl-5{margin-bottom:3rem !important}.ml-xl-5,.mx-xl-5{margin-left:3rem !important}.p-xl-0{padding:0 !important}.pt-xl-0,.py-xl-0{padding-top:0 !important}.pr-xl-0,.px-xl-0{padding-right:0 !important}.pb-xl-0,.py-xl-0{padding-bottom:0 !important}.pl-xl-0,.px-xl-0{padding-left:0 !important}.p-xl-1{padding:.25rem !important}.pt-xl-1,.py-xl-1{padding-top:.25rem !important}.pr-xl-1,.px-xl-1{padding-right:.25rem !important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem !important}.pl-xl-1,.px-xl-1{padding-left:.25rem !important}.p-xl-2{padding:.5rem !important}.pt-xl-2,.py-xl-2{padding-top:.5rem !important}.pr-xl-2,.px-xl-2{padding-right:.5rem !important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem !important}.pl-xl-2,.px-xl-2{padding-left:.5rem !important}.p-xl-3{padding:1rem !important}.pt-xl-3,.py-xl-3{padding-top:1rem !important}.pr-xl-3,.px-xl-3{padding-right:1rem !important}.pb-xl-3,.py-xl-3{padding-bottom:1rem !important}.pl-xl-3,.px-xl-3{padding-left:1rem !important}.p-xl-4{padding:1.5rem !important}.pt-xl-4,.py-xl-4{padding-top:1.5rem !important}.pr-xl-4,.px-xl-4{padding-right:1.5rem !important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem !important}.pl-xl-4,.px-xl-4{padding-left:1.5rem !important}.p-xl-5{padding:3rem !important}.pt-xl-5,.py-xl-5{padding-top:3rem !important}.pr-xl-5,.px-xl-5{padding-right:3rem !important}.pb-xl-5,.py-xl-5{padding-bottom:3rem !important}.pl-xl-5,.px-xl-5{padding-left:3rem !important}.m-xl-n1{margin:-.25rem !important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem !important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem !important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem !important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem !important}.m-xl-n2{margin:-.5rem !important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem !important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem !important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem !important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem !important}.m-xl-n3{margin:-1rem !important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem !important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem !important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem !important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem !important}.m-xl-n4{margin:-1.5rem !important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem !important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem !important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem !important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem !important}.m-xl-n5{margin:-3rem !important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem !important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem !important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem !important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem !important}.m-xl-auto{margin:auto !important}.mt-xl-auto,.my-xl-auto{margin-top:auto !important}.mr-xl-auto,.mx-xl-auto{margin-right:auto !important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto !important}.ml-xl-auto,.mx-xl-auto{margin-left:auto !important}}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:auto;content:"";background-color:rgba(0,0,0,0)}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace !important}.text-justify{text-align:justify !important}.text-wrap{white-space:normal !important}.text-nowrap{white-space:nowrap !important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left !important}.text-right{text-align:right !important}.text-center{text-align:center !important}@media (min-width: 576px){.text-sm-left{text-align:left !important}.text-sm-right{text-align:right !important}.text-sm-center{text-align:center !important}}@media (min-width: 768px){.text-md-left{text-align:left !important}.text-md-right{text-align:right !important}.text-md-center{text-align:center !important}}@media (min-width: 992px){.text-lg-left{text-align:left !important}.text-lg-right{text-align:right !important}.text-lg-center{text-align:center !important}}@media (min-width: 1200px){.text-xl-left{text-align:left !important}.text-xl-right{text-align:right !important}.text-xl-center{text-align:center !important}}.text-lowercase{text-transform:lowercase !important}.text-uppercase{text-transform:uppercase !important}.text-capitalize{text-transform:capitalize !important}.font-weight-light{font-weight:300 !important}.font-weight-lighter{font-weight:lighter !important}.font-weight-normal{font-weight:400 !important}.font-weight-bold{font-weight:700 !important}.font-weight-bolder{font-weight:bolder !important}.font-italic{font-style:italic !important}.text-white{color:#fff !important}.text-default{color:#dee2e6 !important}a.text-default:hover,a.text-default:focus{color:#b2bcc5 !important}.text-primary{color:#1e90ff !important}a.text-primary:hover,a.text-primary:focus{color:#006ad1 !important}.text-secondary{color:#6c757d !important}a.text-secondary:hover,a.text-secondary:focus{color:#494f54 !important}.text-success{color:#28a745 !important}a.text-success:hover,a.text-success:focus{color:#19692c !important}.text-info{color:#0DCAF0 !important}a.text-info:hover,a.text-info:focus{color:#098da7 !important}.text-warning{color:#ffc107 !important}a.text-warning:hover,a.text-warning:focus{color:#ba8b00 !important}.text-danger{color:#dc3545 !important}a.text-danger:hover,a.text-danger:focus{color:#a71d2a !important}.text-light{color:#f8f9fa !important}a.text-light:hover,a.text-light:focus{color:#cbd3da !important}.text-dark{color:#343a40 !important}a.text-dark:hover,a.text-dark:focus{color:#121416 !important}.text-body{color:#212529 !important}.text-muted,.help-text,.help-block{color:#6c757d !important}.text-black-50{color:rgba(0,0,0,0.5) !important}.text-white-50{color:rgba(255,255,255,0.5) !important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none !important}.text-break{word-break:break-word !important;word-wrap:break-word !important}.text-reset{color:inherit !important}.visible{visibility:visible !important}.invisible{visibility:hidden !important}@media print{*,*::before,*::after{text-shadow:none !important;box-shadow:none !important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap !important}pre,blockquote{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px !important}.container{min-width:992px !important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse !important}.table td,.table th{background-color:#fff !important}.table-bordered th,.table-bordered td{border:1px solid #dee2e6 !important}.table-dark{color:inherit}.table-dark th,.table-dark td,.table-dark thead th,.table-dark tbody+tbody{border-color:#dee2e6}.table .thead-dark th{color:inherit;border-color:#dee2e6}}.table th[align=left]{text-align:left}.table th[align=right]{text-align:right}.table th[align=center]{text-align:center}.well{display:block;background-color:rgba(0,0,0,0.03);color:#212529;padding:1.25rem;border-radius:.25rem}.well-lg{padding:1.5rem;border-radius:.3rem}.well-sm{padding:0.5rem;border-radius:.2rem}.draggable .well{background-color:#f7f7f7}.dropdown-menu>li.active>a{color:#fff;text-decoration:none;background-color:#1e90ff}.navbar .nav.nav-underline{--bs-navbar-nav-link-padding-x: 0}.navbar:not(.fixed-bottom):not(.navbar-fixed-bottom):not(.navbar-fixed-bottom)+div>.tab-content>.tab-pane{--bslib-navbar-margin: 20px;margin-top:var(--bslib-navbar-margin)}ul.nav.navbar-nav{flex:1;-webkit-flex:1}ul.nav.navbar-nav.navbar-right{flex:unset;-webkit-flex:unset;display:flex;display:-webkit-flex;justify-content:flex-end;-webkit-justify-content:flex-end}:where(ul.nav.navbar-nav > li).active>a,:where(ul.nav.navbar-nav > li).show>a,.in:where(ul.nav.navbar-nav > li)>a{color:var(--bs-navbar-active-color)}:where(ul.nav.navbar-nav > li).bslib-nav-item{color:var(--bs-navbar-active-color)}.navbar{--bslib-navbar-default-bg: #f8f9fa;--bslib-navbar-inverse-bg: #343a40}.navbar.navbar-default{background-color:var(--bslib-navbar-default-bg, var(--bs-light)) !important}.navbar.navbar-inverse{background-color:var(--bslib-navbar-inverse-bg, var(--bs-dark)) !important;--bs-emphasis-color: white;--bs-emphasis-color-rgb: 255, 255, 255}.navbar-toggle>.icon-bar{display:none}@media (max-width: 575.98px){.navbar-header{width:100%}.navbar-header .navbar-toggle{float:right}}.nav-tabs>li.active>a{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-pills>li.active>a{color:#fff;background-color:#1e90ff}.nav-stacked{flex-direction:column;-webkit-flex-direction:column}.progress-bar-default{background-color:#dee2e6;color:#000}.progress-bar-primary{background-color:#1e90ff;color:#fff}.progress-bar-secondary{background-color:#6c757d;color:#fff}.progress-bar-success{background-color:#28a745;color:#fff}.progress-bar-info{background-color:#0DCAF0;color:#000}.progress-bar-warning{background-color:#ffc107;color:#000}.progress-bar-danger{background-color:#dc3545;color:#fff}.progress-bar-light{background-color:#f8f9fa;color:#000}.progress-bar-dark{background-color:#343a40;color:#fff}@font-face{font-family:'Glyphicons Halflings';src:url("fonts/bootstrap/glyphicons-halflings-regular.eot");src:url("fonts/bootstrap/glyphicons-halflings-regular.eot?#iefix") format("embedded-opentype"),url("fonts/bootstrap/glyphicons-halflings-regular.woff2") format("woff2"),url("fonts/bootstrap/glyphicons-halflings-regular.woff") format("woff"),url("fonts/bootstrap/glyphicons-halflings-regular.ttf") format("truetype"),url("fonts/bootstrap/glyphicons-halflings-regular.svg#glyphicons_halflingsregular") format("svg")}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-euro:before,.glyphicon-eur:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}.form-group{margin-bottom:1rem}.shiny-input-checkboxgroup .checkbox-inline,.shiny-input-checkboxgroup .radio-inline,.shiny-input-radiogroup .checkbox-inline,.shiny-input-radiogroup .radio-inline{padding-left:0;margin-right:.75rem}.shiny-input-checkboxgroup .checkbox-inline label>input,.shiny-input-checkboxgroup .radio-inline label>input,.shiny-input-radiogroup .checkbox-inline label>input,.shiny-input-radiogroup .radio-inline label>input{margin-top:0;margin-right:.3125rem;margin-bottom:0}.input-daterange .input-group-addon.input-group-prepend.input-group-append{padding:inherit;line-height:inherit;text-shadow:inherit;border-width:0}.input-daterange .input-group-addon.input-group-prepend.input-group-append .input-group-text{border-radius:0}.shiny-input-checkboxgroup .checkbox-inline,.shiny-input-radiogroup .radio-inline{cursor:pointer}pre.shiny-code{padding:0.5rem}h1,h2,h3{margin-top:1.5rem}h4,h5,h6{margin-top:1rem}:root{--bslib-spacer: 1rem;--bslib-mb-spacer: var(--bslib-spacer, 1rem)}.bslib-mb-spacing{margin-bottom:var(--bslib-mb-spacer)}.bslib-gap-spacing{gap:var(--bslib-mb-spacer)}.bslib-gap-spacing>.bslib-mb-spacing,.bslib-gap-spacing>.form-group,.bslib-gap-spacing>p,.bslib-gap-spacing>pre{margin-bottom:0}.html-fill-container>.html-fill-item.bslib-mb-spacing{margin-bottom:0}.tab-content>.tab-pane.html-fill-container{display:none}.tab-content>.active.html-fill-container{display:flex}.tab-content.html-fill-container{padding:0} diff --git a/01-data.Rmd b/01-data.Rmd index 690a11b..f87a8aa 100644 --- a/01-data.Rmd +++ b/01-data.Rmd @@ -165,7 +165,7 @@ If you choose to install everything, however, you can simply run the chunk of co ```{r, message=F, warning=F, eval=F} packages <- c("tidyverse", "ape", "devtools", "igraph", "statnet", "intergraph", "tnet", "ggplot2", "rjson", "d3r", "cccd", "networkD3", "visNetwork", - "GISTools", "rgeos", "maptools", "sf", "igraphdata", "ggrepel", + "GISTools", "sf", "igraphdata", "ggrepel", "ggsn", "tidyverse", "superheat", "ggplotify", "ggforce", "colorspace", "ggmap", "dplyr", "ggpubr", "ggraph", "reshape2", "multinet", "RColorBrewer", "Rcpp", "deldir", "vegan", "geosphere", "networkDynamic", diff --git a/01-data.md b/01-data.md new file mode 100644 index 0000000..dd9f2fc --- /dev/null +++ b/01-data.md @@ -0,0 +1,307 @@ +# (PART) **PART II: Work While You Read** {-} + +# Data and Workspace Setup{#DataAndWorkspace} + +![](images/image_break.png){width=100%} + +This section provides downloadable files for the network data sets used in this online companion and in the book as well as information on the primary R packages used for analysis and visualization throughout the tutorials in this document. We provide very brief instructions for importing these data into R using R-studio and some guidance on setting up your R-studio working environment. For additional guidance see [Getting Started in R](#GettingStarted). + +## Data Sets{#DataSets} + +In the analyses illustrated in this document we use a number of real and simulated archaeological data sets to serve as examples for particular data types and techniques. Most of the data sets used here are provided in .csv (comma separated value) or .RData formats and can be downloaded so that you can follow along with these analyses on your own computer. We encourage you to explore these files and see how they are formatted as a guide for setting up your own data sets. + +The data used here include a range of different network data formats and types. The primary data sets are described in detail in Brughmans and Peeples (2023) Chapter 2.8. Note that where spatial locations for archaeological sites are provided the locations have been randomly jittered up to 10 kilometers from their actual locations to maintain data security. + +For the files below you can right click and "save as" to save them for use locally. Note that there are many additional data sets relating to the replication of particular figures in the book that are provided where the code for that particular figure occurs. If you'd like to just download everything at once [see the next section](#Everything) + +### Just Give Me Everything {#Everything} + +Hey, we get it. You're busy and just want all of the data in one convenient package. We provide all of the data used in the appendix here in a single .zip file for you to download. To follow along with the examples in this appendix you need to choose an R working directory and place the contents of the *.zip folder within it such that all of the individual files are contained within a folder called "data". Note that this includes all of the additional files that are required for reproducing particular figures as well. + +**[All_data.zip](All_data.zip)** - A single compressed file containing all of the data files used in this appendix. + +### Roman Road Networks{#RomanRoad} + +The development of an elaborate road system is one of the most enduring legacies of the Roman Republic and Empire. Areas that came under Roman control were connected to Rome and important provincial centers through entirely new roads as well as redeveloped existing roads. From roughly the second century AD onward this resulted in an integrated terrestrial transport network connecting North-Africa, the Middle East, and western and southern Europe. Much of the subsequent development of transport systems in these regions built on this Roman system. + +Our primary source for roads of the entire Roman world is the Barrington Atlas of the Greek and Roman World (Talbert 2000) and their digitization by the Ancient World Mapping Center (2012). In many of our examples we will focus in particular on the roads of the Iberian Peninsula, which have been digitized in great detail by Pau de Soto (de Soto and Carreras 2021). In our analyses of the Roman road network ancient settlements are represented as nodes and the existence of a road between two settlements is represented by an edge. We also include the length of a road as an edge attribute. + +* **[Hispania_nodes](data/Hispania_nodes.csv)** - NodeIDs and names for Roman era settlements in the Iberian Peninsula along with names and latitude and longitude locations in decimal degrees. +* **[Hispania_roads](data/Hispania_roads.csv)** - Edge list of road connections using NodeIDs from Hispania_nodes file. This file contains a "weight" variable defined for each edge which denotes the length of the road segment. + +The [Stanford ORBIS project](https://orbis.stanford.edu/) provides additional data from across the Roman World including settlements, roads, and characterizations of travel time. Some of these data have been wrapped into a convenient R compendium by [Sebastian Heath](https://github.com/sfsheath) and the data are available on GitHub here: + + +```r +if (!require("devtools")) install.packages("devtools") +devtools::install_github("sfsheath/cawd") +``` + +![data sets used for our case studies on the road networks of (a) the Roman Empire as a whole (source: Ancient World Mapping Centre 2012), and (b) a highly-detailed representation of the Roman road network on the Iberian Peninsula (de Soto and Carreras 2021).](images/Fig.2.4.png){width=100%} + +### Southwest Social Networks Project Ceramic Similarity Networks{#SWSN} + +The Southwest Social Networks (SWSN) Project (and subsequent [cyberSW](https://cybersw.org) project) is a large collaborative effort focused on exploring methods and models for network analysis of archaeological data to better understand patterns of interaction, population movement, and demographic change across the U.S. Southwest and Mexican Northwest through time (ca. A.D. 800-1800; Borck et al. 2015; Giomi et al. 2021; Mills et al. 2013a; 2013b; 2015; 2018; Peeples and Haas 2013; Peeples et al. 2016; Peeples and Roberts 2013). During the interval considered by this project the region was inhabited largely by sedentary agricultural populations (though more mobile populations were also present throughout this period) with communities as large as several thousand people at the peak. The region is blessed with excellent archaeological preservation, a fine grained chronology anchored by dendrochronological dates, and nearly 150 years of focused archaeological research. + +The SWSN/cyberSW project team has gathered a massive database with information on the location and size of tens of thousands of archaeological sites and ceramic and other material cultural typological frequency data consisting of millions of objects to explore how patterns of material similarity, exchange, and technology change across time and space in the study area. These data as well as tools needed to analyze them are available in an online platform called [cyberSW (cyberSW.org)](https://cybersw.org). This online platform even allows you to explore these data directly in your internet browser. The size and complexity of the SWSN/cyberSW data make it a particularly good example for discussing the decision processes involved in visualizing and analyzing large networks. + +In several sections of this book we also use subsets of this larger data set: the San Pedro Valley, and the Chaco World. The San Pedro Valley in southern Arizona is a well-studied portion of the SWSN study area (see Clark and Lyons 2012; Gerald 2019) that was an early focus of network methodological exploration by the team (Mills et al. 2013b). This data subset includes detailed ceramic typological frequency for all known major settlements across this region during the late pre-Hispanic period (ca. A.D. 1200-1450). The Chaco World is a large-scale social and political system that spanned much of the Colorado Plateau ca. A.D. 800-1150. This settlement system was marked by the construction of massive public architectural features known as great houses and great kivas. This subset of the database includes information on architecture and ceramic typological data for a large portion of the known Chacoan architectural complexes throughout the U.S. Southwest. The Chaco World has been a major focus of the SWSN/cyberSW project (Giomi et al. 2021; Giomi and Peeples 2019; Mills et al. 2018). + +In these networks, individual settlements are treated as nodes and edges are defined and weighted based on similarities in the ceramic wares recovered at those settlements. Ceramic data used to generate networks are apportioned into a sequence of 50-year chronological intervals using methods described in detail by Roberts and colleagues (2012) and Ortman (2016; see discussion in Mills et al. 2018) so that we are able to explore change through time. Site locations and other site attribute data are also considered in some examples. R implementations of these chronological apportioning methods are available on GitHub as well ([R implementation of Roberts et al. 2012](https://github.com/mpeeples2008/CeramicApportioning), [R implementation of Ortman 2016](https://github.com/mpeeples2008/UniformProbabilityDensityAnalysis)). + +* **[SWSN Attribute Data AD 1300-1350](data/AD1300attr.csv)** - Attribute data for SWSN sites dating between AD 1300 and 1350 including site name, site sub-region (Macro), and jittered easting and northing UTM coordinates (Zone 12N). +* **[SWSN Similarity Data AD 1300-1350](data/AD1300sim.csv)** - Symmetric similarity matrix based on Brainerd-Robinson similarities for all SWSN sites dating between AD 1300 and 1350. +* **[The Chaco World Attribute Data AD 1050-1100](data/AD1050attr.csv)** - Attribute data for sites with Chacoan architectural features dating between AD 1050 and 1100 including site IDs, site names, site sub-regions, counts of different kinds of public architectural features, and jittered easting and northing UTM site locations (Zone 12N). +* **[The Chaco World Ceramic Data AD 1050-1100](data/AD1050cer.csv)** - Ceramic count data by ware for sites with Chacoan architectural features dating between AD 1050 and 1100. +* **[The Chaco World Network AD 1050-1100](data/AD1050net.csv)** - Adjacency matrix of binarized network of ceramic similarity for sites with Chacoan architectural features dating between AD 1050 and 1100. +* **[San Pedro Networks through Time](data/Figure6_20.Rdata)** - An .RData file that contains `igraph` network objects for the San Pedro region ceramic similarity networks for AD1250-1300, AD1300-1350, and AD1350-1400. + +![Map of the cyberSW project study area showing all sites in the database with the San Pedro and Chaco World subsets of the database shaded.](images/Fig.2.5.png){width=100%} + +### Cibola Region Technological Similarity Networks{#Cibola} + +The Cibola region along the Arizona and New Mexico border in the U.S. Southwest is a large and diverse physiographic region spanning the southern edge of the Colorado Plateau and the ancestral homeland of the contemporary Zuni (A:shiwi) people. Peeples and colleagues (Peeples 2011, 2018; Peeples et al. 2021) have explored patterns of technological similarity and communities of practice in this region at a series of sites dating ca. A.D. 1100-1350 through explorations of corrugated ceramic cooking pots. Corrugated pots, which are produced across much of the U.S. Southwest from at least the 9th through the 14th centuries, are coiled ceramic vessels where the coils used to make the vessel are never fully smoothed. Thus, these ceramics retain substantial amounts of evidence of the specific techniques used to produce them. + +In the book we use data on ceramic technological production techniques to generate similarity networks originally published by Peeples (2011; 2018). In these networks each settlement is treated as a node with similarity metrics defining the weights of edges between pairs of sites based on an analysis of a number of metric and coded attributes of individual ceramic vessels. In addition to these material cultural data, we also have additional site attributes such as location and the types and frequency of public architectural features. + +Ceramic technological data from Peeples (2018): Additional data and documentation from this project is available on tDAR [in this collection](https://core.tdar.org/project/427899/connected-communities-networks-identity-and-social-change-in-the-ancient-cibola-world). Nodes are defined as individual settlements with edges defined based on similarities in the technological attributes of cooking pots recovered at those settlements. For more details on the methods and assumptions used to define these networks see Peeples (2018, pg. 100-104). + +* **[Cibola Ceramic Technological Clusters](data/Cibola_clust.csv)** - Counts of ceramic technological clusters for sites in the Cibola region sample. +* **[Cibola Site Attributes](data/Cibola_attr.csv)** - Site location, public architectural feature types, and sub-region designations for sites in the Cibola region sample. +* **[Cibola Binary Network Edge List](data/Cibola_edgelist.csv)** - Binary edge list of Cibola technological similarity network. +* **[Cibola Binary Network Adjacency Matrix](data/Cibola_adj.csv)** - Binary adjacency matrix of Cibola technological similarity network. +* **[Peeples2018.Rdata](data/Peeples2018.Rdata)** - This file contains a number of objects in R format including the site attributes (`site_info`), a symmetric Brainerd-Robinson similarity matrix (`ceramic_br`), a binary network object in the `statnet/network` format (`brnet`), and a weighted network object in the `network` format (`brnet_w`) + +![Network graph showing connections among Cibola region settlements based on strong similarities in the technological attributes of corrugated cooking pots recovered at each site. Sites are colour coded by region where sites in the northern half of the study area are shown in black and sites in the southern half are shown in white.](images/Fig.2.6.png){width=100%} + +### Himalayan Visibility Networks{#Himalaya} + +Hundreds of forts and small fortified structures are located on mountain tops and ridges in the central Himalayan region of Garhwal in Uttarakhand (India). Despite being such a prominent feature of the history of the region that is interwoven with local folklore (Garhwal is derived from 'land of forts'), this fortification phenomenon has received very little research attention. It might have had its origins during the downfall of the Katyuri dynasty in the 11th century and continued up to the 15th century when the region was consolidated by the Parmar dynasty and possibly even later as attested by Mughal, Tibetan, and British aggression. + +In the book we use this research context as an example of spatial networks and more specifically visibility networks.This is made possible thanks to the survey of forts in the region performed in the context of the PhD project by Dr Nagendra Singh Rawat (2017). We use a catalog of 193 sites (Rawat et al. 2020, Appendix S1), and use the case of Chaundkot fort and its surroundings as a particular case study. Chaundkot fort is theorized to have been one of the key strongholds in the region and is also the only one to have been partly excavated (Rawat and Nautiyal 2020). In these case studies we represent strongholds as nodes, and the ability for a line-of-sight to exist between observers located at a pair of strongholds is represented by a directed edge. The length of each line-of-sight is represented by an edge attribute. + +* **[Himalayan Node data](data/Himalaya_nodes.csv)** - Node attribute data for the Himalayan sites including locations in lat/long, elevation, site name/type, and descriptions of landscape features. +* **[Himalayan Edge List](data/Himalaya_visibility.csv)** - Edge list data with information on connections among nodes within 25kms of each other with information on the distance and whether or not the target site is visible from the source. Note that only edges with `Visible = TRUE` should be included as activated edges. + +![The 193 strongholds (nodes) connected by lines-of-sight up to 25km in length (at which distance large fire and smoke signals would have been visible). Node colours represent communities of nodes identified through the Louvain modularity method (see section 4.4.6) only for lines-of-sight up to 15km (see Rawat et al. 2021).](images/Fig.2.7.png){width=100%} + +### Archaeological Publication Networks{#ArchPubs} + +Our knowledge and stories of past human behavior are as much shaped by the material remains we excavate, as they are by the actions and interactions of the archaeologists that study them. Aspects of these actions and interactions are formally represented in publications. Such papers can be co-authored, reflecting scientific collaboration networks and communities of practice. Authors cite other authors’ works to indicate explicitly that they were influenced by it or that it is related to the paper’s subject matter. + +In previous work, we have turned the tools of archaeological network science on archaeological network researchers themselves (Brughmans 2013; Brughmans and Peeples 2017). We studied the co-authorship and citation practices of the more than 250 publications that have applied formal network methods to archaeological research topics from 1968 to the present. From a list of publications, an undirected co-authorship network can be made by representing individual authors as nodes, and connecting a pair of authors with an edge if they have been co-authors on one or more papers, with edge values representing the number of papers they co-authored. Moreover, a directed citation network can be made from the bibliographies of this list of publications. In a citation network, each node represents an individual publication which is connected to all other publications in its bibliography with a directed edge. The edge goes from the citing publication to the cited publication, so it represents the source and direction of academic influence as explicitly expressed in publication. We use networks of archaeological network research publications throughout this volume to illustrate concepts like the acyclic structure of citation networks. + +* **[Publication Networks Attribute Data](data/biblio_attr.csv)** - Attribute data table including information on publications including a unique key identifier, publication type, publication title, publication date, and the author list separated by semi-colons. +* **[Publication Networks Co-Authorship Incidence Matrix](data/biblio_dat.csv)** - An incidence matrix with unique publications as rows and authors as columns. + +![Two-mode archaeological publication network, representing a set of individual authors as nodes who are connected to nodes in a set of publication venues (journals, books, proceedings) in which they have published (see Brughmans and Peeples 2017:Fig. 10).](images/Fig.2.8.png){width=100%} + +### Iron Age Sites in Southern Spain{#Guadalquivir} + +The Guadalquivir river valley in the south of Spain between present-day Seville and Córdoba was densely urbanized in the late Iron Age (early 5th c. B.C. to late 3rd c. B.C.). Many settlements were dotted along the rivers and the southern part of the valley (Fig. 2.6), and this settlement pattern was focused on nuclear settlements sometimes referred to as oppida. Some of these reveal defensive architecture and many are located on elevations. Previous studies of Iron Age settlements in the region have explored possible explanations for their locations (Keay and Earl 2011; Brughmans et al. 2014, 2015). Given their elevated locations, one theory that has received considerable attention was intervisibility. Could small settlements surrounding oppida be seen from them, and could oppida be located partly to allow for visual control over surrounding settlements? Did groups of Iron Age settlements tend to be intervisible, forming communities that were visible on a daily basis? Were there chains of intervisibility that allowed for passing on information from one site to another via visual smoke or fire signals, and did these chains follow the other key communication medium in the area: the navigable rivers? + +These questions have been explored in previous research using GIS and network methods, using a data set of 86 sites and lines-of-sight connecting pairs of Iron Age settlements at distances up to 20km at which large fire and smoke signals would be visible (more about this data set and research topic: Keay and Earl 2011; Brughmans et al. 2014, 2015). To account for errors in the Digital Elevation Model (DEM), a probabilistic line-of-sight analysis was performed that introduces random errors into the DEM which can have a blocking or enhancing effect on the lines-of-sight. The locations of these 86 sites and the network displayed in figure 2.9 are also available as Appendix A in Brughmans et al. 2014. These locations are used in Chapter 7 of the book to illustrate spatial network models that explore different geographical structures that might underlie the settlement pattern. + +* **[Guadalquivir settlement data](data/Guadalquivir.csv)** - Site number and locations in decimal degrees for all sites in the Guadalquivir survey area. + +![The lower Guadalquivir river valley with the 86 Iberian (Iron Age II) sites used in the case study. Note the clustering of sites around the rivers. Lines-of-sight with >50% probability shown. (Source: Brughmans et al. 2014:Fig. 6b.)](images/Fig.2.9.jpg){width=100%} + +## Importing Data in R{#Importing} + +This section briefly describes how the data provided above (or your own data) can be imported in to R for further analyses (see [Working With Files](#WorkingWithFiles) for more info). Before running the code below, however, you need to ensure that your R session is set to the correct working directory (the location where you placed the .csv files you just downloaded). To do that, go to the menu bar at the top and click Session > Set Working Directory > Choose Directory and navigate to the place on your hard drive where these files reside. + +For this example we will read in the `Cibola_edgelist.csv` file and define an object called `el1` which includes the data in that file using the `read.csv()` command. Note that in this case the file we want to read is in a sub-folder of our working directory called "data" so we need to use the `data/` prefix before the file name to correctly call that file. If you do not chose to use a sub-folder or if you call your folder something else, you will need to modify the `data/` section of the code. + + +```r +# read in data with first row representing column names (header=TRUE) +el1 <- read.csv(file = "data/Cibola_edgelist.csv", header = TRUE) +# look at the first few rows +head(el1) +``` + +``` +## FROM TO +## 1 Apache Creek Casa Malpais +## 2 Apache Creek Coyote Creek +## 3 Apache Creek Hooper Ranch +## 4 Apache Creek Horse Camp Mill +## 5 Apache Creek Hubble Corner +## 6 Apache Creek Mineral Creek Pueblo +``` + +In addition to the .csv files, several examples in this book and several of the data sets above are provide as .RData files which can be read directly in R and can contain multiple R objects. These can be read directly into the R environment using the `load()` function. See the example below. Again note that you must specify the specific directory within the working directory where the file is located. + + +```r +load("data/map.RData") +``` + +## Required/Suggested R Packages{#PrimaryPackages} + +
+

In this appendix we rely on a number of pre-existing R packages. In +order to use these packages in a new installation of R and R-studio, you +first need to install them. Note that you will only need to do this once +on a new installation of R. To install packages, you can click on the +“Packages” tab in the window in the bottom right of R studio, then click +the “Install” button at the top and type the names of the packages +separated by commas. Alternatively you can install packages from the +console by simply typing +install.packages("nameofpackagehere").

+
+ +`install.packages(c("statnet", "igraph"))` + +We use a number of R packages in the modules here and in the book for manipulating and analyzing network data and for other general analyses and procedures. The most frequently used network packages include: + +* **[igraph (Csardi and Nepusz 2006)](https://igraph.org/)** - analytical routines for simple graphs and graph analysis +* **[statnet (Krivitsky et al. 2020)](http://statnet.org/)** - A suite of packages designed for the management and statistical analysis of networks including `network`, `sna`, `ergm`, and others. +* **[intergraph (Bojanowski 2015)](https://cran.r-project.org/web/packages/intergraph/intergraph.pdf)** - a set of routines for coercing objects between common network formats in R +* **[ggraph (Pederson 2021)](https://CRAN.R-project.org/package=ggraph)** - a powerful graph visualization package that is based off of the ggplot2 plotting format + +Throughout this Online Companion, we will consistently rely on `igraph` and `statnet` (`statnet` is actually a suite of packages that includes `sna`, `network`, `ergm`, and others). For the most part these two packages do many of the same things. You can use them to calculate centrality metrics, define groups, or evaluate other network structures. In general `igraph` is a bit more centered on complex networks and mathematical models and `statnet` and affiliated packages are more focused on social network analysis though there is considerable overlap. + +Although `igraph` and the `statnet` suite of packages have many of the same features, they are not directly compatible and use different network formats to store data in R. Adding to the potential confusion, function call names are often the same between the two packages. For example degree centrality is calculated using a `degree()` function in both. If you simply use the `degree()` call R will use the function from whichever package was initialized most recently. If this is the wrong package for your data format, you will get an error. In order to avoid such errors and to clear up ambiguity we use the package name followed by `::` in the function call (i.e., `igraph::function_name` or `sna::function_name`) so that R knows which package we intend to use. You can do this with any R function where you want to specify the package (`package::function_name`). + +In general in this Online Companion we use the `igraph` package wherever possible as we find the data format and especially the functions for converting between network data types to be the most useful and intuitive for most kinds of analyses. We use `statnet` and affiliated packages in specific cases where `igraph` lacks specific functionality or important features. Luckily the package called `intergraph` lets us easily convert network objects from one format to another as we will see in the examples ahead. + +Finally, we recommend installing `ggraph` as this is a very useful and intuitive package that allows for diverse network visualizations and customization. These four packages account for the bulk of the examples in this book. We discuss the use of this package in detail in the [visualization section](#Visualization) of this guide. + +### Should I Just Install Everything?{#ShouldIInstall} + +No matter how you plan on working with these documents, you should install the packages in the following chunk at a minimum by running the code below. These packages will get you through everything in Sections 1 through 4 and much of the Sections 5 and 6. + + +```r +packages <- c("igraph", "statnet", "intergraph", "ggraph", + "reshape2", "ggmap", "vegan", "sf") + +install.packages(setdiff(packages, rownames(installed.packages()))) +``` + +If you have plenty of disk space and time and don't wont to worry about installing packages piecemeal, you can install everything at the same time using the code below. Note that there are a large number of packages and dependencies here and many are only used in one or two places in this Online Companion. Most of the packages are used in the [network visualization](#Visualization) section for making plots with very specific features. We generally recommend that you install packages as you need them while you work through this document but you do you. + +If you choose to install everything, however, you can simply run the chunk of code below. Note that the code below will not re-install packages already installed in your current version of R. Note if you are familiar with Git and R Environments, it will be much faster to just use the `renv::restore()` function to build an environment from the repository. See the [Reproducibility section](#Repro) in the introduction for more information. + + +```r +packages <- c("tidyverse", "ape", "devtools", "igraph", "statnet", "intergraph", + "tnet", "ggplot2", "rjson", "d3r", "cccd", "networkD3", "visNetwork", + "GISTools", "sf", "igraphdata", "ggrepel", + "ggsn", "tidyverse", "superheat", "ggplotify", "ggforce", "colorspace", + "ggmap", "dplyr", "ggpubr", "ggraph", "reshape2", "multinet", + "RColorBrewer", "Rcpp", "deldir", "vegan", "geosphere", "networkDynamic", + "scatterplot3d", "patchwork", "concaveman", "latticeExtra", + "orca", "pracma", "netdiffuseR", "graphkernels") + +install.packages(setdiff(packages, rownames(installed.packages()))) + +devtools::install_github("liamgilbey/ggwaffle") +devtools::install_github("QiliShi/NetworkSim") + +if (!requireNamespace("BiocManager", quietly = TRUE)) + install.packages("BiocManager") +BiocManager::install("RBGL") +``` + + +
+

In addition to the R packages listed above, there are a couple of +procedures used in this Online Companion (especially in the “Going +Beyond the Book” section) that require you to have an installation of +Python 3.8 with particular packages associated with it. In order to +implement these sections of code, you will need to also run the +following lines of code. Note that this is a large install that takes +about about 1.4 GB of hard drive space so only do this if you have the +space and REALLY want to explore edge bundling (see Edge Bundling Visualizations) or network comparison methods. You will be +able to reproduce everything in this document except for two chunks of +code in the main section and a few procedures in the Comparing Networks +section without this so feel free to sit this one out.

+
+ +To install Python with the required libraries, run the following chunk of code. Keep in mind this will take several minutes and about 1.4 GB of disk space: + + +```r +install.packages("edgebundle", "reticulate") +library(edgebundle) +library(reticulate) +install_bundle_py(method = "auto", conda = "auto") +``` + +### R Environment{#Environment} + +This version of the book was built with R version 4.2.2 (2022-10-31 ucrt) and the following packages: + + +|package |version |source | +|:--------------|:-------|:--------------| +|ape |5.7-1 |CRAN (R 4.2.3) | +|bookdown |0.37 |CRAN (R 4.2.3) | +|bslib |0.6.1 |CRAN (R 4.2.3) | +|ca |0.71.1 |CRAN (R 4.2.3) | +|cccd |1.6 |CRAN (R 4.2.3) | +|colorspace |2.1-0 |CRAN (R 4.2.3) | +|concaveman |1.1.0 |CRAN (R 4.2.3) | +|d3r |NA |NA | +|deldir |2.0-2 |CRAN (R 4.2.3) | +|devtools |2.4.5 |CRAN (R 4.2.3) | +|downlit |0.4.3 |CRAN (R 4.2.3) | +|dplyr |1.1.4 |CRAN (R 4.2.3) | +|edgebundle |0.4.2 |CRAN (R 4.2.3) | +|geosphere |1.5-18 |CRAN (R 4.2.3) | +|ggforce |0.4.1 |CRAN (R 4.2.3) | +|ggmap |4.0.0 |CRAN (R 4.2.3) | +|ggplot2 |3.4.4 |CRAN (R 4.2.3) | +|ggplotify |0.1.2 |CRAN (R 4.2.3) | +|ggpubr |0.6.0 |CRAN (R 4.2.3) | +|ggraph |2.1.0 |CRAN (R 4.2.3) | +|ggrepel |0.9.4 |CRAN (R 4.2.3) | +|ggsn |NA |NA | +|GISTools |NA |NA | +|graphkernels |1.6.1 |CRAN (R 4.2.3) | +|igraph |2.0.1.1 |CRAN (R 4.2.2) | +|intergraph |2.0-3 |CRAN (R 4.2.3) | +|knitr |1.45 |CRAN (R 4.2.3) | +|latticeExtra |0.6-30 |CRAN (R 4.2.3) | +|Matrix |1.6-5 |CRAN (R 4.2.3) | +|netdiffuseR |1.22.6 |CRAN (R 4.2.3) | +|networkD3 |0.4 |CRAN (R 4.2.3) | +|networkDynamic |0.11.4 |CRAN (R 4.2.3) | +|patchwork |1.1.3 |CRAN (R 4.2.3) | +|pracma |2.4.4 |CRAN (R 4.2.3) | +|RColorBrewer |1.1-3 |CRAN (R 4.2.0) | +|Rcpp |1.0.12 |CRAN (R 4.2.3) | +|reshape2 |1.4.4 |CRAN (R 4.2.3) | +|reticulate |1.35.0 |CRAN (R 4.2.3) | +|rjson |NA |NA | +|rmarkdown |2.25 |CRAN (R 4.2.3) | +|scatterplot3d |NA |NA | +|sf |1.0-15 |CRAN (R 4.2.3) | +|SparseM |1.81 |CRAN (R 4.2.0) | +|statnet |2019.6 |CRAN (R 4.2.3) | +|superheat |0.1.0 |CRAN (R 4.2.3) | +|tidyverse |2.0.0 |CRAN (R 4.2.3) | +|vegan |2.6-4 |CRAN (R 4.2.3) | +|visNetwork |2.1.2 |CRAN (R 4.2.3) | +|xml2 |1.3.6 |CRAN (R 4.2.3) | + +## Suggested Workspace Setup{#WorkspaceSetup} + +In order to follow along with the examples in this Online Companion it will be easiest if you set up your R working directory in a similar format to that used in creating it. Specifically, we suggest you create a new working directory and create an R studio project tied to that specific directory. + +In order to do this, open R-Studio and go to "File > New Project" and click on "New Directory > New Project" in the dialog and then give it an appropriate name and location on your computer. Next, navigate to that location on your computer and create two sub-folders: one called "data" and one called "scripts" (directory names are case sensitive). Place any of the data files you downloaded above or in any other section of this Online Companion in the "data" folder and any R script files you download in the "scripts" folder. + +
+

Note that if you chose the “Just Give Me Everything” download you +will have a .zip file that already contains a sub-folder called “data” +so be sure you’re not double nesting your folders (you want +“working_directory/data” not “working_directory/data/data”).

+
+ +When you close R you will see a dialog that asks if you want to save your work space image. If you do this and provide a name, you can reopen the .RData file at a later time and pick up exactly where your previous session left off. + +If you are new to the R environment and file structures, we suggest you review the [Getting Started with R](#GettingStarted) section for more information. diff --git a/02-network-data-formats.Rmd b/02-network-data-formats.Rmd index 10c65f2..a77f52b 100644 --- a/02-network-data-formats.Rmd +++ b/02-network-data-formats.Rmd @@ -280,7 +280,7 @@ weighted_net <- E(weighted_net)$weight <- cibola_edgelist$weight # Explore the first few rows and columns of network object -head(get.data.frame(weighted_net)) +head(igraph::as_data_frame(weighted_net)) # View network as adjacency matrix. Notice the attr="weight" command that # indicates which edge attribute to use for values in the matrix diff --git a/02-network-data-formats.md b/02-network-data-formats.md new file mode 100644 index 0000000..affbaa8 --- /dev/null +++ b/02-network-data-formats.md @@ -0,0 +1,1373 @@ +# Network Data in R {#NetworkData} + +![](images/image_break.png){width=100%} + +A network is simply a set of entities and the formally defined relationships among them. There are, however, many different ways that networks can be encoded and displayed. This section provides examples of many of the most common network formats and data types discussed in Chapter 3 of Brughmans and Peeples 2023. For most of the examples below we use the Cibola technological similarity network data set (described in Chapter 2.8.3 and [here](#Cibola)) because it is relatively small and easy to display in a variety of formats. + +Throughout this document, we refer to the unique bounded entities connected in a formal network as **nodes** and the connections between them as **edges** but note that there are many other terms used in the literature and in the documentation for the R packages used here. Nodes are often referred to as **vertices** or **actors** and edges are often referred to as **ties** or **links**. Note that we use **network** to refer to the formal system of interdependent pairwise relationships (edges) among a set of entities (nodes) but the term **graph** is often used equivalently in mathematics and other fields. + +## Network Data Formats{#NetworkDataFormats} + +This section follows Chapter 3.2 in Brughmans and Peeples (2023) to provide examples of the same network and attribute data in a variety of different data formats as well as code for converting among these formats in R. + +The network data formats we discuss in this section include: + +* **Edge list** - A network data format consisting of a list of connected node pairs. *E=((n1,n2),(n1,n3),(n1,n4),...,(ni,nj))*. It can also be represented as a matrix with two columns for source and target nodes respectively and with one edge per row. +* **Adjacency list** - A network data format consisting of a set of rows, where the first node in each row is connected to all subsequent nodes in that same row. +* **Adjacency matrix** - A network data format consisting of a matrix of size *n x n*, with a set of rows equal to the number of nodes, and a set of columns equal to the number of nodes. When a pair of nodes is connected by an edge (i.e., when they are adjacent), then the corresponding cell will have an entry. +* **Incidence matrix** - A network data format consisting of a matrix of size *n x e*, with a set of rows equal to the number of nodes, and a set of columns equal to the number of edges. An entry is made in a cell if the corresponding node and edge are connected. Each column in the incidence matrix has two entries. + +Let's first get started by initializing all of the packages we will use in this section. + + +```r +# initialize packages +library(igraph) +library(statnet) +library(intergraph) +library(vegan) +library(multinet) +``` + +
+

The primary packages used in this Section (igraph, +statnet, and intergraph) are already described +in the last section. We also use here the +vegan package which includes many functions focused on +community ecology. In this document, we rely on this package to +calculate several distance/similarity metrics that are useful for +generating similarity networks. +Finally, we provide a brief example at the end of this section using +multinet which is a package focused on conducting multilayer network analyses.

+
+ +### Defining Network Objects in R{#NetworkDataFunctions} + +In general most of the examples of network data formats in the remainder of this section are converted into R network objects in two basic steps. + +* First, we read in some external data file that contains network data, usually generated in some sort of spreadsheet program, and create an R data frame or matrix object. +* Next, we call a function that expects the given data format (edge list, adjacency matrix, incidence matrix, etc.) and converts it in to an R network object. + +As you will see below, we mostly rely on `igraph` functions that take the following basic format: + +`igraph::graph_from_**DataType**` + +where `**DataType**` is replaced with the appropriate format such as `edgelist`, `adjacency_matrix`, and so on. In most of the examples below we then plot the network just to confirm that everything works, but that is certainly optional. + +### Edge List {#Edgelist} + +The edge list is a very quick and easy way to capture network data. It simply lists the edges in the network one by one by node id: *E=((n1,n2),(n1,n3),(n1,n4),...,(ni,nj))*. For the purposes of data management it is usually easiest to create an edge list as a data frame or matrix where each row represents a pair of nodes with connections going from the node in one column to the node in the second column (additional columns can be used for edge weight or other edge attributes). + +In this example, we import the Cibola data set in this format as a data frame and then convert it to an `igraph` network object for further analysis. You can download the [edge list file here](data/Cibola_edgelist.csv) to follow along on your own. Since the edges in this network are undirected this will be a simple binary network, and we will use the `directed = FALSE` argument in the `igraph::graph_from_edgelist` function call. This function simply takes a edge list in tabular format and converts it to a network object R recognizes that can further be used for analysis and visualization. + + +```r +# Read in edge list file as data frame +cibola_edgelist <- + read.csv(file = "data/Cibola_edgelist.csv", header = TRUE) + +# Examine the first several rows +head(cibola_edgelist) +``` + +``` +## FROM TO +## 1 Apache Creek Casa Malpais +## 2 Apache Creek Coyote Creek +## 3 Apache Creek Hooper Ranch +## 4 Apache Creek Horse Camp Mill +## 5 Apache Creek Hubble Corner +## 6 Apache Creek Mineral Creek Pueblo +``` + +```r +# Create graph object. The data frame is converted to a matrix as that +#is required by this specific function. Since this is an undirected +# network directed = FALSE. +cibola_net <- + igraph::graph_from_edgelist(as.matrix(cibola_edgelist), + directed = FALSE) + +# Display igraph network object and then plot a simple node-link diagram +cibola_net +``` + +``` +## IGRAPH 0180d4c UN-- 30 167 -- +## + attr: name (v/c) +## + edges from 0180d4c (vertex names): +## [1] Apache Creek--Casa Malpais Apache Creek--Coyote Creek +## [3] Apache Creek--Hooper Ranch Apache Creek--Horse Camp Mill +## [5] Apache Creek--Hubble Corner Apache Creek--Mineral Creek Pueblo +## [7] Apache Creek--Rudd Creek Ruin Apache Creek--Techado Springs +## [9] Apache Creek--Tri-R Pueblo Apache Creek--UG481 +## [11] Apache Creek--UG494 Atsinna --Cienega +## [13] Atsinna --Los Gigantes Atsinna --Mirabal +## [15] Atsinna --Ojo Bonito Atsinna --Pueblo de los Muertos +## + ... omitted several edges +``` + +```r +# Set random seed to ensure graph layout stays the same each time. +set.seed(3523) +plot(cibola_net) +``` + + + +### Adjacency List{#AdjacencyList} + +The adjacency list consists of a set of rows, where the first node in each row is connected to all subsequent nodes in that same row. It is therefore more concise than the edge list (in which each relationship has its own row), but unlike the edge list it does not result in rows of equal length (each row in an edge list typically has two values, representing the pair of nodes). Adjacency lists are relatively rare in practice but can sometimes be useful formats for directly gathering network data in small networks and are supported by many network analysis software packages. + +In the following chunk of code, we convert the network object we created above into an adjacency list using the `igraph::as_adj_edge_list()` function and examine a couple of the rows. + + +```r +# Convert edge list to adjacency list using igraph function +adj_list <- igraph::as_adj_edge_list(cibola_net) + +# examine adjacency list for the site Apache Creek +adj_list$`Apache Creek` +``` + +``` +## + 11/167 edges from 0180d4c (vertex names): +## [1] Apache Creek--Casa Malpais Apache Creek--Coyote Creek +## [3] Apache Creek--Hooper Ranch Apache Creek--Horse Camp Mill +## [5] Apache Creek--Hubble Corner Apache Creek--Mineral Creek Pueblo +## [7] Apache Creek--Rudd Creek Ruin Apache Creek--Techado Springs +## [9] Apache Creek--Tri-R Pueblo Apache Creek--UG481 +## [11] Apache Creek--UG494 +``` + +```r +# It is also possible to call specific nodes by number. In this case, +# site 2 is Casa Malpais +adj_list[[2]] +``` + +``` +## + 11/167 edges from 0180d4c (vertex names): +## [1] Apache Creek--Casa Malpais Casa Malpais--Coyote Creek +## [3] Casa Malpais--Hooper Ranch Casa Malpais--Horse Camp Mill +## [5] Casa Malpais--Hubble Corner Casa Malpais--Rudd Creek Ruin +## [7] Casa Malpais--Techado Springs Casa Malpais--Tri-R Pueblo +## [9] Casa Malpais--UG481 Casa Malpais--Garcia Ranch +## [11] Casa Malpais--Hinkson +``` + +The output for a particular node can be called by either referencing the name using using `$` followed by the site name or `[[k]]` double brackets where `k` is the row number of the node in question. The printed output is essentially a list of all of the edges incident on the node in question identified by the name of the sending and receiving node. + +### Adjacency Matrix{#AdjacencyMatrix} + +The adjacency matrix is perhaps the most common and versatile network data format for data analysis in network science (in sociology it is sometimes referred to as the sociomatrix). It is a symmetric matrix of size *n x n*, with a set of rows and columns denoting the nodes in that network. The node names or identifiers are typically used to label both rows and columns. When a pair of nodes is connected by an edge (i.e. when they are adjacent), the corresponding cell will have an entry. The diagonal of this matrix represents "self loops" and can variously be defined as connected or unconnected depending on the application. + +We can obtain an adjacency matrix object in R by converting our network object created above or by reading in a file directly with rows and columns denoting site and with 0 or 1 denoting the presence or absence of a relation. We take the data frame object `adj_mat` which is a square matrix of 1s and 0s and convert it into a network object using the `igraph::graph_from_adjacency_matrix()` function. You can download the csv file to follow along on your own [here](data/Cibola_adj.csv). + + +```r +# Convert to adjacency matrix then display first few rows/columns +adj_mat <- igraph::as_adjacency_matrix(cibola_net) +adj_mat[1:5, 1:5] +``` + +``` +## 5 x 5 sparse Matrix of class "dgCMatrix" +## Apache Creek Casa Malpais Coyote Creek Hooper Ranch +## Apache Creek . 1 1 1 +## Casa Malpais 1 . 1 1 +## Coyote Creek 1 1 . 1 +## Hooper Ranch 1 1 1 . +## Horse Camp Mill 1 1 1 1 +## Horse Camp Mill +## Apache Creek 1 +## Casa Malpais 1 +## Coyote Creek 1 +## Hooper Ranch 1 +## Horse Camp Mill . +``` + +```r +# Read in adjacency matrix and convert to network object for plotting +adj_mat2 <- + read.csv(file = "data/Cibola_adj.csv", + header = T, + row.names = 1) + +adj_mat2[1:4, 1:4] +``` + +``` +## Apache.Creek Atsinna Baca.Pueblo Casa.Malpais +## Apache Creek 0 0 0 1 +## Atsinna 0 0 0 0 +## Baca Pueblo 0 0 0 0 +## Casa Malpais 1 0 0 0 +``` + +```r +cibola_net2 <- + igraph::graph_from_adjacency_matrix(as.matrix(adj_mat2), + mode = "undirected") +set.seed(4352) +plot(cibola_net2) +``` + + + +Note when you compare this network graph to the one produced based on the edge list there is an additional unconnected node (WS Ranch) that was not shown in the previous network. This is one of the advantages of an adjacency matrix is it provides a way of easily including unconnected nodes without having to manually add them or include self-loops. + +### Incidence Matrix{#IncidenceMatrix} + +An incidence matrix is most frequently used to define connections among different sets of nodes in a two-mode or bipartite network where the rows and columns represent two different classes of nodes and the presence/absence or value of an edge is indicated in the corresponding cell. + +By way of example here we can read in the data that were used to generate the one-mode networks of ceramic technological similarity we have been examining so far. In the corresponding data frame, each row represents a site and each column represents a specific cluster of technological attributes in cooking pottery (see Peeples 2018, pg. 100-104 for more details) with the number in each cell representing the count of each technological cluster at each site. + +After reading in this rectangular data frame, we can create a network object using the `igraph::graph_from_incidence_matrix()` function. We then plot it as a simple two-mode network with color representing node class. We discuss plotting options in greater detail in the visualization section of this document. You can download the csv file to follow along on your own [here](data/Cibola_clust.csv). + + +```r +# Read in two-way table of sites and ceramic technological clusters +cibola_clust <- + read.csv(file = "data/Cibola_clust.csv", + header = TRUE, + row.names = 1) +head(cibola_clust) +``` + +``` +## Clust1 Clust2 Clust3 Clust4 Clust5 Clust6 Clust7 Clust8 Clust9 +## Apache Creek 7 3 6 16 6 1 1 2 0 +## Atsinna 0 12 26 5 0 1 6 0 7 +## Baca Pueblo 0 9 3 12 1 2 5 0 16 +## Casa Malpais 2 15 7 28 17 16 2 5 1 +## Cienega 2 28 34 2 0 10 11 0 5 +## Coyote Creek 10 13 8 30 20 5 1 8 0 +## Clust10 +## Apache Creek 0 +## Atsinna 0 +## Baca Pueblo 1 +## Casa Malpais 0 +## Cienega 1 +## Coyote Creek 5 +``` + +```r +# Convert into a network object using the incidence matrix format. Note that +# multiple=TRUE as we want this defined as a bipartite network. +cibola_inc <- + igraph::graph_from_incidence_matrix(cibola_clust, + directed = FALSE, + multiple = TRUE) +head(cibola_inc) +``` + +``` +## 6 x 41 sparse Matrix of class "dgCMatrix" +## +## Apache Creek . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 +## Atsinna . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +## Baca Pueblo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +## Casa Malpais . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 +## Cienega . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 +## Coyote Creek . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 +## +## Apache Creek 3 6 16 6 1 1 2 . . +## Atsinna 12 26 5 . 1 6 . 7 . +## Baca Pueblo 9 3 12 1 2 5 . 16 1 +## Casa Malpais 15 7 28 17 16 2 5 1 . +## Cienega 28 34 2 . 10 11 . 5 1 +## Coyote Creek 13 8 30 20 5 1 8 . 5 +``` + +```r +set.seed(4543) +# Plot as two-mode network +plot(cibola_inc, vertex.color = as.numeric(V(cibola_inc)$type) + 1) +``` + + + +### Node and Edge Information{#NodeAttributes} + +Frequently we want to use other information about nodes and edges (node location, site type, edge weight, etc.) in our analyses and need to track these data in a separate attribute object or data column. One common way to do this is to simply create a data frame that contains the required attribute information and call specific data from this data frame when needed. As the following example shows, it is also possible to directly assign attributes to nodes or edges in an `igraph` network object and use those for subsequent analyses using the `V()` for nodes (V standing for vertices) and `E()` for edges calls within `igraph`. + +In the following example we use [this file](data/Cibola_attr.csv) which includes basic attribute data by site (node) for all sites in the network we've been working with here. This file includes x and y coordinates for the sites, information on the presence/absence and shape of Great Kiva public architectural features at those sites, and the Region to which they have been assigned. First we read in the data. + + +```r +# Read in attribute data and look at the first few rows. +cibola_attr <- read.csv(file = "data/Cibola_attr.csv", header = TRUE) +head(cibola_attr) +``` + +``` +## Site x y Great.Kiva Region +## 1 Apache Creek 724125 3747310 Rectangular Great Kiva Mogollon Highlands +## 2 Atsinna 726741 3895499 none El Morro Valley +## 3 Baca Pueblo 651431 3797143 none Upper Little Colorado +## 4 Casa Malpais 659021 3786211 Rectangular Great Kiva Upper Little Colorado +## 5 Cienega 738699 3887985 none El Morro Valley +## 6 Coyote Creek 671154 3780509 Rectangular Great Kiva Upper Little Colorado +``` + +In order to assign an attribute to a particular node or edge we can use the `V` and `E` (vertex and edge) calls in `igraph`. For example, in the following example, we will assign a `region` variable to each node in the network we created above using the `V` function to assign a vertex attribute. You simply type the name of the network object in the parenthesis after `V` and use the `$` atomic variable symbol to assign a name to the attribute that will be associated with that network object. + + +```r +# Assign a variable called "region" to the Cibola_net2 based on the +# column in the Cibola_attr table called "Region" +V(cibola_net2)$region <- cibola_attr$Region + +# If we now call that attribute we get a vector listing each assigned value +V(cibola_net2)$region +``` + +``` +## [1] "Mogollon Highlands" "El Morro Valley" "Upper Little Colorado" +## [4] "Upper Little Colorado" "El Morro Valley" "Upper Little Colorado" +## [7] "Mogollon Highlands" "Carrizo Wash" "Pescado Basin" +## [10] "West Zuni" "Upper Little Colorado" "Mariana Mesa" +## [13] "Mariana Mesa" "West Zuni" "El Morro Valley" +## [16] "Vernon Area" "El Morro Valley" "West Zuni" +## [19] "Pescado Basin" "Carrizo Wash" "El Morro Valley" +## [22] "Upper Little Colorado" "El Morro Valley" "West Zuni" +## [25] "Mariana Mesa" "El Morro Valley" "Mariana Mesa" +## [28] "Mariana Mesa" "Mariana Mesa" "Mogollon Highlands" +## [31] "Pescado Basin" +``` + +```r +# Note that "region" is now listed as an attribute when we view +# the network object +cibola_net2 +``` + +``` +## IGRAPH 019b7ea UN-- 31 167 -- +## + attr: name (v/c), region (v/c) +## + edges from 019b7ea (vertex names): +## [1] Apache.Creek--Casa.Malpais Apache.Creek--Coyote.Creek +## [3] Apache.Creek--Hooper.Ranch Apache.Creek--Horse.Camp.Mill +## [5] Apache.Creek--Hubble.Corner Apache.Creek--Mineral.Creek.Pueblo +## [7] Apache.Creek--Rudd.Creek.Ruin Apache.Creek--Techado.Springs +## [9] Apache.Creek--Tri.R.Pueblo Apache.Creek--UG481 +## [11] Apache.Creek--UG494 Atsinna --Cienega +## [13] Atsinna --Los.Gigantes Atsinna --Mirabal +## [15] Atsinna --Ojo.Bonito Atsinna --Pueblo.de.los.Muertos +## + ... omitted several edges +``` + +This can further be used for plotting or other analyses by calling the variable as a factor ([see this resource](https://r4ds.had.co.nz/factors.html) or the [Getting Started with R](#DataTypes) to learn more about the factor data. + + +```r +set.seed(43534) +plot(cibola_net2, vertex.color = as.factor(V(cibola_net2)$region)) +``` + + + +## Types of Networks{#TypesOfNetworks} + +This section roughly follows Brughmans and Peeples (2023) Chapter 3.3 to describe and provide examples in R format of many of the most common types of networks. In the examples below we will use the `igraph` R package but we also show how to use the `statnet` and `network` packages where applicable. + +In this section, we cover: + +* **Simple Networks** - A set of nodes and a set of edges with no additional information about them. +* **Directed Networks** - A network consisting of a set of nodes and edges connecting them for which the orientation or direction is specified. In other words when A is connected to B, B is not necessarily connected to A. +* **Signed, Categorized, and Weighted Networks** - This category refers to networks where edges (relationships) have additional nominal, ordinal, or metric information encoded in them. A signed network is a network where the edges carry a positive or negative sign indicating some opposed property of relations in the network. A categorized network is a network where edges are classified according to some nominal category that does not necessarily represent an opposition. A weighted network is one in which the edges carry a non-binary value which indicates the strength of a given relationship. +* **Two-Mode Networks** - A network where two separate categories of nodes are defined with edges defined only between these categories. +* **Similarity Networks** - Networks where edges are defined or weighted based on a quantitative metric of similarity or distance based on node attributes or artifact assemblages. +* **Ego Networks** - A network including a focal node, the set of nodes the ego is connected to by an edge and the edges between nodes in this set. +* **Multilayer Networks** - A network where a single set of nodes is connected by two or more sets of edges that each represent a different kind of relationship among the nodes. + +### Simple Networks{#SimpleNetworks} + +We call a network a simple network (or simple graph) if we only have a set of nodes and a set of edges connecting them, with no additional information about the edges or specific rules they need to follow. Simple networks are, in other words, unweighted and undirected one-mode networks. By way of example we will use the Cibola region [adjacency matrix file](data/Cibola_adj.csv) and convert it into a simple network using both `igraph` and `network`. Notice how in both examples we specify that this is an undirected network (`mode = "undirected"` and `directed = FALSE`). + + +```r +# Read in raw adjacency matrix file +adj_mat2 <- + read.csv(file = "data/Cibola_adj.csv", + header = T, + row.names = 1) + +# Convert to a network object using igraph +simple_net_i <- + igraph::graph_from_adjacency_matrix(as.matrix(adj_mat2), + mode = "undirected") +simple_net_i +``` + +``` +## IGRAPH 020a3d7 UN-- 31 167 -- +## + attr: name (v/c) +## + edges from 020a3d7 (vertex names): +## [1] Apache.Creek--Casa.Malpais Apache.Creek--Coyote.Creek +## [3] Apache.Creek--Hooper.Ranch Apache.Creek--Horse.Camp.Mill +## [5] Apache.Creek--Hubble.Corner Apache.Creek--Mineral.Creek.Pueblo +## [7] Apache.Creek--Rudd.Creek.Ruin Apache.Creek--Techado.Springs +## [9] Apache.Creek--Tri.R.Pueblo Apache.Creek--UG481 +## [11] Apache.Creek--UG494 Atsinna --Cienega +## [13] Atsinna --Los.Gigantes Atsinna --Mirabal +## [15] Atsinna --Ojo.Bonito Atsinna --Pueblo.de.los.Muertos +## + ... omitted several edges +``` + +```r +# Covert to a network object using statnet/network +simple_net_s <- + network::network(as.matrix(adj_mat2), directed = FALSE) +simple_net_s +``` + +``` +## Network attributes: +## vertices = 31 +## directed = FALSE +## hyper = FALSE +## loops = FALSE +## multiple = FALSE +## bipartite = FALSE +## total edges= 167 +## missing edges= 0 +## non-missing edges= 167 +## +## Vertex attribute names: +## vertex.names +## +## No edge attributes +``` + +Notice how the two formats differ in the way they internally store network data in R and the way they print output to the screen but that both show a total of 31 nodes (vertices) and 167 edges (for the igraph object the first row specifies node and edge numbers between the -- marks). + +### Directed Networks{#DirectedNetworks} + +Sometimes relationships are directional, meaning they have an orientation. For example, the flow of a river is directed downstream. In such cases we can incorporate this information in our network data by distinguishing between the source and the target of an edge. + +By way of example here we will modify the Cibola network edge list to remove some number of edges at random to simulate directed network data. We will then convert these data into various network and matrix formats to illustrate how directed networks are stored and used in R. We first use the `sample` function to define a sub-sample of our network nodes and then we create a network object from that random sub-sample. By randomly removing some edges from the edge list we are left with a directed network where not all edges will be reciprocated. + + +```r +# Read in edge list file as data frame +cibola_edgelist <- + read.csv(file = "data/Cibola_edgelist.csv", header = TRUE) + +# Create a random sub-sample of 125 edges out of the total 167 using +# the "sample" function +set.seed(45325) +el2 <- cibola_edgelist[sample(seq(1, nrow(cibola_edgelist)), 125, + replace = FALSE), ] + +# Create graph object from the edge list using the directed=TRUE argument +# to ensure this is treated as a directed network object. +directed_net <- + igraph::graph_from_edgelist(as.matrix(el2), directed = TRUE) +directed_net +``` + +``` +## IGRAPH 020e4b1 DN-- 30 125 -- +## + attr: name (v/c) +## + edges from 020e4b1 (vertex names): +## [1] Coyote Creek ->Techado Springs +## [2] Hubble Corner ->Tri-R Pueblo +## [3] Hubble Corner ->Techado Springs +## [4] Heshotauthla ->Pueblo de los Muertos +## [5] Rudd Creek Ruin->Techado Springs +## [6] Heshotauthla ->Hinkson +## [7] Los Gigantes ->Yellowhouse +## [8] Los Gigantes ->Pueblo de los Muertos +## + ... omitted several edges +``` + +```r +# View as adjacency matrix of directed network object +(as_adjacency_matrix(directed_net))[1:5, 1:5] +``` + +``` +## 5 x 5 sparse Matrix of class "dgCMatrix" +## Coyote Creek Techado Springs Hubble Corner Tri-R Pueblo +## Coyote Creek . 1 1 . +## Techado Springs . . . 1 +## Hubble Corner . 1 . 1 +## Tri-R Pueblo . . . . +## Heshotauthla . . . . +## Heshotauthla +## Coyote Creek . +## Techado Springs . +## Hubble Corner . +## Tri-R Pueblo . +## Heshotauthla . +``` + +```r +# Plot network +set.seed(4353) +plot(directed_net) +``` + + + +Notice that when we look at the igraph network plot it has arrows indicating the direction of connection in the edge list. If you are making your own directed edge list, the sending node by default will be in the first column and the receiving node in the second column. In the adjacency matrix the upper and lower triangles are no longer identical. Again, if you are generating your own adjacency matrix, you can simply mark edges sent from nodes denoted as rows and edges received from the same nodes as columns. Finally, in the plot, since R recognizes this as a directed igraph object when we plot the network, it automatically shows arrows indicating the direction of the edge. + +### Signed, Categorized, and Weighted Networks{#WeightedNetworks} + +In many situations we want to add values to specific edges such as signs (sometimes called valences), nominal categories, or weights defining the strength or nature of relationships. There are a variety of ways that we can record and assign such weights or values to edges in R. The simplest way is to directly include that information in one of the formats described above such as an edge list or adjacency matrix. For example, we can add a third column to an edge list that denotes the weight, category, or sign of each edge or can fill the cells in an adjacency matrix with specific values rather than simply 1s or 0s. + +In this example, we will randomly generate edge weights for the Cibola network edge list and adjacency matrix to illustrate how R handles these formats. We use the `sample` function again to create a random vector of values between 1 and 4 for every edge in the network and then add it to the edge list as a new variable called `$Weight`. We then convert this data frame into a network object. + + +```r +# Read in edge list file as data frame +cibola_edgelist <- + read.csv(file = "data/Cibola_edgelist.csv", header = TRUE) +# Add additional column of weights as random integers between 1 and 4 +# for each edge +cibola_edgelist$weight <- + sample(seq(1, 4), nrow(cibola_edgelist), replace = TRUE) + +# Create weighted network object calling only the first two columns +weighted_net <- + igraph::graph_from_edgelist(as.matrix(cibola_edgelist[, 1:2]), + directed = FALSE) +# add edge attribute to indicate weight +E(weighted_net)$weight <- cibola_edgelist$weight + +# Explore the first few rows and columns of network object +head(igraph::as_data_frame(weighted_net)) +``` + +``` +## from to weight +## 1 Apache Creek Casa Malpais 4 +## 2 Apache Creek Coyote Creek 1 +## 3 Apache Creek Hooper Ranch 1 +## 4 Apache Creek Horse Camp Mill 3 +## 5 Apache Creek Hubble Corner 4 +## 6 Apache Creek Mineral Creek Pueblo 4 +``` + +```r +# View network as adjacency matrix. Notice the attr="weight" command that +# indicates which edge attribute to use for values in the matrix +head(as_adjacency_matrix(weighted_net, attr = "weight"))[1:5, 1:5] +``` + +``` +## 5 x 5 sparse Matrix of class "dgCMatrix" +## Apache Creek Casa Malpais Coyote Creek Hooper Ranch +## Apache Creek . 4 1 1 +## Casa Malpais 4 . 1 1 +## Coyote Creek 1 1 . 1 +## Hooper Ranch 1 1 1 . +## Horse Camp Mill 3 2 4 4 +## Horse Camp Mill +## Apache Creek 3 +## Casa Malpais 2 +## Coyote Creek 4 +## Hooper Ranch 4 +## Horse Camp Mill . +``` + +```r +# Plot the network +set.seed(574) +plot(weighted_net, edge.width = E(weighted_net)$weight) +``` + + + +Notice in the final plot that line thickness is used to indicate edges with various weights. We will explore further options for such visualizations in the network [visualizations section](#Visualization) of this document. + +### Two-mode Networks and Affiliation Networks{#TwoMode} + +Two-mode networks are networks where two separate categories of nodes are defined with a structural variable (edges) between these categories. In sociology, two-mode networks are often used for studying the affiliation of individuals with organizations, such as the presence of professionals on the boards of companies or the attendance of scholars at conferences (referred to as affiliation networks). + +Two-mode network data are typically recorded in a two-way table with rows and columns representing two different classes of nodes and with individual cells representing the presence/absence or weight of edges between those classes of nodes. By way of example here we will return to the table of ceramic technological clusters by sites for the Cibola region data. The simplest way to create an unweighted two-mode network from these data is to create a network object directly from a two-way table as we saw above. In this example this will create an edge between each site and each technological cluster present there irrespective of relative frequency using the `igraph::graph_from_incidence_matrix` function. + + +```r +# Read in two-way table of sites and ceramic technological clusters +cibola_clust <- read.csv(file = "data/Cibola_clust.csv", + header = TRUE, + row.names = 1) +# Create network from incidence matrix based on presence/absence of +# a cluster at a site +cibola_inc <- igraph::graph_from_incidence_matrix(cibola_clust, + directed = FALSE, + multiple = TRUE) +cibola_inc +``` + +``` +## IGRAPH 02308df UN-B 41 2214 -- +## + attr: type (v/l), name (v/c) +## + edges from 02308df (vertex names): +## [1] Apache Creek--Clust1 Apache Creek--Clust1 Apache Creek--Clust1 +## [4] Apache Creek--Clust1 Apache Creek--Clust1 Apache Creek--Clust1 +## [7] Apache Creek--Clust1 Apache Creek--Clust2 Apache Creek--Clust2 +## [10] Apache Creek--Clust2 Apache Creek--Clust3 Apache Creek--Clust3 +## [13] Apache Creek--Clust3 Apache Creek--Clust3 Apache Creek--Clust3 +## [16] Apache Creek--Clust3 Apache Creek--Clust4 Apache Creek--Clust4 +## [19] Apache Creek--Clust4 Apache Creek--Clust4 Apache Creek--Clust4 +## [22] Apache Creek--Clust4 Apache Creek--Clust4 Apache Creek--Clust4 +## + ... omitted several edges +``` + +```r +set.seed(4537643) +# Plot as two-mode network +plot(cibola_inc, vertex.color = as.numeric(V(cibola_inc)$type) + 1) +``` + + + +In this case since most clusters are present at most sites, this creates a pretty busy network that may not be particularly useful. An alternative to this is to define some threshold (either in terms of raw count or proportion) to define an edge between a node of one class and another. We provide an example here and build a function that you could modify to do this for your own data. In this function you can set the proportion threshold that you would like to used to define an edge between two classes of nodes. If the proportion of that cluster at that site is greater than or equal to that threshold an edge will be present. In the example below the threshold is set at 0.25 meaning that we will define edges between nodes that share a common type that makes of at least a quarter of the assemblage of both sites. + + +```r +# Define function for creating incidence matrix with threshold +two_mode <- function(x, thresh = 0.25) { + # Create matrix of proportions from x input into function + temp <- prop.table(as.matrix(x), 1) + # Define anything with greater than or equal to threshold as + # present (1) + temp[temp >= thresh] <- 1 + # Define all other cells as absent (0) + temp[temp < 1] <- 0 + # Return the new binarized table as output of the function + return(temp) +} + +# Run the function and create network object +# thresh is set to 0.25 but could be any values from 0-1 +mod_clust <- two_mode(cibola_clust, thresh = 0.25) +# Examine the first few rows +head(mod_clust) +``` + +``` +## Clust1 Clust2 Clust3 Clust4 Clust5 Clust6 Clust7 Clust8 Clust9 +## Apache Creek 0 0 0 1 0 0 0 0 0 +## Atsinna 0 0 1 0 0 0 0 0 0 +## Baca Pueblo 0 0 0 0 0 0 0 0 1 +## Casa Malpais 0 0 0 1 0 0 0 0 0 +## Cienega 0 1 1 0 0 0 0 0 0 +## Coyote Creek 0 0 0 1 0 0 0 0 0 +## Clust10 +## Apache Creek 0 +## Atsinna 0 +## Baca Pueblo 0 +## Casa Malpais 0 +## Cienega 0 +## Coyote Creek 0 +``` + +```r +# Create a graph matrix from the new incidence matrix +two_mode_net <- igraph::graph_from_incidence_matrix( + mod_clust, + directed = FALSE, + multiple = TRUE) + +# Plot results +set.seed(4537) +plot(two_mode_net, + vertex.color = as.numeric(V(cibola_inc)$type) + 1) +``` + + + +Notice how there are now far fewer edges and if you are familiar with the sites in question you might notice some clear regional patterning. + +It is also possible to create one-mode projections of the two-mode data here using simple matrix algebra. All you need to do is multiply a matrix by the transpose of that matrix. The results will be a adjacency matrix for whichever set of nodes represented the rows in the first matrix in the matrix multiplication. Here is an example using the `mod_clust` incidence matrix with threshold created above. In the resulting incidence matrix individual cells will represent the number of different edges in common between the nodes in question and can be treated like an edge weight. The diagonal of the matrix will be the total number of clusters that were present in a site assemblage. In R the operator `%*%` indicates matrix multiplication and the function `t()` will transpose a given matrix. + + +```r +# In R the command "%*%" indicates matrix multiplication and "t()" +# gives the transpose of the matrix within the parentheses. +# Lets first create a one-mode projection focused on sites +site_mode <- mod_clust %*% t(mod_clust) +site_net <- igraph::graph_from_adjacency_matrix(site_mode, + mode = "undirected", + diag = FALSE) +plot(site_net) +``` + + + +```r +# Now lets create a one-mode projection focused on ceramic +# technological clusters. +# Notice that the only change is we switch which side of the +# matrix multiplication we transpose. +clust_mode <- t(mod_clust) %*% mod_clust +head(clust_mode) +``` + +``` +## Clust1 Clust2 Clust3 Clust4 Clust5 Clust6 Clust7 Clust8 Clust9 Clust10 +## Clust1 1 0 0 0 0 0 0 0 0 0 +## Clust2 0 16 9 1 0 2 0 0 0 0 +## Clust3 0 9 10 0 0 1 0 0 0 0 +## Clust4 0 1 0 11 1 0 0 0 0 0 +## Clust5 0 0 0 1 1 0 0 0 0 0 +## Clust6 0 2 1 0 0 2 0 0 0 0 +``` + +```r +clust_net <- igraph::graph_from_adjacency_matrix(clust_mode, + mode = "undirected", + diag = FALSE) +plot(clust_net) +``` + + + +### Similarity Networks{#SimilarityNetworks} + +Similarity networks simply refer to one-mode networks where nodes are defined as entities of interest with edges defined and/or weighted based on some metric of similarity (or distance) defined based on the features, attributes, or assemblage associated with that node. Such an approach is frequently used in archaeology to explore material cultural networks where nodes are contexts of interests (e.g., sites, excavation units, houses, etc.) and edges are defined or weighted based on similarities in the relative frequencies of artifacts or particular classes of artifacts recovered in those contexts. + +There are many different ways to define and track similarity network data for use in R. In this example, we will show several methods using the affiliation data we used in the previous example. Specifically, we will define and weight edges based on similarities in the frequencies of ceramic technological clusters at sites in our Cibola region sample. + +For most of these examples we will use the `statnet` package and the `network` package object format within it rather than `igraph` because `statnet` has a few additional functions that are useful when working with similarity data. In the following examples, we will first demonstrate several different similarity/distance metrics and then discuss approaches to binarization of these similarity networks and other options for working with weighted data. + +#### Brainerd-Robinson Similarity {- #BrainerdRobinson} + +The first metric we will explore here is a rescaled version of the Brainerd-Robinson (BR) similarity metric. This BR measure is commonly used in archaeology including in a number of recent (and not so recent) network studies. This measure represents the total similarity in proportional representation of categories and is defined as: + +$$S = {\frac{2-\sum_{k} \left|x_{k} - y_{k}\right|} {2}}$$ + +where, for all categories $k$, $x$ is the proportion of $k$ in the first assemblage and $y$ is the proportion of $k$ in the second. We subtract the sum from 2 as 2 is the maximum proportional difference possible between two samples. We further divide the result by 2. This provides a scale of similarity from 0 to 1 where 1 is perfect similarity and 0 indicates no similarity. The chunk below defines the code for calculating this modified BR similarity measure. Note here we use a distance metric called "Manhattan Distance" built into the `vegan` package in R. This metric is identical to the Brainerd-Robinson metric. We rescale our results to range from 0 to 1 after calculation. + + +```r +# Read in raw data +cibola_clust <- + read.csv(file = "data/Cibola_clust.csv", + header = TRUE, + row.names = 1) + +# First we need to convert the ceramic technological clusters into proportions +clust_p <- prop.table(as.matrix(cibola_clust), margin = 1) + +# The following line uses the vegdist function in the vegan package +# to calculate the Brainerd-Robinson similarity score. Since vegdist +# by default defines an unscaled distance we must subtract the results +# from 2 and then divide by 2 to get a similarity scaled from 0 to 1. +cibola_br <- ((2 - as.matrix(vegan::vegdist(clust_p, + method = "manhattan"))) / 2) + +# Lets look at the first few rows. +cibola_br[1:4, 1:4] +``` + +``` +## Apache Creek Atsinna Baca Pueblo Casa Malpais +## Apache Creek 1.0000000 0.3433584 0.4455782 0.7050691 +## Atsinna 0.3433584 1.0000000 0.5750090 0.3740804 +## Baca Pueblo 0.4455782 0.5750090 1.0000000 0.5608953 +## Casa Malpais 0.7050691 0.3740804 0.5608953 1.0000000 +``` + +At this point we could simply define this as a weighted network object where weights are equal to the similarity scores, or we could define a threshold for defining edges as present or absent. We will discuss these options in detail after presenting other similarity/distance metrics. + +#### Morisita's Overlap Index {#Morisita} + +Another measure that has been used for defining similarities among assemblages for archaeological similarity networks is Morisita's overlap index. This measure is a measure of the overlap of individual assemblages within a larger population that takes the size of samples into account. Specifically, the approach assumes that as sample size increases diversity will likely increase. This measure produces results that are very similar to the Brainerd-Robinson metric in practice in most cases but this measure may be preferred where there are dramatic differences in assemblage sizes among observations. + +Morisita's index is calculated as: + +$$C_D=\frac{2 \Sigma^Sx_iy_i}{(D_x + D_y)XY}$$ + +where + +* $x_i$ is the number of rows where category $i$ is represented in the total $X$ from the population. +* $y_i$ is the number of rows where category $i$ is presented in the total $Y$ from the population. +* $D_x$ and $D_y$ are the Simpson's diversity index values for $x$ and $y$ respectively. +* $S$ is the total number of columns. + +This metric ranges from 0 (where no categories overlap at all) to 1 where the categories occur in the same proportions in both samples. Because this metric works on absolute counts we can run the `vegdist` function directly on the `Cibola_clust` object. Because we want a similarity rather than a distance (which is the default for this function in R) we subtract the results from 1. + + +```r +# Calculate matrix of Morisita similarities based on the +# Cibola_clust two-way table. +cibola_mor <- 1 - as.matrix(vegan::vegdist(cibola_clust, + method = "morisita")) +cibola_mor[1:4, 1:4] +``` + +``` +## Apache Creek Atsinna Baca Pueblo Casa Malpais +## Apache Creek 1.0000000 0.4885799 0.6014729 0.9060751 +## Atsinna 0.4885799 1.0000000 0.5885682 0.4459998 +## Baca Pueblo 0.6014729 0.5885682 1.0000000 0.6529069 +## Casa Malpais 0.9060751 0.4459998 0.6529069 1.0000000 +``` + + +#### $\chi^{2}$ Distance {- #ChiSquare} + +The next measure we will use is the $\chi^{2}$ distance metric which is the basis of correspondence analysis and related methods commonly used for frequency seriation in archaeology (note that this should probably really be called the $\chi$ distance since the typical form we use is not squared, but the name persists this way in the literature so that's what we use here). This measure is defined as: + +$$\chi_{jk} = \sqrt{\sum \frac 1{c_{j}} +({x_{j}-y_{j})^{2}}}$$ + +where $c_j$ denotes the $j_{th}$ element of the average row profile (the proportional abundance of $j$ across all rows) and $x$ and $y$ represent row profiles for the two sites under comparison. This metric therefore takes raw abundance (rather than simply proportional representation) into account when defining distance between sites. The definition of this metric is such that rare categories play a greater role in defining distances among sites than common categories (as in correspondence analysis). This measure has a minimum value of 0 and no theoretical upper limit. + +The code for calculating $\chi^{2}$ distances is defined in the chunk below and a new object called `Cibola_X` is created using this measure. It is sometimes preferable to rescale this measure so that it is bounded between 0 and 1. We create a second object called `Cibola_X01` which represents rescaled distances by simply dividing the matrix by the maximum observed value (there are many other ways to do this but this will be fine for our demonstration purposes). Again, we subtract these results from 1 to convert a distance to a similarity. + + +```r +# Define function for calculating chi-squared distance +chi_dist <- function(x) { + # calculates the profile for every row + rowprof <- x / apply(x, 1, sum) + # calculates the average profile + avgprof <- apply(x, 2, sum) / sum(x) + # creates a distance object of chi-squared distances + chid <- dist(as.matrix(rowprof) %*% diag(1 / sqrt(avgprof))) + # return the results + return(as.matrix(chid)) +} + +# Run the script and then create the rescaled 0-1 version +cibola_x <- chi_dist(cibola_clust) +cibola_x01 <- 1 - (cibola_x / max(cibola_x)) + +cibola_x01[1:4, 1:4] +``` + +``` +## Apache Creek Atsinna Baca Pueblo Casa Malpais +## Apache Creek 1.0000000 0.2904662 0.1010795 0.6166508 +## Atsinna 0.2904662 1.0000000 0.3393173 0.2999925 +## Baca Pueblo 0.1010795 0.3393173 1.0000000 0.1469591 +## Casa Malpais 0.6166508 0.2999925 0.1469591 1.0000000 +``` + +#### Jaccard Similarity{- #Jaccard} + +In many situations we may have either only presence/absence data or data where a one or a few categories dominate the assemblages of most sites. In the former case, the measures of similarity above will not work. In the latter case, although the measures above may work, the results may largely be a product of similarities or differences in those few common categories. In such cases, it may be preferable to use a similarity metric based on presence/absence data. The Jaccard similarity coefficient for two sets of presence/absence values is simply the intersection of values in those two cases divided by the union of those two cases. Formally this can be written as: + +$$J(A,B) = \frac{|A \cap B|}{|A \cup B|} = \frac{|A \cap B|}{|A| + |B| - |A \cap B|}$$ + +where + +* $|A \cap B|$ is the intersection of vectors A and B or the number of categories where both vectors = present. +* $|A \cup B|$ is the union of vectors A and B or the total number of categories where *either* A or B is present. + +We can define a simple function in R to calculate Jaccard similarity coefficients. These values will always range from 0 (no categories in common) to 1 (all categories in common). + + +```r +jaccard <- function(a, b) { + intersection <- length(which((a-b) == 0)) + union <- length(a) + length(b) - intersection + return(intersection / union) +} +``` + +In order to try this function out, we will create three simple vectors of values and then compare them. Note that this function compares matches for the position of each item in the vector. So if `vec1[1] = 1` and `vec2[1] = 1` that is considered a match: + + +```r +vec1 <- c(0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0) +vec2 <- c(0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1) +vec3 <- c(0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1) + +jaccard(vec1, vec2) +``` + +``` +## [1] 0.4666667 +``` + +```r +jaccard(vec2, vec3) +``` + +``` +## [1] 0.5714286 +``` + +```r +jaccard(vec1, vec3) +``` + +``` +## [1] 0.375 +``` + +If we want to run this for an entire incidence matrix we could roll it into another function. Note that this function automatically takes an incidence matrix, converts it into presence/absence data and then calculates Jaccard coefficients for every pair of nodes. The output is a square matrix of similarities from row to row. Let's try it out in our `cibola_clust` data we used above: + + +```r +jaccard_inc <- function(dat) { + dat[dat > 0] <- 1 + out <- matrix(NA, nrow(dat), nrow(dat)) + for (i in seq_len(nrow(dat))) { + for (j in seq_len(nrow(dat))) { + out[i, j] <- jaccard(dat[i, ], dat[j, ]) + } + } + return(out) +} + +cibola_j <- jaccard_inc(dat = cibola_clust) + +cibola_j[1:4, 1:4] +``` + +``` +## [,1] [,2] [,3] [,4] +## [1,] 1.0000000 0.4285714 0.4285714 0.8181818 +## [2,] 0.4285714 1.0000000 0.6666667 0.5384615 +## [3,] 0.4285714 0.6666667 1.0000000 0.5384615 +## [4,] 0.8181818 0.5384615 0.5384615 1.0000000 +``` + +Note that we have created R scripts with the two functions above as separate files. If you would like to initialize those functions without copying and pasting the code above, you can use the `source()` function to call a function straight from a file. These are both located in the "scripts" folder so we add that sub-folder to the file name in the argument. [Click here to download jaccard.R](scripts/jaccard.R) and [here for jaccard_inc.R](scripts/jaccard_inc.R). + + +```r +source("scripts/jaccard.R") +source("scripts/jaccard_inc.R") +``` + +#### Creating Network Objects from Similarity Matrices {- #NetFromSim} + +Now that we have defined our measures of similarity, the next step is to convert these into network objects that our R packages will be able to work with. We can do this by either creating binary networks (where ties are either present or absent) or weighted networks (which in many cases are simply the raw similarity/distance matrices we calculated above). We will provide examples of both approaches, starting with simple binary networks. There are many ways to define networks from matrices like those we generated above and our examples below should not been seen as an exhaustive set of procedures. + +##### Creating binary network objects {-} + +First, we will produce a network object based on our BR similarity matrix created above. In this example, we define ties as present between pairs of sites when they share more than 65% commonality (BR > 0.65) in terms of the proportions of ceramics recovered from pairs of sites. + +In the code below, the `event2dichot` function (from the `statnet` package) takes our matrix and divides it into 1s and 0s based on the cut off we choose. Here we're using and `absolute` cut off meaning we're assigning a specific value to use as the cut off (0.65). We then send the output of this function to the network function just as before. + + +```r +# Define our binary network object from BR similarity +brnet <- + network(event2dichot(cibola_br, + method = "absolute", + thresh = 0.65), + directed = FALSE) +# Now let's add names for our nodes based on the row names +# of our original matrix +brnet %v% "vertex.names" <- row.names(cibola_clust) +# look at the results. +brnet +``` + +``` +## Network attributes: +## vertices = 31 +## directed = FALSE +## hyper = FALSE +## loops = FALSE +## multiple = FALSE +## bipartite = FALSE +## total edges= 167 +## missing edges= 0 +## non-missing edges= 167 +## +## Vertex attribute names: +## vertex.names +## +## No edge attributes +``` + +```r +# plot network using default layout +set.seed(7564) +plot(brnet) +``` + + + +In the next chunk of code we will use the $\chi^2$ distances to create binary networks. This time, we will not use an absolute value to define ties as present, but instead will define those similarities greater than 80 percent of all similarities as present. We will then once again plot just as above. + + +```r +# Note we use 1 minus chacoX01 here so to convert a distance +# to a similarity +xnet <- + network(event2dichot(cibola_x01, + method = "quantile", + thresh = 0.80), + directed = FALSE) +# Once again add vertex names as row names of data frame +xnet %v% "vertex.names" <- row.names(cibola_clust) +# look at the results +xnet +``` + +``` +## Network attributes: +## vertices = 31 +## directed = FALSE +## hyper = FALSE +## loops = FALSE +## multiple = FALSE +## bipartite = FALSE +## total edges= 80 +## missing edges= 0 +## non-missing edges= 80 +## +## Vertex attribute names: +## vertex.names +## +## No edge attributes +``` + +```r +# plot network using default layout +set.seed(346) +plot(xnet) +``` + + + +Finally, we'll use our Jaccard coefficients and we will define our threshold for defining edges as present or absent as the grand mean of the entire similarity matrix using `method = "mean"`. There are more options in the `event2dichot` function. See the help material for more. + + +```r +jnet <- + network(event2dichot(cibola_j, + method = "mean"), + directed = FALSE) + +jnet %v% "vertex.names" <- row.names(cibola_clust) + +jnet +``` + +``` +## Network attributes: +## vertices = 31 +## directed = FALSE +## hyper = FALSE +## loops = FALSE +## multiple = FALSE +## bipartite = FALSE +## total edges= 207 +## missing edges= 0 +## non-missing edges= 207 +## +## Vertex attribute names: +## vertex.names +## +## No edge attributes +``` + +```r +# plot network using default layout +set.seed(343546) +plot(jnet) +``` + + + +##### Creating Weighted Network Objects {-} + +It is also possible to use R to create weighted networks where individual edges are valued. We have found that this works reasonably well with networks of co-presence or something similar (counts of mentions in texts or monuments for example) but this does not perform well when applied to large similarity or distance matrices (because every possible link has a value, the network gets unwieldy very fast). In the latter case, we have found it is often better to just work directly with the underlying similarity/distance matrix. + +If you do, however, chose to create a weighted network object from a similarity matrix it only requires a slight modification from the procedure above. In the chunk of code below, we will simply add the arguments `ignore.eval = FALSE` and `names.eval = "weight"` to let the network function know we would like weights to be retained and we would like that attribute called 'weight'. We will apply this to the matrix of Morisita similarities defined above and then plot the result. + + +```r +# create weighted network object from co-occurrence matrix by +# adding the ignore.eval=F argument +mor_wt <- network( + cibola_mor, + directed = FALSE, + ignore.eval = FALSE, + names.eval = "weight" +) + +mor_wt %v% "vertex.names" <- row.names(cibola_mor) +mor_wt +``` + +``` +## Network attributes: +## vertices = 31 +## directed = FALSE +## hyper = FALSE +## loops = FALSE +## multiple = FALSE +## bipartite = FALSE +## total edges= 465 +## missing edges= 0 +## non-missing edges= 465 +## +## Vertex attribute names: +## vertex.names +## +## Edge attribute names: +## weight +``` + +```r +# plot weighted network using default layout +set.seed(4634) +plot(mor_wt) +``` + + + +The resulting network is nearly complete so it is a bit unwieldy for plotting but calculating network statistics on this weighted network can often still be useful as we will see in the exploratory analysis section. + +### Ego Networks{#EgoNetworks} + +When we aim to understand the relational environment within which an entity is embedded, because it is relevant for our research questions or because data collection challenges dictate this focus, archaeological network research can make use of so-called ego-networks: a type of network that includes a focal node (the so-called ego), the set of nodes the ego is connected to by an edge (the so-called alters) and the edges between this set of nodes. + +Extracting an ego-network from an existing igraph network object in R is very easy. Here we will extract and plot the ego-network for Apache Creek, the first site in the network files we created above. First we read in data and create a network object and then we apply the `igraph::make_ego_graph` function to the network object we created. + + +```r +# Read in edge list file as data frame +cibola_edgelist <- + read.csv(file = "data/Cibola_edgelist.csv", header = TRUE) + +# Create graph object. The data frame is converted to a matrix as +# that is required by this specific function. Since this is an +# undirected network, directed = FALSE. +cibola_net <- + igraph::graph_from_edgelist(as.matrix(cibola_edgelist), + directed = FALSE) + +# Extract ego-networks +ego_nets <- make_ego_graph(cibola_net) + +# Examine the first ego-network +ego_nets[[1]] +``` + +``` +## IGRAPH 0334815 UN-- 12 59 -- +## + attr: name (v/c) +## + edges from 0334815 (vertex names): +## [1] Apache Creek --Casa Malpais Apache Creek --Coyote Creek +## [3] Casa Malpais --Coyote Creek Apache Creek --Hooper Ranch +## [5] Casa Malpais --Hooper Ranch Coyote Creek --Hooper Ranch +## [7] Apache Creek --Horse Camp Mill Casa Malpais --Horse Camp Mill +## [9] Coyote Creek --Horse Camp Mill Hooper Ranch --Horse Camp Mill +## [11] Apache Creek --Hubble Corner Casa Malpais --Hubble Corner +## [13] Coyote Creek --Hubble Corner Hooper Ranch --Hubble Corner +## [15] Horse Camp Mill--Hubble Corner Apache Creek --Mineral Creek Pueblo +## + ... omitted several edges +``` + +```r +# Plot Apache Creek ego-network +set.seed(754) +plot(ego_nets[[1]]) +``` + + + +```r +# Plot Platt Ranch ego-network for comparison +set.seed(45367) +plot(ego_nets[[30]]) +``` + + + +In these ego-networks, only nodes connected to the target nodes (Apache Creek in the first example and then Platt Ranch in the second) are shown and only edges among those included nodes are shown. + +It is also possible to determine the size of ego-networks for an entire one-mode network using the `ego_size` function. The output of this function is a vector that can be further assigned as a network node attribute. + + +```r +ego_size(cibola_net) +``` + +``` +## [1] 12 12 12 12 13 14 13 13 10 14 15 7 9 14 13 14 15 11 14 14 15 2 14 19 15 +## [26] 12 12 11 7 6 +``` + +### Multilayer Network{#Multinet} + +In the simplest terms, multilayer networks are networks where a single set of nodes are connected by two or more sets of edges that each represent a different kind of relationship among the nodes. This is a relatively new area of network science in archaeological network research but we expect this will likely change in the coming years. There are now new R packages which help manage and analyze multilayer network data. + +The `multinet` package (Rossi and Vega 2021) is designed to facilitate the analysis of multilayer networks. In order to explore some of the possibilities here we use example data and analyses included in this package. Specifically, we will look at the famous network data on Florentine families in the 14th century where connections were defined in terms of both business and marriage. + + +```r +# create object with Florentine multilayer network data +florentine <- ml_florentine() + +# Examine the data +florentine +``` + +``` +## ml-net[15, 2, 26, 35 (35,0)] +``` + +```r +summary(florentine) +``` + +``` +## n m dir nc slc dens cc apl dia +## _flat_ 15 35 0 1 15 0.3333333 0.3409091 2.085714 4 +## business 11 15 0 1 11 0.2727273 0.4166667 2.381818 5 +## marriage 15 20 0 1 15 0.1904762 0.1914894 2.485714 5 +``` + +```r +# plot the data +plot(florentine) +``` + + + +The `multinet` network objects are compatible with `igraph` and individual layers can be analyzed just like other `igraph` network objects. Where this `multinet` approach likely has greater utility is in conducting comparisons among layers or conducting analyses that take several layers into account simultaneously. A detailed exploration of this approach is beyond the scope of this document (but we provide a simple example below) and we suggest interested readers read the package information and tutorials associated with this package for more. In the example here we calculate degree across multiple layers using the `degree_ml` function and then run a Louvain cluster detection algorithm across all graph layers using `glouvain_ml`. Multilayer networks have considerable potential for archaeological data and we hope to see more research in this area in the future. + + +```r +# If we want to calculate degree centrality across multiple layers of a +# multilayer network, the multinet package can help us do that directly +# and quite simply. +multinet::degree_ml(florentine) +``` + +``` +## [1] 2 11 3 3 4 7 4 3 6 6 6 1 6 3 5 +``` + +```r +# Similarly, we could apply cluster detection algorithms to all layers +# of a multilayer network simultaneously. +multinet::glouvain_ml(florentine) +``` + +``` +## actor layer cid +## 1 Pazzi business 0 +## 2 Pazzi marriage 0 +## 3 Medici business 0 +## 4 Medici marriage 0 +## 5 Salviati business 0 +## 6 Salviati marriage 0 +## 7 Ridolfi marriage 0 +## 8 Tornabuoni business 0 +## 9 Tornabuoni marriage 0 +## 10 Ginori business 0 +## 11 Ginori marriage 0 +## 12 Acciaiuoli marriage 0 +## 13 Barbadori business 0 +## 14 Barbadori marriage 0 +## 15 Albizzi marriage 0 +## 16 Peruzzi business 1 +## 17 Peruzzi marriage 1 +## 18 Strozzi marriage 1 +## 19 Guadagni business 1 +## 20 Guadagni marriage 1 +## 21 Castellani business 1 +## 22 Castellani marriage 1 +## 23 Bischeri business 1 +## 24 Bischeri marriage 1 +## 25 Lamberteschi business 1 +## 26 Lamberteschi marriage 1 +``` + +For an archaeological example of multilevel network analysis [this GitHub project](https://github.com/ajupton/archy-multilayer-nets) by Andy Upton. + +## Converting Among Network Object Types{#ConvertingNetworkFormats} + +
+

In most of the examples in this document we have been using the +igraph package but for the similarity networks we chose to +use statnet due to the convenience of functions for working +directly with similarity data. Not to worry as it is easy to convert one +format to another and preserve all of the attributes using a package +called intergraph. By way of example below we can covert +the weighted network object we created in the previous step and convert +it to a igraph object and view the attributes using the +asIgraph function. If we wanted to go the other direction +and covert a igraph object to a network object +(which is the format all of the statnet package require) we +would instead use asNetwrok.

+
+ +Here is a simple example: + + +```r +mor_wt_i <- asIgraph(mor_wt) +mor_wt_i +``` + +``` +## IGRAPH 03754f7 U-W- 31 465 -- +## + attr: na (v/l), vertex.names (v/c), na (e/l), weight (e/n) +## + edges from 03754f7: +## [1] 1-- 2 1-- 3 1-- 4 1-- 5 1-- 6 1-- 7 1-- 8 1-- 9 1--10 1--11 1--12 1--13 +## [13] 1--14 1--15 1--16 1--17 1--18 1--19 1--20 1--21 1--22 1--23 1--24 1--25 +## [25] 1--26 1--27 1--28 1--29 1--30 1--31 2-- 3 2-- 4 2-- 5 2-- 6 2-- 7 2-- 8 +## [37] 2-- 9 2--10 2--11 2--12 2--13 2--14 2--15 2--16 2--17 2--18 2--19 2--20 +## [49] 2--21 2--22 2--23 2--24 2--25 2--26 2--27 2--28 2--29 2--30 2--31 3-- 4 +## [61] 3-- 5 3-- 6 3-- 7 3-- 8 3-- 9 3--10 3--11 3--12 3--13 3--14 3--15 3--16 +## [73] 3--17 3--18 3--19 3--20 3--21 3--22 3--23 3--24 3--25 3--26 3--27 3--28 +## [85] 3--29 3--30 3--31 4-- 5 4-- 6 4-- 7 4-- 8 4-- 9 4--10 4--11 4--12 4--13 +## + ... omitted several edges +``` + +```r +# view first 10 edge weights to show that they are retained +E(mor_wt_i)$weight[1:10] +``` + +``` +## [1] 0.4885799 0.6014729 0.9060751 0.4049019 1.0000000 0.7087214 0.7724938 +## [8] 0.4521581 0.7996468 1.0000000 +``` + +And back in the other direction: + + +```r +mor_new <- asNetwork(mor_wt_i) +mor_new +``` + +``` +## Network attributes: +## vertices = 31 +## directed = FALSE +## hyper = FALSE +## loops = FALSE +## multiple = FALSE +## bipartite = FALSE +## total edges= 465 +## missing edges= 0 +## non-missing edges= 465 +## +## Vertex attribute names: +## vertex.names +## +## Edge attribute names: +## weight +``` + +```r +(mor_new %e% "weight")[1:10] +``` + +``` +## [1] 0.4885799 0.6014729 0.9060751 0.4049019 1.0000000 0.7087214 0.7724938 +## [8] 0.4521581 0.7996468 1.0000000 +``` + diff --git a/03-exploratory-analysis.Rmd b/03-exploratory-analysis.Rmd index 4e5c1c1..bed046f 100644 --- a/03-exploratory-analysis.Rmd +++ b/03-exploratory-analysis.Rmd @@ -125,10 +125,10 @@ igraph::degree(directed_net, mode = "out")[1:5] # out-degree igraph::degree(simple_net, normalize = T)[1:5] # it is also possible to directly plot the degree distribution for -# a given network using the degree.distribution function. +# a given network using the degree_distribution function. # Here we embed that call directly in a call for a histogram plot # using the "hist" function -hist(igraph::degree.distribution(simple_net)) +hist(igraph::degree_distribution(simple_net)) # graph level centralization igraph::centr_degree(simple_net) @@ -444,10 +444,10 @@ R allows you to use a variety of common cluster detection algorithms to define g #### Girvan-Newman Clustering{#GirvanNewman} -Girvan-Newman clustering is a divisive algorithm based on betweenness that defines a partition of network that maximizes modularity by removing nodes with high betweenness iteratively (see discussion in Brughmans and Peeples 2023 Chapter 4.6). In R this is referred to as the `igraph::edge.betweenness.community` function. This function can be used on directed or undirected networks with or without edge weights. This function outputs a variety of information including individual edge betweenness scores, modularity information, and partition membership. See the help documents for more information +Girvan-Newman clustering is a divisive algorithm based on betweenness that defines a partition of network that maximizes modularity by removing nodes with high betweenness iteratively (see discussion in Brughmans and Peeples 2023 Chapter 4.6). In R this is implemented with the `igraph::cluster_edge_betweenness` function. This function can be used on directed or undirected networks with or without edge weights. This function outputs a variety of information including individual edge betweenness scores, modularity information, and partition membership. See the help documents for more information ```{r} -gn <- igraph::edge.betweenness.community(simple_net) +gn <- igraph::cluster_edge_betweenness(simple_net) set.seed(4353) plot(simple_net, vertex.color = gn$membership) ``` diff --git a/03-exploratory-analysis.md b/03-exploratory-analysis.md new file mode 100644 index 0000000..b1adf81 --- /dev/null +++ b/03-exploratory-analysis.md @@ -0,0 +1,1145 @@ +# Exploratory Network Analysis{#Exploratory} + +![](images/image_break.png){width=100%} + +Exploratory network analysis is simply exploratory data analysis applied to network data. This covers a range of statistical and visual techniques designed to explore the structure of networks as well as the relative positions of nodes and edges. These methods can be used to look for particular structures or patterning of interest, such as the most central nodes, or to summarize and describe the structure of the network to paint a general picture of it before further analysis. This section serves as a companion to Chapter 4 in the Brughmans and Peeples book (2023) and provides basic examples of the exploratory network analysis methods outlined in the book as well as a few others. + +Note that we have created a distinct section on [exponential random graph models (ERGM)](#ERGM) in the "Going Beyond the Book" section of this document as that approach necessitates extended discussion. We replicate the boxed example from Chapter 4 of the book in that section. + +## Example Network Objects{#ExampleNetworkObjects} + +In order to facilitate the exploratory analysis examples in this section, we want to first create a set of `igraph` and `statnet` network objects that will serve our purposes across all of the analyses below. Specifically, we will generate and define: + + * **`simple_net`** - A simple undirected binary network with isolates + * **`simple_net_noiso`** - A simple undirected binary network without isolates + * **`directed_net`** - A directed binary network + * **`weighted_net`** - An undirected weighted network + * **`sim_net_i`** - A similarity network with edges weighted by similarity in the `igraph` format + * **`sim_net`** - A similarity network with edges weighted by similarity in the `network` format + * **`sim_mat`** - A data frame object containing a weighted similarity matrix + +Each of these will be used as appropriate to illustrate particular methods. + +In the following chunk of code we initialize all of the packages that we will use in this section and define all of the network objects that we will use (using the object names above). In these examples we will once again use the [Cibola technological similarity data](#Cibola) we used in the [Network Data Formats](#NetworkData) section previously. + + +```r +# initialize packages +library(igraph) +library(statnet) +library(intergraph) +library(vegan) + +# read in csv data +cibola_edgelist <- + read.csv(file = "data/Cibola_edgelist.csv", header = TRUE) +cibola_adj_mat <- read.csv(file = "data/Cibola_adj.csv", + header = T, + row.names = 1) + +# Simple network with isolates +simple_net <- + igraph::graph_from_adjacency_matrix(as.matrix(cibola_adj_mat), + mode = "undirected") + +# Simple network with no isolates +simple_net_noiso <- + igraph::graph_from_edgelist(as.matrix(cibola_edgelist), + directed = FALSE) + +#Create a directed network by sub-sampling edge list +set.seed(45325) +el2 <- cibola_edgelist[sample(seq(1, nrow(cibola_edgelist)), 125, + replace = FALSE), ] + +directed_net <- igraph::graph_from_edgelist(as.matrix(el2), + directed = TRUE) + +# Create a weighted undirected network by adding column of random +# weights to edge list +cibola_edgelist$weight <- sample(seq(1, 4), nrow(cibola_edgelist), + replace = TRUE) +weighted_net <- + igraph::graph_from_edgelist(as.matrix(cibola_edgelist[, 1:2]), + directed = FALSE) + +E(weighted_net)$weight <- cibola_edgelist$weight + +# Create a similarity network using the Brainerd-Robinson metric +cibola_clust <- + read.csv(file = "data/Cibola_clust.csv", + header = TRUE, + row.names = 1) +clust_p <- prop.table(as.matrix(cibola_clust), margin = 1) +sim_mat <- + (2 - as.matrix(vegan::vegdist(clust_p, method = "manhattan"))) / 2 +sim_net <- network( + sim_mat, + directed = FALSE, + ignore.eval = FALSE, + names.eval = "weight" +) +sim_net_i <- asIgraph(sim_net) +``` + +## Calculating Network Metrics in R {#CalcMetric} + +Although the calculations behind the scenes for centrality metrics, clustering algorithms, and other network measures may be somewhat complicated, calculating these measures in R using network objects is usually quite straight forward and typically only involves a single function and a couple of arguments within it. There are, however, some things that need to be kept in mind when applying these methods to network data. In this document, we provide examples of some of the most common functions you may use as well as a few caveats and potential problems. + +Certain network metrics require networks with specific properties and may produce unexpected results if the wrong kind of network is used. For example, closeness centrality is only well defined for binary networks that have no isolates. If you were to use the `igraph::closeness` command to calculate closeness centrality on a network with isolates, you would get results, but you would also get a warning telling you "closeness centrality is not well-defined for disconnected graphs." For other functions if you provide data that does not meet the criteria required by that function you may instead get an error and have no results returned. In some cases, however, a function may simply return results and not provide any warning, so it is important that you are careful when selecting methods to avoid providing data that violates assumptions of the method provided. Remember, if you have questions about how a function works or what it requires you can type `?function_name` at the console with the function in question and you will get the help document that should provide more information. You can also include package names in the help call to ensure you get the correct function (i.e., `?igraph::degree`) + +## Centrality{#Centrality} + +One of the most common kinds of exploratory network analysis involves calculating basic network centrality and centralization statistics. There are a wide array of methods available in R through the `igraph` and `statnet` packages. In this section we highlight a few examples as well as a few caveats to keep in mind. + +### Degree Centrality{#Degree} + +Degree centrality can be calculated using the `igraph::degree` function for simple networks with or without isolates as well as simple directed networks. This method is not, however, appropriate for weighted networks or similarity networks (because it expects binary values). If you apply the `igraph::degree` function to a weighted network object you will simply get the binary network degree centrality values. The alternative for calculating weighted degree for weighted and similarity networks is to simply calculate the row sums of the underlying similarity matrix (minus 1 to account for self loops) or adjacency matrix. For the degree function the returned output is a vector of values representing degree centrality which can further be assigned to an R object, plotted, or otherwise used. We provide a few examples here to illustrate. Note that for directed graphs you can also specify the mode as `in` for in-degree or `out` for out-degree or `all` for the sum of both. + +Graph level degree centralization is equally simple to call using the `centr_degree` function. This function returns an object with multiple parts including a vector of degree centrality scores, the graph level centralization metric, and the theoretical maximum number of edges (n \* [n-1]). This metric can be normalized such that the maximum centralization value would be 1 using the `normalize = TRUE` argument as we demonstrate below. See the comments in the code chunk below to follow along with which type of network object is used in each call. In most cases we only display the first 5 values to prevent long lists of output (using the `[1:5]` command). + + +```r +# simple network with isolates +igraph::degree(simple_net)[1:5] +``` + +``` +## Apache.Creek Atsinna Baca.Pueblo Casa.Malpais Cienega +## 11 8 1 11 13 +``` + +```r +# simple network no isolates +igraph::degree(simple_net_noiso)[1:5] +``` + +``` +## Apache Creek Casa Malpais Coyote Creek Hooper Ranch Horse Camp Mill +## 11 11 11 11 12 +``` + +```r +# directed network +igraph::degree(directed_net, mode = "in")[1:5] # in-degree +``` + +``` +## Coyote Creek Techado Springs Hubble Corner Tri-R Pueblo Heshotauthla +## 1 6 5 6 2 +``` + +```r +igraph::degree(directed_net, mode = "out")[1:5] # out-degree +``` + +``` +## Coyote Creek Techado Springs Hubble Corner Tri-R Pueblo Heshotauthla +## 6 2 5 2 11 +``` + +```r +# weighted network - rowSums of adjacency matrix +(rowSums(as.matrix( + as_adjacency_matrix(weighted_net, + attr = "weight") +)) - 1)[1:5] +``` + +``` +## Apache Creek Casa Malpais Coyote Creek Hooper Ranch Horse Camp Mill +## 25 29 21 18 27 +``` + +```r +# similarity network. Note we use the similarity matrix here and +# not the network object +(rowSums(sim_mat) - 1)[1:5] +``` + +``` +## Apache Creek Atsinna Baca Pueblo Casa Malpais Cienega +## 16.00848 15.87024 14.77997 17.30358 17.09394 +``` + +```r +# If you want to normalize your degree centrality metric by the +# number of nodes present you can do that by adding the normalize=TRUE +# command to the function calls above. For weighted and similarity +# networks you can simply divide by the number of nodes minus 1. +igraph::degree(simple_net, normalize = T)[1:5] +``` + +``` +## Apache.Creek Atsinna Baca.Pueblo Casa.Malpais Cienega +## 0.36666667 0.26666667 0.03333333 0.36666667 0.43333333 +``` + +```r +# it is also possible to directly plot the degree distribution for +# a given network using the degree_distribution function. +# Here we embed that call directly in a call for a histogram plot +# using the "hist" function +hist(igraph::degree_distribution(simple_net)) +``` + + + +```r +# graph level centralization +igraph::centr_degree(simple_net) +``` + +``` +## $res +## [1] 11 8 1 11 13 11 6 13 14 18 11 12 13 11 12 12 13 14 11 5 10 12 13 13 9 +## [26] 14 13 14 6 0 10 +## +## $centralization +## [1] 0.2408602 +## +## $theoretical_max +## [1] 930 +``` + +```r +# To calculate centralization score for a similarity matrix, use the +# sna::centralization function +sna::centralization(sim_mat, normalize = TRUE, sna::degree) +``` + +``` +## [1] 0.1082207 +``` + +If you are interested in calculating graph level density you can do this using the `edge_density` function. Note that just like the degree function above, this only works for binary networks and if you submit a weighted network object you will simply get the binary edge density value. + + +```r +edge_density(simple_net_noiso) +``` + +``` +## [1] 0.383908 +``` + +```r +edge_density(weighted_net) +``` + +``` +## [1] 0.383908 +``` + +### Betweenness Centrality{#Betweenness} + +The betweenness functions work very much like the degree function calls above. Betweenness centrality in `igraph` can be calculated for simple networks with and without isolates, directed networks, and weighted networks. In the case of weighted networks or similarity networks, the shortest paths between sets of nodes are calculated such that the path of greatest weight is taken at each juncture. You can normalize your results by using `normalize = TRUE` just like you could for degree. The `igraph::betweenness` function will automatically detect if a graph is directed or weighted and use the appropriate method but you can also specify a particular edge attribute to use for weight if you perhaps have more than one weighting scheme. + + +```r +# calculate betweenness for simple network +igraph::betweenness(simple_net)[1:5] +``` + +``` +## Apache.Creek Atsinna Baca.Pueblo Casa.Malpais Cienega +## 1.125000 0.000000 0.000000 8.825306 8.032865 +``` + +```r +# calculate betweenness for weighted network +igraph::betweenness(weighted_net, directed = FALSE)[1:5] +``` + +``` +## Apache Creek Casa Malpais Coyote Creek Hooper Ranch Horse Camp Mill +## 20.94423 18.96259 17.67829 15.66853 2.78036 +``` + +```r +# calculate betweenness for weighted network specifying weight attribute +igraph::betweenness(weighted_net, weights = E(weighted_net)$weight)[1:5] +``` + +``` +## Apache Creek Casa Malpais Coyote Creek Hooper Ranch Horse Camp Mill +## 20.94423 18.96259 17.67829 15.66853 2.78036 +``` + +```r +# calculate graph level centralization +centr_betw(simple_net) +``` + +``` +## $res +## [1] 1.1250000 0.0000000 0.0000000 8.8253059 8.0328650 3.2862641 +## [7] 0.2500000 58.7048084 15.6031093 142.3305364 1.1250000 9.0503059 +## [13] 11.9501530 6.2604913 1.2590038 12.8566507 8.0328650 41.0052110 +## [19] 0.5722222 2.7950980 0.2844828 9.0503059 15.3558646 8.0328650 +## [25] 0.0000000 16.0653473 11.9501530 17.0225282 0.0000000 0.0000000 +## [31] 2.1735632 +## +## $centralization +## [1] 0.3064557 +## +## $theoretical_max +## [1] 13050 +``` + +### Eigenvector Centrality{#Eigenvector} + +The `igraph::eigen_centrality` function can be calculated for simple networks with and without isolates, directed networks, and weighted networks. By default scores are scaled such that the maximum score of 1. You can turn this scaling of by using the `scale = FALSE` argument. This function automatically detects whether a network object is directed or weighted but you can also call edge attributes to specify a particular weight attribute. By default this function outputs many other features of the analysis such as the number of steps toward convergence and the number of iterations but if you just want the centrality results you can use the atomic vector call to \$vector. + + +```r +eigen_centrality(simple_net, + scale = TRUE)$vector[1:5] +``` + +``` +## Apache.Creek Atsinna Baca.Pueblo Casa.Malpais Cienega +## 0.46230981 0.54637071 0.07114132 0.53026366 0.85007181 +``` + +```r +eigen_centrality( + weighted_net, + weights = E(weighted_net)$weight, + directed = FALSE, + scale = FALSE +)$vector[1:5] +``` + +``` +## Apache Creek Casa Malpais Coyote Creek Hooper Ranch Horse Camp Mill +## 0.08116512 0.10608344 0.07254989 0.05355994 0.10123595 +``` + +### Page Rank Centrality{#PageRank} + +The `igraph::page_rank` function can be calculated for simple networks with and without isolates, directed networks, and weighted networks. By default scores are scaled such that the maximum score is 1. You can turn this scaling off by using the `scale = FALSE` argument. This function automatically detects whether a network object is directed or weighted but you can also call edge attributes to specify a particular weight attribute. You can change the algorithm used to implement the page rank algorithm (see help for details) and can also change the damping factor if desired. + + +```r +page_rank(directed_net, + directed = TRUE)$vector[1:5] +``` + +``` +## Coyote Creek Techado Springs Hubble Corner Tri-R Pueblo Heshotauthla +## 0.01375364 0.03433734 0.02521968 0.04722743 0.01549665 +``` + +```r +page_rank( + weighted_net, + weights = E(weighted_net)$weight, + directed = FALSE, + algo = "prpack" +)$vector[1:5] +``` + +``` +## Apache Creek Casa Malpais Coyote Creek Hooper Ranch Horse Camp Mill +## 0.03340837 0.03761940 0.02901255 0.02610001 0.03551477 +``` + +### Closeness Centrality {#Closeness} + +The `igraph::closeness` function calculates closeness centrality and can be calculated for directed and undirected simple or weighted networks with no isolates. This function can also be used for networks with isolates, but you may receive an additional message suggesting that closeness is undefined for networks that are not fully connected. For very large networks you can use the `igraph::estimate_closeness` function with a cutoff setting that will consider paths of length up to cutoff to calculate closeness scores. For directed networks you can also specify whether connections in, out, or in both directions should be used. + +
+

Note that the function igraph::closeness() should not +normally be used with networks with multiple components. Depending on +your settings, however, the function call may not return an error so be +careful.

+
+ +Let's take a look at some examples: + + +```r +igraph::closeness(simple_net)[1:5] +``` + +``` +## Apache.Creek Atsinna Baca.Pueblo Casa.Malpais Cienega +## 0.01470588 0.01470588 0.01315789 0.01886792 0.02000000 +``` + +```r +igraph::closeness(simple_net_noiso)[1:5] +``` + +``` +## Apache Creek Casa Malpais Coyote Creek Hooper Ranch Horse Camp Mill +## 0.01470588 0.01886792 0.01754386 0.01470588 0.01923077 +``` + +```r +igraph::closeness(weighted_net, weights = E(weighted_net)$weight)[1:5] +``` + +``` +## Apache Creek Casa Malpais Coyote Creek Hooper Ranch Horse Camp Mill +## 0.01010101 0.01298701 0.01219512 0.01111111 0.01063830 +``` + +```r +igraph::closeness(directed_net, mode = "in")[1:5] +``` + +``` +## Coyote Creek Techado Springs Hubble Corner Tri-R Pueblo Heshotauthla +## 1.00000000 0.04166667 0.04761905 0.04347826 0.09090909 +``` + +### Hubs and Authorities {#HubsAndAuthorities} + +In directed networks it is possible to calculate hub and authority scores to identify nodes that are characterized by high in-degree and high out-degree in particular. Because this is a measure that depends on direction it is only appropriate for directed network objects. If you run this function for an undirected network hub scores and authority scores will be identical. These functions can also be applied networks that are both directed and weighted. If you do not want all options printed you can use the atomic vector \$vector call as well. + + +```r +igraph::hub_score(directed_net)$vector[1:5] +``` + +``` +## Coyote Creek Techado Springs Hubble Corner Tri-R Pueblo Heshotauthla +## 0.31998744 0.12265832 0.30740409 0.08450797 1.00000000 +``` + +```r +igraph::authority_score(directed_net)$vector[1:5] +``` + +``` +## Coyote Creek Techado Springs Hubble Corner Tri-R Pueblo Heshotauthla +## 0.05372558 0.32708203 0.28835263 0.35970234 0.25265287 +``` + +## Triads and clustering{#TriadsAndClustering} + +Another important topic in network science concerns considerations of the overall structure and clustering of connections across a network as a whole. There are a variety of methods which have been developed to characterize the overall degree of clustering and closure in networks, many of which are based on counting triads of various configurations. In this section, we briefly outline approaches toward evaluating triads, transitivity, and clustering in R. + +### Triads{#Triads} + +A triad is simply a set of three nodes and a description of the configuration of edges among them. For undirected graphs, there are four possibilities for describing the connections among those nodes (empty graph, 1 connection, 2 connections, 3 connections). For directed graphs the situation is considerably more complicated because ties can be considered in both directions and an edge in one direction isn't necessarily reciprocated. Thus there are 16 different configurations that can exist (see Brughmans and Peeples 2023: Figure 4.4). + +One common method for outlining the overall structural properties of a network is to conduct a "triad census" which counts each of the 4 or 16 possible triads for a given network. Although a triad census can be conducted on an undirected network using the `igraph::triad_census` function, a warning will be returned along with 0 results for all impossible triad configurations so be aware. The results are returned as a vector of counts of each possible node configuration in an order outlined in the help document associated with the function (see `?triad_census` for more). + + +```r +igraph::triad_census(directed_net) +``` + +``` +## [1] 1404 2007 0 134 146 174 0 0 195 0 0 0 0 0 0 +## [16] 0 +``` + +```r +igraph::triad_census(simple_net) +``` + +``` +## Warning in igraph::triad_census(simple_net): At +## vendor/cigraph/src/misc/motifs.c:1140 : Triad census called on an undirected +## graph. All connections will be treated as mutual. +``` + +``` +## [1] 1033 0 2551 0 0 0 0 0 0 0 441 0 0 0 0 +## [16] 470 +``` + +Often can be useful to visualize the motifs defined for each entry in the triad census and this can be done using the `graph_from_isomorphism_class()` function which outputs every possible combination of nodes of a given size you specify (3 in this case). We can plot these configurations in a single plot using the `ggraph` and `ggpubr` packages. These packages are described in more detail in the visualization section of this document. We label each configuration using the "isomporhism code" that are frequently used to describe triads. + + +```r +library(ggraph) +library(ggpubr) + +g <- list() +xy <- + as.data.frame(matrix( + c(0.1, 0.1, 0.9, 0.1, 0.5, 0.45), + nrow = 3, + ncol = 2, + byrow = TRUE + )) + + +names <- c("003", "012", "102", "021D", "021U", "021C", + "111D", "111U", "030T", "030C", "201", "120D", + "120U", "120C", "210", "300") + +for (i in 0:15) { + g_temp <- graph_from_isomorphism_class(size = 3, + number = i, + directed = TRUE) + g[[i + 1]] <- ggraph(g_temp, + layout = "manual", + x = xy[, 1], + y = xy[, 2]) + + xlim(0, 1) + + ylim(0, 0.5) + + geom_node_point(size = 6, col = "purple") + + geom_edge_fan( + arrow = arrow(length = unit(4, "mm"), + type = "closed"), + end_cap = circle(6, "mm"), + start_cap = circle(6, "mm"), + edge_colour = "black" + ) + + theme_graph( + plot_margin = + margin(2, 2, 2, 2) + ) + + ggtitle(names[i + 1]) + + theme(plot.title = element_text(hjust = 0.5)) +} + +# motifs ordered by order in triad_census function +ggarrange( + g[[1]], g[[2]], g[[4]], g[[7]], + g[[3]], g[[5]], g[[6]], g[[10]], + g[[8]], g[[12]], g[[11]], g[[9]], + g[[14]], g[[13]], g[[15]], g[[16]], + nrow = 4, + ncol = 4 +) +``` + + + +### Transitivity and Clustering{#Transitivity} + +A network's global average transitivity (or clustering coefficient) is three times the number of closed triads over the total number of triads in a network. This measure can be calculated using `igraph::transitivity` for simple networks with or without isolates, directed networks, and weighted networks. There are options within the function to determine the specific type of transitivity (global transitivity is the default) and for how to treat isolates. See the help document (`?igraph::transitivity`) for more details. If you want to calculate local transitivity for a particular node you can use the `type = "local"` argument. This will return a `NA` value for nodes that are not part of any triads (isolates and nodes with a single connection). + + +```r +igraph::transitivity(simple_net, type = "global") +``` + +``` +## [1] 0.7617504 +``` + +```r +igraph::transitivity(simple_net, type = "local") +``` + +``` +## Apache.Creek Atsinna Baca.Pueblo +## 0.8727273 1.0000000 NaN +## Casa.Malpais Cienega Coyote.Creek +## 0.8363636 0.8333333 0.8727273 +## Foote.Canyon Garcia.Ranch Heshotauthla +## 0.8666667 0.4358974 0.7252747 +## Hinkson Hooper.Ranch Horse.Camp.Mill +## 0.4183007 0.8727273 0.8333333 +## Hubble.Corner Jarlosa Los.Gigantes +## 0.7435897 0.8000000 0.8787879 +## Mineral.Creek.Pueblo Mirabal Ojo.Bonito +## 0.7272727 0.8333333 0.6703297 +## Pescado.Cluster Platt.Ranch Pueblo.de.los.Muertos +## 0.9272727 0.7000000 0.9555556 +## Rudd.Creek.Ruin Scribe.S Spier.170 +## 0.8333333 0.7692308 0.8333333 +## Techado.Springs Tinaja Tri.R.Pueblo +## 1.0000000 0.7582418 0.7435897 +## UG481 UG494 WS.Ranch +## 0.7142857 1.0000000 NaN +## Yellowhouse +## 0.8222222 +``` + +## Walks, Paths, and Distance{#WalksPathsDistance} + +There are a variety of network metrics which rely on distance and paths across networks that can be calculated in R. There are a great many functions available and we highlight just a few here. + +### Distance{#Distance} + +In some cases, you may simply want information about the graph distance between nodes in general or perhaps the average distance. There are a variety of functions that can help with this including `igraph::distances` and `igraph::mean_distance`. These work on simple networks, directed networks, and weighted networks. + + +```r +# Create matrix of all distances among nodes and view the first +# few rows and columns +igraph::distances(simple_net)[1:4, 1:4] +``` + +``` +## Apache.Creek Atsinna Baca.Pueblo Casa.Malpais +## Apache.Creek 0 4 4 1 +## Atsinna 4 0 2 3 +## Baca.Pueblo 4 2 0 3 +## Casa.Malpais 1 3 3 0 +``` + +```r +# Calculate the mean distance for a network +igraph::mean_distance(simple_net) +``` + +``` +## [1] 1.949425 +``` + +### Shortest Paths{#ShortestPaths} + +If you want to identify particular shortest paths to or from nodes in a network you can use the `igraph::shortest_paths` function or alternatively the `igraph::all_shortest_paths` if you want all shortest paths originating at a particular node. To call this function you simply need to provide a network object and an id for the origin and destination of the path. The simplest solution is just to call the node number. This function works with directed and undirected networks with or without weights. Although it can be applied to networks with isolates, the isolates themselves will produce `NA` results. + + +```r +# track shortest path from Apache Creek to Pueblo de los Muertos +igraph::shortest_paths(simple_net, from = 1, to = 21) +``` + +``` +## $vpath +## $vpath[[1]] +## + 5/31 vertices, named, from 05583f3: +## [1] Apache.Creek Casa.Malpais Garcia.Ranch +## [4] Heshotauthla Pueblo.de.los.Muertos +## +## +## $epath +## NULL +## +## $predecessors +## NULL +## +## $inbound_edges +## NULL +``` + +The output provides the ids for all nodes crossed in the path from origin to destination. + +### Diameter{#Diameter} + +The `igraph::diameter` function calculates the diameter of a network (the longest shortest path) and you can also use the `farthest_vertices` function to get the ids of the nodes that form the ends of that longest shortest path. This metric can be calculated for directed and undirected, weighted and unweighted networks, with or without isolates. + + +```r +igraph::diameter(directed_net, directed = TRUE) +``` + +``` +## [1] 4 +``` + +```r +igraph::farthest_vertices(directed_net, directed = TRUE) +``` + +``` +## $vertices +## + 2/30 vertices, named, from 0558f63: +## [1] Apache Creek Pueblo de los Muertos +## +## $distance +## [1] 4 +``` + +## Components and Bridges{#ComponentsAndBridges} + +Identifying fully connected subgraphs within a large network is a common analytical procedure and is quite straight forward in R using the igraph package. If you first want to know whether or not a given network is fully connected you can use the `igraph::is_connected` function to check. + + +```r +igraph::is_connected(simple_net) +``` + +``` +## [1] FALSE +``` + +```r +igraph::is_connected(simple_net_noiso) +``` + +``` +## [1] TRUE +``` + +You can also count components using the `count_components` function. + + +```r +igraph::count_components(simple_net) +``` + +``` +## [1] 2 +``` + +### Identifying Components{#IdentifyingComponents} + +If you want to decompose a network object into its distinct components you can use the `igraph::decompose` function which outputs a list object with each entry representing a distinct component. Each object in the list can then be called using `[[k]]` where `k` is the number of the item in the list. + + +```r +components <- igraph::decompose(simple_net, min.vertices = 1) + +components +``` + +``` +## [[1]] +## IGRAPH 06a4de9 UN-- 30 167 -- +## + attr: name (v/c) +## + edges from 06a4de9 (vertex names): +## [1] Apache.Creek--Casa.Malpais Apache.Creek--Coyote.Creek +## [3] Apache.Creek--Hooper.Ranch Apache.Creek--Horse.Camp.Mill +## [5] Apache.Creek--Hubble.Corner Apache.Creek--Mineral.Creek.Pueblo +## [7] Apache.Creek--Rudd.Creek.Ruin Apache.Creek--Techado.Springs +## [9] Apache.Creek--Tri.R.Pueblo Apache.Creek--UG481 +## [11] Apache.Creek--UG494 Atsinna --Cienega +## [13] Atsinna --Los.Gigantes Atsinna --Mirabal +## [15] Atsinna --Ojo.Bonito Atsinna --Pueblo.de.los.Muertos +## + ... omitted several edges +## +## [[2]] +## IGRAPH 06a4e0b UN-- 1 0 -- +## + attr: name (v/c) +## + edges from 06a4e0b (vertex names): +``` + +```r +V(components[[2]])$name +``` + +``` +## [1] "WS.Ranch" +``` + +In the example here this network is fully connected with the exception of 1 node (WS Ranch). When you run the decompose function it separates WS ranch into a component as an isolate with no edges. + +### Cutpoints{#Cutpoints} + +A cutpoint is a node, the removal which creates a network with a higher number of components. There is not a convenient igraph function for identifying cutpoints but there is a function in the `sna` package within the `statnet` suite. Using the `intergraph` package we can easily convert an `igraph` object to a `network` object (using the `asNetwork` function) within the call to use this function. + +The `sna::cutpoint` function returns the node id for any cutpoints detected. We can use the numbers returned to find the name of the node in question. + + +```r +cut_p <- cutpoints(asNetwork(simple_net)) +cut_p +``` + +``` +## [1] 18 +``` + +```r +V(simple_net)$name[cut_p] +``` + +``` +## [1] "Ojo.Bonito" +``` + +```r +set.seed(4536) +plot(simple_net) +``` + + + +The example here reveals that Ojo Bonito is a cutpoint and if we look at the figure we can see that it is the sole connection with Baca Pueblo which would otherwise become and isolate and distinct component if Ojo Bonito were removed. + +### Bridges{#Bridges} + +A bridge is an edge, the removal of which results in a network with a higher number of components. The function igraph::min_cut finds bridges in network objects for sets of nodes or for the graph as a whole. The output of this function includes a vector called \$cut which provides the edges representing bridges. By default this function only outputs the cut value but you can use the argument `value.only = FALSE` to get the full output. + + +```r +min_cut(simple_net_noiso, value.only = FALSE) +``` + +``` +## $value +## [1] 1 +## +## $cut +## + 1/167 edge from 0558911 (vertex names): +## [1] Ojo Bonito--Baca Pueblo +## +## $partition1 +## + 1/30 vertex, named, from 0558911: +## [1] Baca Pueblo +## +## $partition2 +## + 29/30 vertices, named, from 0558911: +## [1] Apache Creek Casa Malpais Coyote Creek +## [4] Hooper Ranch Horse Camp Mill Hubble Corner +## [7] Mineral Creek Pueblo Rudd Creek Ruin Techado Springs +## [10] Tri-R Pueblo UG481 UG494 +## [13] Atsinna Cienega Los Gigantes +## [16] Mirabal Ojo Bonito Pueblo de los Muertos +## [19] Scribe S Spier 170 Tinaja +## [22] Garcia Ranch Hinkson Heshotauthla +## [25] Jarlosa Pescado Cluster Yellowhouse +## [28] Foote Canyon Platt Ranch +``` + +As this example illustrates the edge between Ojo Bonito and Baca Pueblo is a bridge (perhaps not surprising as Ojo Bonito was a cut point). + +## Cliques and Communities{#CliquesAndCommunities} + +Another very common task in network analysis involves creating cohesive sub-groups of nodes in a larger network. There are wide variety of methods available for defining such groups and we highlight a few of the most common here. + +### Cliques{#Cliques} + +A clique as a network science concept is arguably the strictest method of defining a cohesive subgroup. It is any set of three or more nodes in which each node is directly connected to all other nodes. It can be alternatively defined as a completely connected subnetwork, or a subnetwork with maximum density. The function `igraph::max_cliques` finds all maximal cliques in a network and outputs a list object with nodes in each set indicated. For the sake of space here we only output one clique of the 24 that were defined by this function call. + + +```r +max_cliques(simple_net, min = 1)[[24]] +``` + +``` +## + 9/31 vertices, named, from 05583f3: +## [1] Los.Gigantes Cienega Tinaja Spier.170 +## [5] Scribe.S Pescado.Cluster Mirabal Heshotauthla +## [9] Yellowhouse +``` + +Note in this list that the same node can appear in more than one maximal clique. + +### K-cores{#KCores} + +A k-core is a maximal subnetwork in which each vertex has at least degree k within the subnetwork. In R this can be obtained using the `igraph::coreness` function and the filtering by value as appropriate. This function creates a vector of k values which can then be used to remove nodes as appropriate or symbolize them in plots. + + +```r +# Define coreness of each node +kcore <- coreness(simple_net) +kcore[1:6] +``` + +``` +## Apache.Creek Atsinna Baca.Pueblo Casa.Malpais Cienega Coyote.Creek +## 9 8 1 9 9 9 +``` + +```r +# set up color scale +col_set <- heat.colors(max(kcore), rev = TRUE) +set.seed(2509) +plot(simple_net, vertex.color = col_set[kcore]) +``` + + + +In the plot shown here the darker read colors represent higher maximal k-core values. + +### Cluster Detection Algorithms{#ClusterDetection} + +R allows you to use a variety of common cluster detection algorithms to define groups of nodes in a network using a variety of different assumptions. We highlight a few of the most common here. + +#### Girvan-Newman Clustering{#GirvanNewman} + +Girvan-Newman clustering is a divisive algorithm based on betweenness that defines a partition of network that maximizes modularity by removing nodes with high betweenness iteratively (see discussion in Brughmans and Peeples 2023 Chapter 4.6). In R this is implemented with the `igraph::cluster_edge_betweenness` function. This function can be used on directed or undirected networks with or without edge weights. This function outputs a variety of information including individual edge betweenness scores, modularity information, and partition membership. See the help documents for more information + + +```r +gn <- igraph::cluster_edge_betweenness(simple_net) +set.seed(4353) +plot(simple_net, vertex.color = gn$membership) +``` + + + +#### Walktrap Algorithm{#Walktrap} + +The walktrap algorithm is designed to work for either binary or weighted networks and defines communities by generating a large number of short random walks and determining which sets of nodes consistently fall along the same short random walks. This can called using the `igraph::cluster_walktrap` function. The "steps" argument determines the length of the short walks and is set to 4 by default. + + +```r +wt <- igraph::cluster_walktrap(simple_net, steps = 4) +set.seed(4353) +plot(simple_net, vertex.color = wt$membership) +``` + + + +#### Louvain Modularity{#Louvain} + +Louvain modularity is a cluster detection algorithm based on modularity. The algorithm iteratively moves nodes among community definitions in a way that optimizes modularity. This measure can be calculated on simple networks, directed networks, and weighted networks and is implemented in R through the `igraph::cluster_louvain` function. + + +```r +lv <- igraph::cluster_louvain(simple_net) +set.seed(4353) +plot(simple_net, vertex.color = lv$membership) +``` + + + +#### Calculating Modularity for Partitions{#Modularity} + +If you would like to compare modularity scores among partitions of the same graph, this can be achieved using the `igraph::modularity` function. In the modularity call you simply supply an argument indicating the partition membership for each node. Note that this can also be used for attribute data such as regional designations. In the following chunk of code we will compare modularity for each of the clustering methods described above as well using subregion designations [from the original Cibola region attribute data](data/Cibola_attr.csv) + + +```r +# Modularity for Girvan-Newman +modularity(simple_net, membership = membership(gn)) +``` + +``` +## [1] 0.4103589 +``` + +```r +# Modularity for walktrap +modularity(simple_net, membership = membership(wt)) +``` + +``` +## [1] 0.4157195 +``` + +```r +# Modularity for Louvain clustering +modularity(simple_net, membership = membership(lv)) +``` + +``` +## [1] 0.4131378 +``` + +```r +# Modularity for subregion +cibola_attr <- read.csv("data/Cibola_attr.csv") +modularity(simple_net, membership = as.factor(cibola_attr$Region)) +``` + +``` +## [1] 0.1325612 +``` + +Note that although modularity can be useful in comparing among partitions like this approach has been shown to be poor at detecting small communities within a network so will not always be appropriate. + +#### Finding Edges Within and Between Communities{#FindingEdgesBetween} + +In many cases you may be interested in identifying edges that remain within or extend between some network partition. This can be done using the `igraph::crossing` function. This function expects a igraph cluster definition object and an igraph network and will return a list of `TRUE` and `FALSE` values for each edge where true indicates an edge that extends beyond the cluster assigned to the nodes. Let's take a look at the first 10 edges in our simple_net object based on the Louvain cluster definition. + + +```r +igraph::crossing(lv, simple_net)[1:6] +``` + +``` +## Apache.Creek|Casa.Malpais Apache.Creek|Coyote.Creek +## FALSE FALSE +## Apache.Creek|Hooper.Ranch Apache.Creek|Horse.Camp.Mill +## FALSE FALSE +## Apache.Creek|Hubble.Corner Apache.Creek|Mineral.Creek.Pueblo +## FALSE FALSE +``` + +Beyond this, if you plot an igraph object and add a cluster definition to the call it will produce a network graph with the clusters outlined and with nodes that extend between clusters shown in red. + + +```r +set.seed(54) +plot(lv, simple_net) +``` + + + +## Case Study: Roman Roads{#ExploratoryRomanRoads} + +In the case study provided at the end of Chapter 4 of Brughmans and Peeples (2023) we take a simple network based on [Roman era roads](#RomanRoads) and spatial proximity of settlements in the Iberian Peninsula and calculate some basic exploratory network statistics. As described in the book, we can create different definitions and criteria for network edges and these can have impacts on the network and node level properties. In this case, we define three different networks as follows: + +* **`road_net`** - A basic network where every road connecting two settlements is an edge +* **`road_net2`** - A network that retains all of the ties of the above network but also connects isolated nodes that are within 50 Kms of one of the road network settlements +* **`road_net3`** - A network that retains all of the ties of the first road network but connects each isolate to its nearest neighbor among the road network settlements + +First let's read in the [data file](data/road_networks.RData) that contains all three networks and start by plotting them in turn on a map. We are using a custom network map function here that is save in a file called [map_net.R](scripts/map_net.R) that takes locations with decimal degrees locations and plots a network directly on a map. We will go over the specifics of the function in more detail in the [Network Visualization](#Visualization) and [Spatial Networks](#SpatialNetworks) sections but here we simply call the script directly from the .R file. Make sure you have the libraries initialized below to replicate this map. + + +```r +library(igraph) +library(ggmap) +library(sf) +library(dplyr) + +# Read in required data +load("data/road_networks.RData") + +source("scripts/map_net.R") + +# Create Basic network map +map_net( + nodes = nodes, + net = road_net, + bounds = c(-9.5, 36, 3, 43.8), + gg_maptype = "watercolor", + zoom_lev = 6, + map_title = "Basic Network" +) +``` + + + +```r +# Create Basic network map +map_net( + nodes = nodes, + net = road_net2, + bounds = c(-9.5, 36, 3, 43.8), + gg_maptype = "watercolor", + zoom_lev = 6, + map_title = "Basic Network+ 50Km Buffer" +) +``` + + + +```r +# Create Basic network map +map_net( + nodes = nodes, + net = road_net3, + bounds = c(-9.5, 36, 3, 43.8), + gg_maptype = "watercolor", + zoom_lev = 6, + map_title = "Basic Network + Nearest Neighbor Isolates" +) +``` + + + +Now that we've replicated the visuals, we want to replicate network statistics. Since we're going to calculate several of the same network statistics for the networks in question, we can wrap this all into a function to save a bit of time. The following function expects an `igraph` network object and calculates each of the 10 variables show in the example in the book and returns them as a matrix. + +Although the function below is somewhat long, it is very simple. It defines a function with a single argument `net` which is an `igraph` network object. It then creates an output matrix called `out` with the appropriate number of rows and columns and then populates the first column with the name of each measure. Next each network measure is evaluated in turn and assigned to the appropriate row in column 2 of the `out` matrix. Finally, the full matrix is returned: `return(out)`. + + +```r +library(igraph) + +net_stats <- function(net) { + out <- matrix(NA, 10, 2) + out[, 1] <- c("Nodes", "Edges", "Isolates", "Density", "Average Degree", + "Average Shortest Path", "Diamater", + "Clustering Coefficient", "Closed Triad Count", + "Open Triad Count") + # number of nodes + out[1, 2] <- vcount(net) + # number of edges + out[2, 2] <- ecount(net) + # number of isolates + out[3, 2] <- sum(igraph::degree(net) == 0) + # network density rounding to the third digit + out[4, 2] <- round(edge_density(net), 3) + # mean degree rounding to the third digit + out[5, 2] <- round(mean(igraph::degree(net)), 3) + # mean shortest path length rounding to the third digit + out[6, 2] <- round(igraph::mean_distance(net), 3) + # network diameter + out[7, 2] <- igraph::diameter(net) + # average global transitivity rounding to the third digit + out[8, 2] <- round(igraph::transitivity(net, type = "average"), 3) + # closed triads in triad_census + out[9, 2] <- igraph::triad_census(net)[16] + # open triads in triad_census + out[10, 2] <- igraph::triad_census(net)[11] +return(out) +} +``` + +Now let's run it for each of the three networks in turn to reproduce the results in the book. We then combine the results into a single table that is nicely formatted using the `kable` function. If you'd prefer you can simply view the results of `net_stats()` right at the console. + + +```r +ns1 <- net_stats(road_net) + +ns2 <- net_stats(road_net2) + +ns3 <- net_stats(road_net3) + +ns_res <- cbind(ns1, ns2[, 2], ns3[, 2]) +colnames(ns_res) <- c("Measure", "Basic Network", "50 Km Buffer", + "Nearest Neighbors") + +knitr::kable(ns_res, format = "html") +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Measure Basic Network 50 Km Buffer Nearest Neighbors
Nodes 122 122 122
Edges 127 144 160
Isolates 33 22 0
Density 0.017 0.02 0.022
Average Degree 2.082 2.361 2.623
Average Shortest Path 6.603 6.758 7.088
Diamater 15 15 16
Clustering Coefficient 0.162 0.199 0.136
Closed Triad Count 19 23 19
Open Triad Count 241 279 331
+ +For an extended discussion of the Cranborne Chase case study and exponential random graph models [click here](#ERGM) diff --git a/04-uncertainty.md b/04-uncertainty.md new file mode 100644 index 0000000..8072d5c --- /dev/null +++ b/04-uncertainty.md @@ -0,0 +1,1073 @@ +# Quantifying Uncertainty{#Uncertainty} + +![](images/image_break.png){width=100%} + +In almost any archaeological network study, the networks we create are incomplete (i.e., we know that we are missing nodes or edges for various reasons: site destruction, lack of survey coverage, looting, etc.). How might the fact that our networks are samples of a larger and typically unobtainable “total network” influence our interpretations of network structure and node position? In this section, we take inspiration from recent research in other areas of network research (Borgatti et al. 2006; Costenbader and Valente 2003; Smith and Moody 2013; Smith et al. 2017; Smith et al. 2022) and develop a means for assessing the impact of missing and poor quality information in our networks. This accompanies Chapter 5 of Brughmans and Peeples (2023) and we recommend that you read Chapter 5 as you work through the examples below. + +For most of the other analyses presented in the book it is possible to use a number of different network software packages to conduct similar analyses. The analyses presented in Chapter 5, however, require the creation of custom scripts and procedures that are only possible in a programming language environment like R. We attempt here to not only provide information on how to replicate the examples in the book but also provide guidance on how you might modify the functions and code provided here for your own purposes. + +## R Scripts and Custom Functions{#UncertaintyScripts} + +In this chapter, we have created a number of relatively complex custom functions to conduct the assessments of network uncertainty outlined in Chapter 5. We provide step by step overviews of how these functions work below but it is useful to bundle the functions into .R script files and call them directly from the file when working with your own data. + +The scripts described in detail below include: + +* **[sim_missing_nodes.R](scripts/sim_missing_nodes.R)** - Assessments of the stability of centrality metrics for networks with nodes missing at random or due to a biased sampling process. +* **[sim_missing_edges.R](scripts/sim_missing_edges.R)** -Assessments of the stability of centrality metrics for networks with edges missing at random or due to a biased sampling process. +* **[sim_missing_inc.R](scripts/sim_missing_inc.R)** - Assessments of the stability of centrality metrics for networks with nodes missing at random or based on biased sampling from incidence matrix data. +* **[sim_target_node.R](scripts/sim_target_node.R)** - Assessments of the stability in rank order position of a target node in networks with nodes missing at random or due to a biased sampling process. +* **[sim_samp_error.R](scripts/sim_samp_error.R)** - Assessments of the stability centrality metrics due to sampling error in frequency data underlying similarity networks. +* **[edge_prob.R](scripts/edge_prob.R)** - Functions for conducting edge probability modeling and plotting candidate networks and centrality distributions. + +Each of these are described in greater detail below along with an example. To run these scripts in R, you need to put them in your working directory and then use the `source()` function. The `source` function will run all code within the .R file and initialize any functions they contain. For example: + + +```r +source("scripts/sim_missing_nodes.R") +``` + +Note that you must include the correct absolute or relative file path for the script to run properly. + +## A General Approach to Uncertainty{#UncertaintyGeneral} + +As outlined in the book, our basic approach to quantifying and dealing with uncertainty is to use the sample we have as a means for understanding the robustness or vulnerability of the population from which that sample was drawn to the kinds of variability or perturbations we might expect. The procedures we outline primarily take the following basic form: + +* Define a network based on the available sample, calculate the metrics and characterize the properties of interest in that network. +* Derive a large number of modified samples from the network created in step 1 (or the underlying data) that simulate the potential data problem or sampling issue we are trying to address. For example, if we are interested in the impact of nodes missing at random, we could randomly delete some proportion of the nodes in each sample derived from the network created in step 1. +* Calculate the metrics and characterize the properties of the features of interest in every one of the random samples created in step 2 and assess central tendency (mean, median) and distributional properties (range, standard deviation, distribution shape, etc.) or other features of the output as appropriate. +* Compare the distributions of metrics and properties (at the graph, node, or edge level) from the random samples to the “original” network created in step 1 to assess the potential impacts of the perturbation or data treatment. This comparison between the properties of the network created in step 1 and the distribution of properties created in step 3 will provide information directly relevant to assessing the impact of the kind of perturbation created in step 2 on the original network sample and, by extension, the complete network from which it was drawn. + +The underlying assumption of the approach outlined here is that the robustness or vulnerability to a particular perturbation of the observed network data, drawn from a total network that is unattainable, provides information about the robustness or vulnerability of that unattainable total network to the same kinds of perturbations. For example, if we are interested in exploring the degree distribution of a network and our sampling experiments show massive fluctuations in degree in sub-samples with only small numbers of nodes removed at random, this would suggest that the particular properties of this network are not robust to nodes missing at random for degree calculations. From this, we should not place much confidence in any results obtained from the original sample as indicative of the total network from which it was drawn. On the other hand, say we instead find that in the resampling experiments the degree distributions in our sub-samples are substantially similar to that of the original network sample even when moderate or large numbers of nodes are removed. In that case, we might conclude that our network structure is such that assessments of degree distribution are robust to node missigness within the range of what we might expect for our original sample. It is important to note, however, that this finding should not be transferred to any other metrics as any given network is likely to be robust to certain kinds of perturbations for certain network metrics, but not to others. + +## Nodes Missing at Random{#NodesAtRandom} + +This sub-section accompanies the discussion of nodes or edges missing at random in Brughmans and Peeples (2023) Chapter 5.3.1. Here we take one interval of the Chaco World ceramic similarity network (ca. A.D. 1050-1100) and simulate the impact of nodes missing at random on network centrality statistics. Download the [ceramic similarity adjacency matrix](data/AD1050net.csv) to follow along. + +The first thing we need to do is initialize our required libraries, import the network adjacency matrix, and convert it into a `igraph` network object. For several of the examples in this section we are using a simple undirected network though the code would also work for weighted or directed networks as well. + + +```r +library(igraph) +library(reshape2) +library(ggraph) +library(ggpubr) +library(statnet) +``` + +``` +## Installed ReposVer Built +## ergm "4.6.0" "4.12.0" "4.2.3" +## ergm.count "4.1.1" "4.1.3" "4.2.3" +## ndtv "0.13.3" "0.13.4" "4.2.3" +## network "1.18.2" "1.20.0" "4.2.3" +## networkDynamic "0.11.4" "0.12.0" "4.2.3" +## sna "2.7-2" "2.8" "4.2.3" +## statnet.common "4.9.0" "4.13.0" "4.2.3" +## tergm "4.2.0" "4.2.2" "4.2.3" +## tsna "0.3.5" "0.3.6" "4.2.3" +``` + +```r +# Import adjacency matrix and covert to network +chaco <- read.csv(file = "data/AD1050net.csv", row.names = 1) + +chaco_net <- igraph::graph_from_adjacency_matrix(as.matrix(chaco), + mode = "undirected") +``` + + +First, following Chapter 5.3.1, we will assess the robustness of these data to nodes missing at random for betweenness and eigenvector centrality. In order to do this we need to define a function that removes a specified proportion of nodes at random, assesses the specified metric of interest, and compares each sub-sample to the original sample in terms of the rank order correlation (Spearman's $\rho$) among nodes for the metric in question. + +To help you understand how this works, we first will walk through this example line by line for a single centrality measure so that you can see how this process is designed. Following that, we initialize [a more complex script](scripts/sim_missing_nodes.R) which can conduct the same analysis for multiple measures and even accommodate biased sampling processes as we will see below. + +Let's start with the simple version. We have commented the code chunk below so that you can follow along with the process. We define two variables along the way: + +* `nsim` which is the number of simulations to conduct at each sampling fraction +* `props` which is a vector of sampling proportions to be test (0 > value < 1). + + +```r +# Calculate node level metric of interest (betweenness in this case) +# in the original network object +met_orig <- igraph::betweenness(chaco_net) + +# Define variables +nsim <- 1000 # How many random simulations to create at each sampling level +props <- c(0.9, 0.8, 0.7, 0.6, 0.5, # set sub-sample proportions to test + 0.4, 0.3, 0.2, 0.1) + +# Create an output matrix that will receive the results +output <- matrix(NA, nsim, length(props)) +colnames(output) <- as.character(props) + +# Using for loops iterate over every value of props defined above nsim times +for (j in seq_len(length(props))) { + for (i in 1:nsim) { + # define a sub-sample at props[j] by retaining nodes from network + sub_samp <- sample(seq(1, vcount(chaco_net)), size = + round(vcount(chaco_net) * props[j], 0)) + # Create a network sub-set based on the sample defined above + sub_net <- igraph::induced_subgraph(chaco_net, sort(sub_samp)) + # Calculate betweenness (or any measure of interest) in the sub-set + temp_stats <- igraph::betweenness(sub_net) + # Assess Spearman's rho correlation between met_orig and temp_stats + # and record in the output object at row i and column j. + output[i, j] <- suppressWarnings(cor(temp_stats, + met_orig[sort(sub_samp)], + method = "spearman")) + } +} # repeat for all values of props, nsim times each + + +# Visualize the results as a box plot using ggplot. +# Melt wide data format into long data format first. +df <- melt(as.data.frame(output)) + +# Plot visuals +ggplot(data = df) + + geom_boxplot(aes(x = variable, y = value)) + + xlab("Sub-Sample Size as Proportion of Original") + + ylab(expression("Spearman's" ~ rho)) + + theme_bw() + + # The lines inside this theme() call are simply + # there to change the font size of the figure + theme( + axis.text.x = element_text(size = rel(2)), + axis.text.y = element_text(size = rel(2)), + axis.title.x = element_text(size = rel(2)), + axis.title.y = element_text(size = rel(2)), + legend.text = element_text(size = rel(1)) + ) +``` + + + +The code above worked reasonably well but it would be a bit laborious to modify the code every time we wanted to use a different data set or a different network metric or to consider biased sampling processes. In order to address this issue, we have created a general function called `sim_missing_nodes` that can replicate the analysis shown in the previous section for more centrality measures (betweenness, degree, or eigenvector) and, as we will see below, can also be used to assess biased sampling processes. The function is essentially structured just like what we saw in the last chunk but with a few additions to assess which measure you plan on using, and to catch other errors. + +The function requires the following arguments: + +* **`net`** - An `igraph` network object which can be undirected, directed, or weighted but must be a one-mode network. +* **`nsim`** - The number of random simulated networks to be created at each sampling fraction. The default is 1000. +* **`props`** - A vector containing the sampling fractions to consider. Numbers must be in decimal form and be greater than 0 and less than 1. The default is `props = c(0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1)`. Note that for small networks it is inadvisable to use very small values of `props`. +* **`met`** - This argument is used to define the metric of interest and must be one of: `"betweenness"`, `"degree"`, or `"eigenvector"`. If you do not specify this argument you will receive an error. +* **`missing_probs`** - This argument expects a vector that has as many values as there are nodes in the network. Each value should be a probability value between 0 and 1 (inclusive) that a node will be retained in a sub-sample of the network. The default for this argument is `NA`. If the argument isn't specified the function assumes you are testing nodes missing at random. + +Let's take a look at the code. You can also download [the script](scripts/sim_missing_nodes.R) to use it on your own data. + + +```r +sim_missing_nodes <- function(net, + nsim = 1000, + props = c(0.9, 0.8, 0.7, 0.6, 0.5, + 0.4, 0.3, 0.2, 0.1), + met = NA, + missing_probs = NA) { + # Initialize required library + require(reshape2) + + props <- as.vector(props) + + if (FALSE %in% (is.numeric(props) & (props > 0) & (props <= 1))) { + stop("Variable props must be numeric and be between 0 and 1", + call. = F) + } + + # Select measure of interest based on variable met and calculate + if (!(met %in% c("degree", "betweenness", "eigenvector"))) { + stop( + "Argument met must be either degree, betweenness, or eigenvector. + Check function call.", + call. = F + ) + } + else { + if (met == "degree") { + met_orig <- igraph::degree(net) + } + else { + if (met == "betweenness") { + met_orig <- igraph::betweenness(net) + } + else { + if (met == "eigenvector") { + met_orig <- igraph::eigen_centrality(net)$vector + } + } + } + } + +# Create data frame for out put and name columns + output <- matrix(NA, nsim, length(props)) + colnames(output) <- as.character(props) + +# Iterate over each value of props and then each value from 1 to nsim + for (j in seq_len(length(props))) { + for (i in 1:nsim) { + # Run code in brackets if missing_probs is NA + if (is.na(missing_probs)[1]) { + sub_samp <- sample(seq(1, vcount(net)), + size = round(vcount(net) * props[j], 0)) + sub_net <- igraph::induced_subgraph(net, sort(sub_samp)) + } + # Run code in brackets if missing_probs contains values + else { + sub_samp <- sample(seq(1, vcount(net), prob = missing_probs), + size = round(vcount(net) * props[j], 0)) + sub_net <- igraph::induced_subgraph(net, sort(sub_samp)) + } + # Select measure of interest based on met and calculate(same as above) + if (met == "degree") { + temp_stats <- igraph::degree(sub_net) + } + else { + if (met == "betweenness") { + temp_stats <- igraph::betweenness(sub_net) + } + else { + if (met == "eigenvector") { + temp_stats <- igraph::eigen_centrality(sub_net)$vector + } + } + } + # Record output for row and column by calculating Spearman's rho between + # met_orig and each temp_stats iteration. + output[i, j] <- suppressWarnings(cor(temp_stats, + met_orig[sort(sub_samp)], + method = "spearman")) + } + } + # Return output as data.frame + df_output <- suppressWarnings(reshape2::melt(as.data.frame(output))) + return(df_output) +} +``` + +The script is long, but it largely consists of a series of `if...else` statements that select the appropriate analyses based on the user supplied arguments. + +Let's give this new function a try and calculate eigenvector centrality on the same `chaco_net` network object. Note that the function above has the `melt` function built in so the data returned are in a format required to create the plot. + + +```r +# Run the function +set.seed(5609) +ev_test <- sim_missing_nodes(net = chaco_net, met = "eigenvector") + +ggplot(data = ev_test) + + geom_boxplot(aes(x = variable, y = value)) + + xlab("Sub-Sample Size as Proportion of Original") + + ylab(expression("Spearman's" ~ rho)) + + theme_bw() + + theme( + axis.text.x = element_text(size = rel(2)), + axis.text.y = element_text(size = rel(2)), + axis.title.x = element_text(size = rel(2)), + axis.title.y = element_text(size = rel(2)), + legend.text = element_text(size = rel(1)) + ) +``` + + + +## Edges Missing at Random{#EdgesAtRandom} + +In some cases, we are interested in the potential impact of missing edges rather than missing nodes. For example, if we have created a network of co-presence (based on shared ceramic types, site mentions on monuments, or some other similar data) how robust is our network to the omission of certain edges? How are different centrality metrics influenced by edge omission? + +In this section, will conduct an analysis similar to that described above for assessing missing nodes. Specifically, we will sub-sample our networks by removing a fraction of edges and then test the stability of centrality measures and node position across a range of sampling fractions. + +The function we defined in the previous only needs to be modified slightly to help assess edges as well. We have created a new function and [associated script file](scripts/sim_missing_edges.R) that accomplishes this goal. To give you a peak beneath the hood, here are the primary lines that we needed to change. We replaced the first chunk of code here which creates a sub sample based on `vcount` or vertex count with a new line that uses `ecount` or edge count. We then further switched the `induced_subgraph` and instead used the `delete_edges` function. + +
+

Note that you should not try to evaluate the chunk of code below as +it contains only portions of the larger functions described here and +will return an error.

+
+ + +```r +# Code from sim_missing_nodes +sub_samp <- sample(seq(1, vcount(net), prob = missing_probs), + size = round(vcount(net) * props[j], 0)) +sub_net <- igraph::induced_subgraph(net, sort(sub_samp)) + +# Replaced code in sim_missing_edges +sub_samp <- sample(seq(1, ecount(net), prob = missing_probs), + size = round(ecount(net) * props[j], 0)) +sub_net <- igraph::delete_edges(net, which(!(seq(1, ecount(net)) + %in% sub_samp))) +``` + +Let's call our new function and assess the impact of edges missing at random on degree centrality to give this a try. We use the same default arguments for `nsim` and `props` as we did above. Since we have saved our function as a .R file, we can initialize it using the `source` function which lets you run all code in a specified .R file. + + +```r +# First initialize the function using the .R script +source("scripts/sim_missing_edges.R") + +# Run the function +set.seed(5609) +dg_edge_test <- sim_missing_edges(net = chaco_net, met = "degree") + +# Visualize the results +ggplot(data = dg_edge_test) + + geom_boxplot(aes(x = variable, y = value)) + + xlab("Sub-Sample Size as Proportion of Original") + + ylab(expression("Spearman's" ~ rho)) + + theme_bw() + + theme( + axis.text.x = element_text(size = rel(2)), + axis.text.y = element_text(size = rel(2)), + axis.title.x = element_text(size = rel(2)), + axis.title.y = element_text(size = rel(2)), + legend.text = element_text(size = rel(1)) + ) +``` + + + +## Assessing Indivdiual Nodes/Edges{#IndNodesAtRandom} + +This sub-section follows along with Chapter 5.3.2 in Brughmans and Peeples (2023). In some cases we may be interested not simply in the robustness of a particular network metric to a specific kind of perturbation across all nodes or edges, but instead in the potential variability of the position or characteristics of a single node (or group of nodes) due to such perturbations. In order to explore individual nodes, we can employ similar procedures to those outlined above with a few additional modifications to our function. In this example we will use the [Cibola technological similarity network](#Cibola). As we describe in the book, we want to assess the stability of the position of the Garcia Ranch site. Specifically, we want to know the whether or not the high betweenness centrality value of Garcia Ranch is robust to nodes missing at random in this network. + +We define a new function to conduct these analyses below. This function is similar to those used above but instead of providing Spearman's $/rho$ values it outputs the specific rank order of the node in question across each simulation. + +The function requires six pieces of information from the user: + +* **`net`** - An `igraph` network object. Again this is currently set up for simple networks but could easily be modified. +* **`target`** - The name of the `target` node you wish to assess (exactly as it is written in the network object). +* **`prop`** - The proportion of nodes you wish to retain in the test. This should be a single number > 0 and < 1. +* **`nsim`** - The number of simulations. The default is 1000. +* **`met`** - This argument is used to define the metric of interest and must be one of: `"betweenness"`, `"degree"`, or `"eigenvector"`. If you do not specify this argument you will receive an error. +* **`missing_probs`** - This argument expects a vector that has as many values as there are nodes in the network. Each value should be a probability value between 0 and 1 (inclusive) that a node will be retained in a sub-sample of the network. You must include a probability value for the `target` node though that node will always be retained no matter what value you use. The default for this argument is `NA`. If the argument isn't specified the function assumes you are testing nodes missing at random. + +Briefly how this function works is it first determines which node number corresponds with the `target` you wish to assess and creates `nsim` subgraphs that retain that target. The metric of interest is then calculated in each network and the rank order position of the target node in every network is returned. This function can be calculated either as a "missing at random" process by leaving `missing_probs` set to the default `NA`. + +Let's take a look at an example. [Use these data](data/Cibola_edgelist.csv) to follow along. You can download the [sim_target_node.R script here](scripts/sim_target_node.R) + +First we read in the data: + + +```r +# Read in edge list file as data frame and create network object +cibola_edgelist <- + read.csv(file = "data/Cibola_edgelist.csv", header = TRUE) +cibola_net <- + igraph::graph_from_edgelist(as.matrix(cibola_edgelist), + directed = FALSE) +``` + +Now lets initialize our function from our script using the `source()` function, run the function, and create a bar plot to visualize the results. Following the example in the book, we are assessing the robustness of the rank order position of Garcia Ranch in terms of betweenness centrality to nodes missing at random. Note that Garcia Ranch has the second highest centrality score in the original network: + + +```r +source("scripts/sim_target_node.R") + +# Run the function +set.seed(52793) +gr <- sim_target_node( + net = cibola_net, + target = "Garcia Ranch", + prop = 0.8, + nsim = 1000, + met = "betweenness" +) + +# Visualize the results +df <- as.data.frame(gr) +colnames(df) <- "RankOrder" + +ggplot(df, aes(x = RankOrder)) + + geom_bar() + + theme_bw() + + labs(title = " ", x = "Rank Order", y = "Count") + + theme( + axis.text.x = element_text(size = rel(2)), + axis.text.y = element_text(size = rel(2)), + axis.title.x = element_text(size = rel(2)), + axis.title.y = element_text(size = rel(2)) + ) +``` + + + +As we describe in the book, the position of Garcia Ranch as a highly central node appears to be stable to nodes missing at random. Indeed, by far the most common position for this node was 2 which is its position in the original network. + +## Nodes/Edges Missing Due to Biased Sampling{#MissingBiased} + +This sub-section follows along with Brughmans and Peeples (2023) Chapter 5.3.3. There are many contexts where we are interested in modeling data that are not missing at random but instead are influenced by some biased sampling process. For example, say we have a study area where there have been lots of general reconnaissance surveys that have recorded most of the large sites but few full coverage surveys that have captured smaller sites. In that case, we may wish to model missingness such that small sites would more likely be missing than large sites. + +The `sim_missing_nodes` and `sime_missing_edges` functions we created above can both help us test the impacts of biased sampling processes. In order to do this we simply use the additional argument `missing_probs`. This argument requires a vector the same length as the number of nodes or edges, depending on which function you are using. This vector should contain numeric values between 0 and 1 that denote the probabilities that a node or edge will be retained in the sub-sampling effort (and must be in the same order that nodes or edges are recorded in the network object). + +Let's look under the hood to see how this change is implemented in the code. We only need to modify one line of code to include biased sampling processes. In the chunk of code below we have two lines that use the `sample` function. This function takes a vector of numbers and selects a sample (without replacement by default) of the specified size. If you add the argument `prob` it will use the vector or probabilities provided to weight the sample. That's all there is to it. + +
+

The code chunk below is just for the purposes of demonstration and +only represents part of the function so don’t try to evaluate this chunk +or you’ll get an error.

+
+ + +```r +# Random sampling process +sub_samp <- sample(seq(1, vcount(net)), + size = round(vcount(net) * props[j], 0)) + +# Biased sampling process +sub_samp <- sample(seq(1, vcount(net)), prob = missing_probs, + size = round(vcount(net) * props[j], 0)) +``` + + +Now, let's try a real example using the `chaco_net` data again by creating a random variable to stand in for `missing_probs` here. We will test the impact on missing nodes. We simply create a vector of 223 random uniform numbers using the `runif` function to simulate probabilities associated with the 223 nodes. In practice, these probabilities could be based on site size, visibility, or any other feature you choose. + + +```r +# Create 233 random numbers between 0 and 1 to stand in for +# node probabilities +set.seed(4463) +mis <- runif(223, 0, 1) +mis[1:10] +``` + +``` +## [1] 0.6903157 0.9895447 0.3810867 0.2849476 0.2689112 0.9784197 0.8042309 +## [8] 0.5805580 0.8660900 0.6179489 +``` + +```r +# Run the function +dg_test <- sim_missing_nodes(chaco_net, met = "degree", missing_probs = mis) + +# Visualize the results +ggplot(data = dg_test) + + geom_boxplot(aes(x = variable, y = value)) + + xlab("Sub-Sample Size as Proportion of Original") + + ylab(expression("Spearman's" ~ rho)) + + theme_bw() + + theme( + axis.text.x = element_text(size = rel(2)), + axis.text.y = element_text(size = rel(2)), + axis.title.x = element_text(size = rel(2)), + axis.title.y = element_text(size = rel(2)), + legend.text = element_text(size = rel(1)) + ) +``` + + + +### Resampling with Incidence Matrices{#SimIncidence} + +In the book, we illustrate our approach to biased sampling using the [co-authorship network](#ArchPubs) data. In this case we start with an incidence matrix of publications and authors and we want to assess the potential impact of missing publications on the network of authors. Since we gathered these data from digital repositories and citations, it is likely that we are missing some publications and it is reasonable to assume that we would be more likely to miss older publications than newer ones given the inclusion of newer publications in searchable digital indexes. Thus, in this example we want to assess missingness such that newer publications are more likely to be retained than older ones in our sample. We compare this to missing at random to assess how these results relate to one another. + +First we need to provide two data files. The first is the [bibliographic attribute data](data/biblio_attr.csv) which includes date, publication type, and other information on each publication designated by a unique identifier. The second is an [incidence matrix](data/biblio_dat2.csv) of publications denoted by unique identifier (rows) and authors (columns). We read this into R and then create an adjacency matrix of author to author connections using matrix algebra (multiply the incidence matrix by the transpose of the incidence matrix), convert it into a `igraph` network object and then calculate betweenness centrality for all nodes. We plot a simple network node link diagram to visualize these data. + + +```r +# Read in publication and author attribute data +bib <- read.csv("data/biblio_attr.csv") +bib[1:3, ] +``` + +``` +## Key Item.Type +## 1 FUV8A7JK journalArticle +## 2 C7MRVHWA bookSection +## 3 3EG6T4P6 journalArticle +## Publication.Title +## 1 Archaeological Review from Cambridge +## 2 Network analysis in archaeology. New approaches to regional interaction +## 3 American Antiquity +## Publication.Year Authors +## 1 2014 Stoner, Jo +## 2 2013 Isaksen, Leif +## 3 1991 Peregrine, Peter +``` + +```r +# Read in incidence matrix of publication and author data +bib_dat <- + as.matrix(read.table( + "data/biblio_dat2.csv", + header = TRUE, + row.names = 1, + sep = "," + )) + +# Create adjacency matrix from incidence matrix using matrix algebra +bib_adj <- t(bib_dat) %*% bib_dat +# Convert to igraph network object removing self loops (diag=FALSE) +bib_net <- igraph::graph_from_adjacency_matrix(bib_adj, + mode = "undirected", + diag = FALSE) +# Calculate Betweenness Centrality +bw_all <- igraph::betweenness(bib_net) + +# Plot network with nodes scaled based on betweenness +set.seed(346) +ggraph(bib_net, layout = "fr") + + geom_edge_link0(width = 0.2) + + geom_node_point(shape = 21, + aes(size = bw_all * 5), + fill = "gray", + alpha = 0.75) + + theme_graph() + + theme(legend.position = "none") +``` + + + +Although it may at first seem like we could use the same function we used previously to assess missing nodes, there are some key differences in the organization of data in this network that won't permit that. Specifically, we are interested in nodes (authors) missing at random, but we we want to model probabilities associated with publications. This is a slightly more complicated procedure because the function needs both the network object and the incidence matrix from which it was generated so that the sub-networks can be defined inside the function. We have created a `sim_missing_inc` function (simulating missing data using an incidence matrix) that conducts this task. + +This function requires five specific pieces of information from the user: + +* **`net`** - You must include a network object in `igraph` format. We use a simple network here but the code could be modified for directed or valued networks. +* **`inc`** - You must also include an incidence matrix (as an R matrix object) which describes the relationships between. The incidence matrix needs to have unique row names and column names. The mode you are interested in assessing should be the columns (in other words if you are interested in authors missing at random authors should be represented by columns and publications by rows). +* **`nsim`** - You must specify the number of simulations to perform. The default is 1000. +* **`props`** - You must specify the proportion of nodes to be retained for each set of `nsim` runs. This should be provided as a vector of proportions ranging from > 0 to 1. By default, the script will calculate a 0.9 sub-sample all the way down to a 0.1 sub-sample at 0.1 intervals using `props=c(0.9,0.8,0.7,0.6,0.5,0.4,0.3,0.2,0.1)`. +* **`lookup_dat`** - Finally, you need to provide a data frame or matrix that contains two columns. The first column should be the unique name for each row in the incidence matrix (publication key in this case). The second column should include a numeric value between 0 and 1 which indicates the probability that each row will be retained in the resampling process. If you include nothing for `missing_probs` the function will remove columns at random with an equal probability for each. + +In our example here, we must first calculate the data we need to provide for the `missing_probs` argument above. To do this we simply take the vector of publication years in the `bib` object we read in and rescale them such that the maximum value (most recent publication) equals 1 and older publications are less than 1. This will mean that older publications will more often be removed in our random sub-samples than newer ones as outlined in the example in the book. + +
+

Note that the script provided here is focused on assessing whichever +category of nodes is represented by columns in the original incidence +matrix. You would need to modify this code to use it for an incidence +matrix where rows or your target or simply use the t() +transpose function to place columns in the target position.

+
+ + + +```r +# Create a data frame of all unique combinations of publication code +# and year from attributes data +lookup <- unique(bib[, c(1, 4)]) +# Assign a probability for a publication to be retained inverse to +# the year it was published +lookup_prob <- + (lookup$Publication.Year - min(lookup$Publication.Year)) / + (max(lookup$Publication.Year) - min(lookup$Publication.Year)) + +# Create data frame with required output. We have added a sort function +# here to ensure that the order of probabilities in lookup_dat is the +# same as the order of rows in the incidence matrix. You will get +# spurious results if you do not ensure these are the same. +lookup_dat <- sort(data.frame(Key = lookup[,1], prob = lookup_prob)) +head(lookup_dat) +``` + +``` +## Key prob +## 117 24QNVV37 0.9583333 +## 86 29GVMCNZ 0.0625000 +## 81 2QC8N5RN 1.0000000 +## 112 2QU9ZNUG 0.8125000 +## 126 2T8HPW5E 0.9583333 +## 18 37TC37D2 1.0000000 +``` + +With the probabilities of retention for `missing_probs` in place, we can then call source code for our [sim_missing_inc.R script](scripts/sim_missing_inc.R) and run the function. Following the example in the book we run the function for `nsim = 1000` and for 3 sampling fractions `(0.9, 0.8, 0.7)` with the metric of interest as `betweenness`. Let's first run the function for our "probability by date" biased sampling process. + + +```r +source("scripts/sim_missing_inc.R") + +# Run function +set.seed(4634) +bib_bias <- sim_missing_inc( + net = bib_net, + inc = bib_dat, + missing_probs = lookup_dat$prob, + props = c(0.9, 0.8, 0.7), + met = "betweenness", +) +head(bib_bias) +``` + +``` +## variable value +## 1 0.9 0.9747220 +## 2 0.9 0.9608842 +## 3 0.9 0.9999614 +## 4 0.9 0.9999803 +## 5 0.9 0.9760737 +## 6 0.9 0.9999803 +``` + +Next we want to run the function again to simulate nodes missing at random. All we need to do is change the `missing_probs` argument to `NA` (or exclude that argument altogether). Let's run it: + + +```r +# Run the function +set.seed(4363) +bib_rand <- sim_missing_inc( + net = bib_net, + inc = bib_dat, + missing_probs = NA, + props = c(0.9, 0.8, 0.7), + met = "betweenness" +) +head(bib_rand) +``` + +``` +## variable value +## 1 0.9 0.9817859 +## 2 0.9 0.9804552 +## 3 0.9 0.9818085 +## 4 0.9 0.8911948 +## 5 0.9 0.8733779 +## 6 0.9 1.0000000 +``` + +Now we can combine the results into a single data frame and plot them as paired box plots for comparison. In order to create paired box plots it is easiest to create a single data frame that contains the results of both runs above. We combine these and add a new column called "Treatment" that specifies for each row in the data frame whether it was part of the Random or Biased sample. + + +```r +# Add a variable denoting which sample design it came from +bib_rand$treatment <- rep("Random", nrow(bib_rand)) +bib_bias$treatment <- rep("Biased", nrow(bib_bias)) + +# Bind into a single data frame, convert sampling faction to factor +# and change order of levels for plotting +df <- rbind(bib_rand, bib_bias) +df$variable <- as.factor(df$variable) +df$variiable <- factor(df$variable, levels = c("0.9", "0.8", "0.7")) + +# Plot the results +ggplot(data = df) + + geom_boxplot(aes(x = variable, y = value, fill = treatment)) + + scale_fill_manual(values = c("white", "gray"), name = "Group") + + xlab("Sub-Sample Size as Proportion of Original") + + ylab(expression("Spearman's" ~ rho)) + + theme_bw() + + theme( + axis.text.x = element_text(size = rel(2)), + axis.text.y = element_text(size = rel(2)), + axis.title.x = element_text(size = rel(2)), + axis.title.y = element_text(size = rel(2)), + legend.text = element_text(size = rel(2)) + ) +``` + + + +## Edge Probability Modeling{#EdgeProbability} + +In this section we take inspiration from some recent work in the area of "Dark Networks" (see Everton 2012) or the investigation of illicit networks. In this field, a number of methods have recently been developed that allow researchers to more directly incorporate assessments of the reliability of specific edges into analyses. This can be done in a number of different ways. Perhaps the most common approach for networks based on data gathered from intelligence sources (such as studies of terrorist networks) is to qualitatively assign different levels of confidence to ties between pairs of actors using an ordinal scale determined based on the source of the information (reliable, usually reliable,... unreliable). This ordinal scale of confidence can then be converted into a probability (from 0 to 1) and that probability value could be used to inform the creation of a range of "possible" networks given the underlying data. + +We are not aware of any archaeological examples where edges have been formally qualitatively assigned "confidence levels" in exactly this way, but we think there are potential applications of this method. For example, we could define a network where we assign a low probability of a tie between two archaeological sites if they share an import from a third site/region and a higher probability for a tie between two sites if they share imports from each others' region. Importantly, such methods can be used to combine information from different sources into a single assessment of the probability of connection. + +Since we do not have any data structured in exactly this way, we will use a small simulated data set that consists of and edge list with weights. [Use this file](data/sim_edge.csv) to follow along. This is a simple edge list with probability values assigned to each edge at values of 0.2, 0.4, 0.6, 0.8, and 1.0. + +Let's read in the data and plot it: + + +```r +# Read in edge_list +sim_edge <- as.matrix(read.csv("data/sim_edge.csv", + header = T, row.names = 1)) + +# Create network object and assign edge weights and node names +sim_net <- igraph::graph_from_edgelist(sim_edge[, 1:2]) +E(sim_net)$weight <- sim_edge[order(sim_edge[, 3]), 3] +V(sim_net)$name <- seq(1:20) + +# Create color ramp palette +edge_cols <- colorRampPalette(c("gray", "darkblue"))(5) + +# Plot the resulting network +set.seed(4364672) +ggraph(sim_net, layout = "fr") + + geom_edge_link0(aes(width = E(sim_net)$weight * 5), + edge_colour = edge_cols[E(sim_net)$weight * 5], + show.legend = FALSE) + + geom_node_point(shape = 21, + size = igraph::degree(sim_net) + 3, + fill = "red") + + geom_node_text( + aes(label = as.character(name)), + col = "white", + size = 3.5, + repel = FALSE + ) + + theme_graph() +``` + + + +In the next chunk of code we define a function that iterates over every edge in the simulated network we just created and defines each edge as either present or absent using a simple random binomial with the probability set by the edge weight as described above. The output of this function (`edge_prob`) is a list object that contains `nsim` `igraph` network objects that are candidate networks of the original. + +In order to extract values of interest from these candidate networks, we created another function called `compile_stat`. This function iterates over all `nsim` networks in the `net_list` list object and calculates the centrality metric of interest in this case returning the results as a simple matrix. It is then possible to compare things like average degree or the distribution of degree for particular nodes across all of the candidate networks. We have placed these two functions in an additional script file called [edge_prob.R](scripts/edge_prob) which you can download to use and modify on your own. + + +```r +# Define function for assessing and retaining edges based on edge +# weight probabilities +edge_prob <- function(net, nsim = 1000, probs) { + net_list <- list() + for (i in 1:nsim) { + sub_set <- NULL + for (j in 1:ecount(net)) { + temp <- rbinom(1, 1, prob = probs[j]) + if (temp == 1) { + sub_set <- c(sub_set, j) + } + } + net_list[[i]] <- + igraph::delete_edges(net, which(!(seq(1, ecount( + net + )) + %in% sub_set))) + } + return(net_list) +} + +# Define function for assessing statistic of interest +compile_stat <- function(net_list, met) { + out <- matrix(NA, vcount(net_list[[1]]), length(net_list)) + for (i in seq_len(length(net_list))) { + # Select measure of interest based on met and calculate(same as above) + if (met == "degree") { + out[, i] <- igraph::degree(net_list[[i]]) + } + else { + if (met == "betweenness") { + out[, i] <- igraph::betweenness(net_list[[i]]) + } + else { + if (met == "eigenvector") { + out[, i] <- igraph::eigen_centrality(net_list[[i]])$vector + } + } + } + } + return(out) +} +``` + +Now we run the edge_prob function for `nsim = 1000` and display a few candidate networks. + + +```r +el_test <- edge_prob(sim_net, nsim = 1000, probs = sim_edge[, 3]) + +set.seed(9651) +comp1 <- ggraph(el_test[[1]], layout = "fr") + + geom_edge_link0(aes(width = E(el_test[[1]])$weight), + edge_colour = edge_cols[E(el_test[[1]])$weight * 5], + show.legend = FALSE) + + geom_node_point(shape = 21, + size = igraph::degree(el_test[[1]]), + fill = "red") + + geom_node_text( + aes(label = as.character(name)), + col = "white", + size = 2.5, + repel = FALSE + ) + + theme_graph() + +comp2 <- ggraph(el_test[[2]], layout = "fr") + + geom_edge_link0(aes(width = E(el_test[[2]])$weight), + edge_colour = edge_cols[E(el_test[[2]])$weight * 5], + show.legend = FALSE) + + geom_node_point(shape = 21, + size = igraph::degree(el_test[[2]]), + fill = "red") + + geom_node_text( + aes(label = as.character(name)), + col = "white", + size = 2.5, + repel = FALSE + ) + + theme_graph() + +comp3 <- ggraph(el_test[[3]], layout = "fr") + + geom_edge_link0(aes(width = E(el_test[[3]])$weight), + edge_colour = edge_cols[E(el_test[[3]])$weight * 5], + show.legend = FALSE) + + geom_node_point(shape = 21, + size = igraph::degree(el_test[[3]]), + fill = "red") + + geom_node_text( + aes(label = as.character(name)), + col = "white", + size = 2.5, + repel = FALSE + ) + + theme_graph() + +ggarrange(comp1, comp2, comp3) +``` + + + +We then use the `compile_stat` function to assess degree centrality for one particular node, displaying a histogram of values with mean indicated. + + +```r +dg_stat <- compile_stat(el_test, met = "degree") + +dg_20 <- data.frame(val = dg_stat[20, ]) + +ggplot(dg_20, aes(val)) + + geom_histogram(binwidth = 1) + + xlab("Degree Centrality of Node 20") + + geom_vline(xintercept = mean(dg_20$val), col = "red") + + theme_bw() +``` + + + +### Edge Probability and Similarity Networks{#EdgeProbSim} + +One area of archaeological network research where the edge probability modeling approach outlined above may be of use relates to similarity networks. Many similarity networks used in archaeology are built such that the edge weights are scaled between 0 and 1. These edge weights could be thought of as "probabilities" just as we saw with the simulated example above. Indeed, this conforms with the frequent interpretation of similarity values as relating to probabilities of interaction in numerous network studies (e.g., Mills et al. 2013a, 2013b, 2015; Golitko and Feinman 2015; Golitko et al. 2012, etc.). + +Let's take a look at an example using a weighted similarity network generated using the [Cibola technological similarity data](#Cibola) used above. Download the [RData file here](data/Cibola_wt.RData) to follow along. + + +```r +load("data/Cibola_wt.RData") + +# View first few edge weights in network object +E(cibola_wt)$weight[1:10] +``` + +``` +## [1] 0.7050691 0.7757143 0.8348214 0.8656783 0.8028571 0.7329193 0.7509158 +## [8] 0.8441558 0.7857143 0.8102919 +``` + +```r +set.seed(4446347) +sim_nets <- edge_prob(cibola_wt, nsim = 1000, probs = E(cibola_wt)$weight) +``` + +Now let's plot a couple of the candidate networks: + + +```r +# Precompute layout +set.seed(9631) +xy <- layout_with_fr(cibola_wt) + +# Example 1 +comp1 <- ggraph(sim_nets[[1]], + layout = "manual", + x = xy[, 1], + y = xy[, 2]) + + geom_edge_link() + + geom_node_point(shape = 21, + size = igraph::degree(sim_nets[[1]]) / 3, + fill = "red") + + theme_graph() + +# Example 2 +comp2 <- ggraph(sim_nets[[2]], + layout = "manual", + x = xy[, 1], + y = xy[, 2]) + + geom_edge_link() + + geom_node_point(shape = 21, + size = igraph::degree(sim_nets[[2]]) / 3, + fill = "red") + + theme_graph() + +# Example 3 +comp3 <- ggraph(sim_nets[[3]], + layout = "manual", + x = xy[, 1], + y = xy[, 2]) + + geom_edge_link() + + geom_node_point(shape = 21, + size = igraph::degree(sim_nets[[3]]) / 3, + fill = "red") + + theme_graph() + +ggarrange(comp1, comp2, comp3) +``` + + + + +```r +bw_test <- compile_stat(sim_nets, met = "betweenness") + +bw_10 <- data.frame(val = bw_test[10, ]) + +ggplot(bw_10, aes(val)) + + geom_histogram() + + xlab("Betweenness Centrality of Node 10") + + geom_vline(xintercept = mean(bw_10$val), col = "red") + + theme_bw() +``` + + + + +## Uncertainty Due to Small or Variable Sample Size{#SampleSize} + +This section follows Brughmans and Peeples (2023) Chapter 5.3.5 to provide an example of how you can use the simulation approach outlined here to assess sampling variability in the frequency data underlying archaeological networks. In this example, we use apportioned ceramic frequency data from the Chaco World portion of the [Southwest Social Networks database](#SWSN). You can [download the data here](data/AD1050cer.csv) to follow along. + +The goal of this sub-section is to illustrate how you can use a bootstrapping approach to assess variability in network properties based on sampling error in the raw data underlying archaeological networks. In our example based on ceramic similarity networks here this involves creating a large number of random replicates of each row of our raw ceramic data with sample size held constant (as the observed sample size for that site) and with the probabilities that a given sherd will be a given type determined by the underlying multinomial frequency distribution of types at that site. In other words, we pull a bunch of random samples from the site with the probability that a given sample is a given type determined by the relative frequency of that type in the actual data. Once this procedure has been completed, we can then assess centrality metrics or any other graph, node, or edge level property and determine the degree to which absolute values and relative ranks are potentially influenced by sampling error. + +There are many ways to set up such a resampling procedure and many complications (for example, how do we deal with limited diversity of small samples?). For the purposes of illustration here, we will implement a very simple procedure where we simply generate new samples of a fixed size based on our observed data and determine the degree to which our network measures are robust to this perturbation. In the chunk of code below we create 1000 replicates based on our original ceramic data. + +The following chunk of code first reads in the ceramic data, converts it to a Brainerd-Robinson similarity matrix and then defines a function called `sim_samp_error` which creates `nsim` random replicates of the ceramic data, converts them to similarity matrices, and outputs those results as a list object. You can [download the script for the fucnction below here](scripts/sim_samp_error.R). + + +```r +# Read in raw ceramic data +ceramic <- + read.csv(file = "data/AD1050cer.csv", + header = TRUE, + row.names = 1) +# Convert to proportion +ceramic_p <- prop.table(as.matrix(ceramic), margin = 1) +# Convert to Brainerd-Robinson similarity matrix +ceramic_br <- ((2 - as.matrix(vegan::vegdist(ceramic_p, + method = "manhattan"))) / 2) + +# Create function for assessing impact of sampling error on +# weighted degree for similarity network +sim_samp_error <- function(cer, nsim = 1000) { + sim_list <- list() + for (i in 1:nsim) { + data_sim <- NULL + # the for-loop below creates a random multinomial replicate + # of the ceramic data + for (j in seq_len(nrow(cer))) { + data_sim <- + rbind(data_sim, t(rmultinom(1, rowSums(cer)[j], prob = cer[j, ]))) + } + # Convert simulated data to proportion, create similarity matrix, + # calculate degree, and assess correlation + temp_p <- prop.table(as.matrix(data_sim), margin = 1) + sim_list[[i]] <- ((2 - as.matrix(vegan::vegdist(temp_p, + method = "manhattan"))) / 2) + } + return(sim_list) +} +``` + +The following chunk of code runs the `sim_samp_error` function defined above for our Chaco ceramic data and then defines a new function called `sim_cor` which takes the output of `sim_samp_error` and the original ceramic similarity matrix (`ceramic_BR`) and calculates weighted degree centrality and the Spearman's $\rho$ correlations between the original similarity matrix and each random replicate. This `sim_cor` script could be modified to use any network metric that outputs a vector. Once these results are returned we visualize the results as a histogram. + +Note that this could take several seconds to a few minutes depending on your computer. + + +```r +set.seed(4634) +sim_nets <- sim_samp_error(cer = ceramic, nsim = 1000) + +sim_cor <- function(sim_nets, sim) { + # change this line to use a different metric + dg_orig <- rowSums(sim) + dg_cor <- NULL + for (i in seq_len(length(sim_nets))) { + # change this line to use a different metric + dg_temp <- rowSums(sim_nets[[i]]) + dg_cor[i] <- + suppressWarnings(cor(dg_orig, dg_temp, method = "spearman")) + } + return(dg_cor) +} + +dg_cor <- sim_cor(sim_nets, ceramic_br) + +df <- as.data.frame(dg_cor) + +ggplot(df, aes(x = dg_cor)) + + geom_histogram(bins = 100, color = "white", fill = "black") + + theme_bw() + + scale_x_continuous(name = "Correlation in Degree Centraility", + limits = c(0.9, 1)) + + theme( + axis.text.x = element_text(size = rel(1.5)), + axis.text.y = element_text(size = rel(1.5)), + axis.title.x = element_text(size = rel(1.5)), + axis.title.y = element_text(size = rel(1.5)), + legend.text = element_text(size = rel(1.5)) + ) +``` + + + +As described in Chapter 5.3.5, in some cases we want to observe patterns of variation due to sampling error for individual sites or sets of sites. In the next chunk of code we illustrate how to produce figure 5.14 from the Brughmans and Peeples (2023) book. Specifically, this plot consists of a series of line plots where the x axis represents each node in the network ordered by degree centrality in the original observed network. For each node there is a vertical line which represents the 95% confidence interval around degree across the `nsim` random replicates produced to evaluate sampling error. The blue line represents degree in the original network and the red line represents median degree in the resampled networks. + +To create this plot, we first iterate through every object in `sim_nets` and calculate weighted degree centrality and then add that to a two-column matrix along with a node id. Once we have done this for all simulations, we use the `summarise` function to calculate the mean, median, max, min, and confidence interval (95%). We then plot the observed values of degree in blue, the mean values in red, and the confidence intervals as black vertical bars for each node. The nodes are sorted from low to high degree centrality in the original network. + + + +```r +# Create data frame containing degree and site id for nsim random +# similarity matrices +df <- matrix(NA, 1, 2) # define empty matrix +# calculate degree centrality for each random run and bind in +# matrix along with id +for (i in seq_len(length(sim_nets))) { + temp <- cbind(seq(1, nrow(sim_nets[[i]])), rowSums(sim_nets[[i]])) + df <- rbind(df, temp) +} +df <- as.data.frame(df[-1, ]) # remove first row in initial matrix +colnames(df) <- c("site", "degree") # add column names + +# Use summarise function to create median, confidence intervals, +# and other statistics for degree by site. +out <- df %>% + group_by(site) %>% + dplyr::summarise( + Mean = mean(degree), + Median = median(degree), + Max = max(degree), + Min = min(degree), + Conf = sd(degree) * 1.96 + ) +out$site <- as.numeric(out$site) +out <- out[order(rowSums(ceramic_br)), ] + +# Create data frame of degree centrality for the original ceramic +# similarity matrix +dg_wt <- as.data.frame(rowSums(ceramic_br)) +colnames(dg_wt) <- "dg.wt" + +# Plot the results +ggplot() + + geom_line( + data = out, + aes( + x = reorder(site, Median), + y = Median, + group = 1 + ), + col = "red", + lwd = 1.5, + alpha = 0.5 + ) + + geom_errorbar(data = out, aes( + x = reorder(site, Median), + ymin = Median - Conf, + ymax = Median + Conf + )) + + geom_path( + data = sort(dg_wt), + aes(x = order(dg.wt), y = dg.wt), + col = "blue", + lwd = 1.5, + alpha = 0.5 + ) + + theme_bw() + + ylab("Degree") + + scale_x_discrete(name = "Sites in Rank Order of Degree") + + theme( + axis.text.x = element_blank(), + axis.ticks.x = element_blank(), + axis.text.y = element_text(size = rel(2)), + axis.title.x = element_text(size = rel(2)), + axis.title.y = element_text(size = rel(2)), + legend.text = element_text(size = rel(2)) + ) +``` + + + + diff --git a/05-visualization.Rmd b/05-visualization.Rmd index 09d8268..ad699b7 100644 --- a/05-visualization.Rmd +++ b/05-visualization.Rmd @@ -222,7 +222,9 @@ We describe the specifics of spatial data handling, geographic coordinates, and ``` ```{r, echo=F, warning=F} -source("stadia_API.R") +if (file.exists("stadia_API.R")) { + source("stadia_API.R") +} ``` @@ -230,6 +232,7 @@ source("stadia_API.R") ```{r geo_layout, warning=F, message=F, fig.heigh=7, fig.width=7, cache=T} library(sf) library(ggmap) +library(ggplot2) # Convert attribute location data to sf coordinates and change # map projection @@ -243,25 +246,35 @@ coord1 <- do.call(rbind, st_geometry(loc_trans)) %>% xy <- as.data.frame(coord1) colnames(xy) <- c("x", "y") -# Get basemap "stamen_terrain_background" data for map in black and white -# the bbox argument is used to specify the corners of the box to be -# used and zoom determines the detail. -base_cibola <- get_stadiamap( - bbox = c(-110.2, 33.4, -107.8, 35.3), - zoom = 10, - maptype = "stamen_terrain_background", - color = "bw" -) +# Get a basemap when a Stadia API key is available; otherwise fall back +# to a simple coordinate plot so the document still renders. +if (nzchar(Sys.getenv("STADIAMAPS_API_KEY"))) { + base_cibola <- get_stadiamap( + bbox = c(-110.2, 33.4, -107.8, 35.3), + zoom = 10, + maptype = "stamen_terrain_background", + color = "bw" + ) + base_plot <- ggmap(base_cibola, darken = 0.35) +} else { + base_plot <- ggplot() + + coord_quickmap(xlim = c(-110.2, -107.8), ylim = c(33.4, 35.3)) + + theme_void() + + theme( + panel.background = element_rect(fill = "grey20", color = NA), + plot.background = element_rect(fill = "grey20", color = NA) + ) +} # Extract edge list from network object -edgelist <- get.edgelist(net) +edgelist <- igraph::as_edgelist(net) # Create data frame of beginning and ending points of edges edges <- data.frame(xy[edgelist[, 1], ], xy[edgelist[, 2], ]) colnames(edges) <- c("X1", "Y1", "X2", "Y2") # Plot original data on map -ggmap(base_cibola, darken = 0.35) + +base_plot + geom_segment( data = edges, aes( @@ -806,14 +819,27 @@ colnames(xy) <- c("x", "y", "Region") # Run hammer bundling routine g <- asIgraph(brnet) -hbundle <- edge_bundle_hammer(g, xy, bw = 5, decay = 0.3) - -f6_3c <- ggplot() + - geom_path(data = hbundle, aes(x, y, group = group), - col = "gray66", size = 0.5) + - geom_point(data = xy, aes(x, y, col = Region), - size = 5, alpha = 0.75, show.legend = FALSE) + - theme_void() +if (reticulate::py_module_available("datashader.bundling")) { + hbundle <- edge_bundle_hammer(g, xy, bw = 5, decay = 0.3) + + f6_3c <- ggplot() + + geom_path(data = hbundle, aes(x, y, group = group), + col = "gray66", size = 0.5) + + geom_point(data = xy, aes(x, y, col = Region), + size = 5, alpha = 0.75, show.legend = FALSE) + + theme_void() +} else { + edgelist <- igraph::as_edgelist(g) + edges <- data.frame(xy[edgelist[, 1], 1:2], xy[edgelist[, 2], 1:2]) + colnames(edges) <- c("x", "y", "xend", "yend") + + f6_3c <- ggplot() + + geom_segment(data = edges, aes(x = x, y = y, xend = xend, yend = yend), + col = "gray66", alpha = 0.35, size = 0.4) + + geom_point(data = xy, aes(x, y, col = Region), + size = 5, alpha = 0.75, show.legend = FALSE) + + theme_void() +} f6_3c ``` @@ -834,7 +860,6 @@ Let's now look at the figure code: ```{r Fig6_3d, message=F, warning=F, fig.width=10, fig.height=10} # Initialize libraries -library(ggwaffle) library(tidyverse) # Create igraph object from data imported above @@ -877,7 +902,7 @@ nodes_wide <- igraph::as_data_frame(g, "vertices") nodes_long <- nodes_wide %>% dplyr::select(c1:c4) %>% mutate(id = seq_len(nrow(nodes_wide))) %>% - gather("attr", "value", c1:c4) + tidyr::pivot_longer(c1:c4, names_to = "attr", values_to = "value") nodes_out <- NULL for (j in seq_len(nrow(nodes_long))) { temp <- do.call("rbind", replicate(round(nodes_long[j, ]$value * 50, 0), @@ -885,48 +910,59 @@ for (j in seq_len(nrow(nodes_long))) { nodes_out <- rbind(nodes_out, temp) } -# Create a list object for the call to each bar chart by node -bar_list <- lapply(1:vcount(g), function(i) { - gt_plot <- ggplotGrob( - ggplot(waffle_iron(nodes_out[nodes_out$id == i, ], - aes_d(group = attr))) + - geom_waffle(aes(x, y, fill = group), size = 10) + - coord_equal() + - labs(x = NULL, y = NULL) + - theme( - legend.position = "none", - panel.background = element_rect(fill = "white", colour = NA), - line = element_blank(), - text = element_blank() - ) - ) - panel_coords <- gt_plot$layout[gt_plot$layout$name == "panel", ] - gt_plot[panel_coords$t:panel_coords$b, panel_coords$l:panel_coords$r] -}) - -# Convert the results above into custom annotation -annot_list <- lapply(1:vcount(g), function(i) { - xmin <- nodes_wide$x[i] - .25 - xmax <- nodes_wide$x[i] + .25 - ymin <- nodes_wide$y[i] - .25 - ymax <- nodes_wide$y[i] + .25 - annotation_custom( - bar_list[[i]], - xmin = xmin, - xmax = xmax, - ymin = ymin, - ymax = ymax - ) -}) - -# create basic network -p <- ggraph(g, "manual", x = V(g)$x, y = V(g)$y) + - geom_edge_link0() + - theme_graph() + - coord_fixed() - -# put everything together by combining with the annotation (bar plots + network) -f6_3d <- Reduce("+", annot_list, p) +if (requireNamespace("ggwaffle", quietly = TRUE)) { + library(ggwaffle) + + # Create a list object for the call to each bar chart by node + bar_list <- lapply(1:vcount(g), function(i) { + gt_plot <- ggplotGrob( + ggplot(waffle_iron(nodes_out[nodes_out$id == i, ], + aes_d(group = attr))) + + geom_waffle(aes(x, y, fill = group), size = 10) + + coord_equal() + + labs(x = NULL, y = NULL) + + theme( + legend.position = "none", + panel.background = element_rect(fill = "white", colour = NA), + line = element_blank(), + text = element_blank() + ) + ) + panel_coords <- gt_plot$layout[gt_plot$layout$name == "panel", ] + gt_plot[panel_coords$t:panel_coords$b, panel_coords$l:panel_coords$r] + }) + + # Convert the results above into custom annotation + annot_list <- lapply(1:vcount(g), function(i) { + xmin <- nodes_wide$x[i] - .25 + xmax <- nodes_wide$x[i] + .25 + ymin <- nodes_wide$y[i] - .25 + ymax <- nodes_wide$y[i] + .25 + annotation_custom( + bar_list[[i]], + xmin = xmin, + xmax = xmax, + ymin = ymin, + ymax = ymax + ) + }) + + # create basic network + p <- ggraph(g, "manual", x = V(g)$x, y = V(g)$y) + + geom_edge_link0() + + theme_graph() + + coord_fixed() + + # put everything together by combining with the annotation (bar plots + network) + f6_3d <- Reduce("+", annot_list, p) +} else { + f6_3d <- ggraph(g, "manual", x = V(g)$x, y = V(g)$y) + + geom_edge_link0(alpha = 0.4) + + geom_node_point(aes(fill = c1), shape = 21, size = 8, color = "black") + + scale_fill_gradient(low = "white", high = "steelblue") + + theme_graph() + + coord_fixed() +} f6_3d ``` @@ -1067,7 +1103,7 @@ xy <- as.data.frame(coord1) colnames(xy) <- c("x", "y") # Extract edge list from network object -edgelist <- get.edgelist(road_net) +edgelist <- igraph::as_edgelist(road_net) # Create data frame of beginning and ending points of edges edges <- as.data.frame(matrix(NA, nrow(edgelist), 4)) @@ -1156,7 +1192,7 @@ colnames(zz) <- c("x", "y") # Extract edge list from network object -edgelist <- get.edgelist(g.net) +edgelist <- igraph::as_edgelist(g.net) # Create data frame of beginning and ending points of edges edges2 <- data.frame(zz[edgelist[, 1], ], zz[edgelist[, 2], ]) @@ -1236,7 +1272,7 @@ colnames(xy) <- c("x", "y") # Extract edge list from network object for road_net -edgelist1 <- get.edgelist(aegean_net) +edgelist1 <- igraph::as_edgelist(aegean_net) # Create data frame of beginning and ending points of edges edges1 <- as.data.frame(matrix(NA, nrow(edgelist1), 4)) @@ -1853,37 +1889,68 @@ coord1 <- do.call(rbind, st_geometry(z)) %>% xy <- as.data.frame(coord1) colnames(xy) <- c("x", "y") -hbundle <- edge_bundle_hammer(g.net, xy, bw = 0.9, decay = 0.2) - -ggmap(base2, darken = 0.15) + - geom_polygon( - data = map3, - aes(x, y, - group = Group.1), - col = "black", - size = 0.5, - fill = NA - ) + - geom_path( - data = hbundle, - aes(x, y, group = group), - color = "white", - show.legend = FALSE - ) + - geom_path( - data = hbundle, - aes(x, y, group = group), - color = "darkorchid4", - show.legend = FALSE - ) + - geom_point( - data = xy, - aes(x, y), - alpha = 0.4, - size = 2.5, - show.legend = FALSE - ) + - theme_graph() +if (reticulate::py_module_available("datashader.bundling")) { + hbundle <- edge_bundle_hammer(g.net, xy, bw = 0.9, decay = 0.2) + + ggmap(base2, darken = 0.15) + + geom_polygon( + data = map3, + aes(x, y, + group = Group.1), + col = "black", + size = 0.5, + fill = NA + ) + + geom_path( + data = hbundle, + aes(x, y, group = group), + color = "white", + show.legend = FALSE + ) + + geom_path( + data = hbundle, + aes(x, y, group = group), + color = "darkorchid4", + show.legend = FALSE + ) + + geom_point( + data = xy, + aes(x, y), + alpha = 0.4, + size = 2.5, + show.legend = FALSE + ) + + theme_graph() +} else { + edgelist <- igraph::as_edgelist(g.net) + edges <- data.frame(xy[edgelist[, 1], ], xy[edgelist[, 2], ]) + colnames(edges) <- c("x", "y", "xend", "yend") + + ggmap(base2, darken = 0.15) + + geom_polygon( + data = map3, + aes(x, y, + group = Group.1), + col = "black", + size = 0.5, + fill = NA + ) + + geom_segment( + data = edges, + aes(x = x, y = y, xend = xend, yend = yend), + color = "darkorchid4", + alpha = 0.3, + show.legend = FALSE + ) + + geom_point( + data = xy, + aes(x, y), + alpha = 0.4, + size = 2.5, + show.legend = FALSE + ) + + theme_graph() +} ``` ### Figure 6.18: Group-in-a-box {- #Figure_6_18} @@ -2130,8 +2197,6 @@ Note that the data required is a list object that contains multiple temporal sli library(networkDynamic) library(ndtv) -library(scatterplot3d) -library(prettyGraphs) library(statnet) load("data/Figure6_23.Rdata") @@ -2145,42 +2210,54 @@ compute.animation(sanpedro, default.dist = 7, animation.mode = "kamadakawai") # Define colors for regions mycol <- c( - add.alpha("#1b9e77", 0.75), - add.alpha("#d95f02", 0.75), - add.alpha("#7570b3", 0.75), - add.alpha("#e7298a", 0.75), - add.alpha("#66a61e", 0.75), - add.alpha("#e6ab02", 0.75) + scales::alpha("#1b9e77", 0.75), + scales::alpha("#d95f02", 0.75), + scales::alpha("#7570b3", 0.75), + scales::alpha("#e7298a", 0.75), + scales::alpha("#66a61e", 0.75), + scales::alpha("#e6ab02", 0.75) ) -# Plot time prism -set.seed(364467) -timePrism( - sanpedro, - at = c(1, 2, 3), - displaylabels = FALSE, - planes = TRUE, - display.isolates = FALSE, - label.cex = 0.5, - usearrows = FALSE, - vertex.cex = 0.5, - edge.col = "gray50", - vertex.col = mycol[factor(sp_attr$SWSN_MacroGroup)] -) +if (requireNamespace("scatterplot3d", quietly = TRUE) && + requireNamespace("prettyGraphs", quietly = TRUE)) { + library(scatterplot3d) + library(prettyGraphs) + + # Plot time prism + set.seed(364467) + timePrism( + sanpedro, + at = c(1, 2, 3), + displaylabels = FALSE, + planes = TRUE, + display.isolates = FALSE, + label.cex = 0.5, + usearrows = FALSE, + vertex.cex = 0.5, + edge.col = "gray50", + vertex.col = mycol[factor(sp_attr$SWSN_MacroGroup)] + ) -# Plot proximity timeline -set.seed(235254) -proximity.timeline( - sanpedro, - default.dist = 10, - mode = "sammon", - labels.at = 17, - vertex.cex = 4, - render.edges = FALSE, - vertex.col = mycol[factor(sp_attr$SWSN_MacroGroup)], - chain.direction = "reverse", - xaxt = "n" -) + # Plot proximity timeline + set.seed(235254) + proximity.timeline( + sanpedro, + default.dist = 10, + mode = "sammon", + labels.at = 17, + vertex.cex = 4, + render.edges = FALSE, + vertex.col = mycol[factor(sp_attr$SWSN_MacroGroup)], + chain.direction = "reverse", + xaxt = "n" + ) +} else { + plot( + sp_nets[[1]], + vertex.col = mycol[factor(sp_attr$SWSN_MacroGroup)], + displaylabels = FALSE + ) +} ``` ### Figure 6.24: Animation {- #Figure_6_24} @@ -2331,7 +2408,7 @@ xy <- as.data.frame(coord1) colnames(xy) <- c("x", "y") # Create edge list with xy coordinates for each source and target -edgelist2 <- get.edgelist(r_net) +edgelist2 <- igraph::as_edgelist(r_net) edges2 <- data.frame(xy[edgelist2[, 1], ], xy[edgelist2[, 2], ]) colnames(edges2) <- c("X1", "Y1", "X2", "Y2") diff --git a/05-visualization.md b/05-visualization.md new file mode 100644 index 0000000..232eddd --- /dev/null +++ b/05-visualization.md @@ -0,0 +1,11408 @@ +# Network Visualization{#Visualization} + +![](images/image_break.png){width=100%} + +This section follows along with Brughmans and Peeples (2023) chapter 6 to illustrate the wide variety of techniques which can be used for network visualization. We begin with some general examples of network plotting and then demonstrate how to replicate all of the specific examples that appear in the book. For most of the examples below we rely on R but in a few cases we use other software and provide additional details and data formats. + +There are already some excellent resources online for learning how to create beautiful and informative network visuals. We recommend the excellent online materials produced by Dr. Katherine Ognyanova [available on her website](https://kateto.net/) and her [Static and dynamic network visualization with R](https://kateto.net/network-visualization) workshop materials in particular. Many of the examples here and in the book take inspiration from her work. In addition to this, the [R Graph Gallery](https://www.r-graph-gallery.com/) website created by [Holtz Yan](https://github.com/holtzy) provides numerous excellent examples of plots in R using the `ggplot2` and `ggraph` packages among many others. If you are new to R, it will probably be helpful for you to read a bit about basic graphic functions (including in the tutorials listed here) before getting started. + +## Data and R Setup{#VizDatasets} + +In order to make it as easy as possible for users to replicate specific visuals from the book and the other examples in this tutorial we have tried to make the examples as modular as possible. This means that we provide calls to initialize the required libraries for each plot within each relevant chunk of code (so that you can more easily tell what package does what) and we also provide links to download the data required to replicate each figure in the description of that figure below. The data sets we use here include both .csv and other format files as well as .Rdata files that contain sets of specific R objects formatted as required for individual chunks of code. + +If you plan on working through this entire tutorial and would like to download all of the associated data at once [you can download this zip file](All_data.zip). Simply extract this zip folder into your R working directory and the examples below will then work. Note that all of the examples below are setup such that the data should be contained in a sub-folder of your working directory called "data" (note that directories and file names are case sensitive). + +## Visualizing Networks in R{#ViZInR} + +
+

There are many tools available for creating network visualizations in +R including functions built directly into the igraph and +statnet packages. Before we get into the details, we first +briefly illustrate the primary network plotting options for +igraph, statnet and a visualization package +called ggraph. We start here by initializing our required +libraries and reading in an adjacency matrix and creating network +objects in both the igraph and statnet format. +These will be the basis for all examples in this section.

+
+ +Let's start by reading in our example data and then we describe each package in turn: + + +```r +library(igraph) +library(statnet) +``` + +``` +## Installed ReposVer Built +## ergm "4.6.0" "4.12.0" "4.2.3" +## ergm.count "4.1.1" "4.1.3" "4.2.3" +## ndtv "0.13.3" "0.13.4" "4.2.3" +## network "1.18.2" "1.20.0" "4.2.3" +## networkDynamic "0.11.4" "0.12.0" "4.2.3" +## sna "2.7-2" "2.8" "4.2.3" +## statnet.common "4.9.0" "4.13.0" "4.2.3" +## tergm "4.2.0" "4.2.2" "4.2.3" +## tsna "0.3.5" "0.3.6" "4.2.3" +``` + +```r +library(ggraph) +library(intergraph) + + +cibola <- + read.csv(file = "data/Cibola_adj.csv", + header = TRUE, + row.names = 1) + +cibola_attr <- read.csv(file = "data/Cibola_attr.csv", header = TRUE) + +# Create network in igraph format +cibola_i <- igraph::graph_from_adjacency_matrix(as.matrix(cibola), + mode = "undirected") +cibola_i +``` + +``` +## IGRAPH 0c20044 UN-- 31 167 -- +## + attr: name (v/c) +## + edges from 0c20044 (vertex names): +## [1] Apache.Creek--Casa.Malpais Apache.Creek--Coyote.Creek +## [3] Apache.Creek--Hooper.Ranch Apache.Creek--Horse.Camp.Mill +## [5] Apache.Creek--Hubble.Corner Apache.Creek--Mineral.Creek.Pueblo +## [7] Apache.Creek--Rudd.Creek.Ruin Apache.Creek--Techado.Springs +## [9] Apache.Creek--Tri.R.Pueblo Apache.Creek--UG481 +## [11] Apache.Creek--UG494 Atsinna --Cienega +## [13] Atsinna --Los.Gigantes Atsinna --Mirabal +## [15] Atsinna --Ojo.Bonito Atsinna --Pueblo.de.los.Muertos +## + ... omitted several edges +``` + +```r +# Create network object in statnet/network format +cibola_n <- asNetwork(cibola_i) +cibola_n +``` + +``` +## Network attributes: +## vertices = 31 +## directed = FALSE +## hyper = FALSE +## loops = FALSE +## multiple = FALSE +## bipartite = FALSE +## total edges= 167 +## missing edges= 0 +## non-missing edges= 167 +## +## Vertex attribute names: +## vertex.names +## +## No edge attributes +``` + +### `network` package{#networkpackage} + +All you need to do to plot a `network/statnet` network object is to simply type `plot(nameofnetwork)`. By default, this creates a network plot where all nodes and edges are shown the same color and weight using the Fruchterman-Reingold graph layout by default. There are, however, many options that can be altered for this basic plot. In order to see the details you can type `?plot.network` at the console for the associated document. + + +```r +set.seed(6332) +plot(cibola_n) +``` + + + +In order to change the color of nodes, the layout, symbols, or any other features, you can add arguments as detailed in the help document. These arguments can include calls to other functions, mathematical expressions, or even additional data in other attribute files. For example in the following plot, we calculate degree centrality directly within the plot call and then divide the result by 10 to ensure that the nodes are a reasonable size in the plot. We use the `vertex.cex` argument to set node size based on the results of that expression. Further we change the layout using the "mode" argument to produce a network graph using the Kamada-Kawai layout. We change the color of the nodes so that they represent the `Region` variable in the associated attribute file using the `vertex.col` argument and and set change all edge colors using the `edge.col` argument. Finally, we use `displayisolates = FALSE` to indicate that we do not want the single isolated node to be plotted. These are but a few of the many options. + + +```r +set.seed(436) +plot( + cibola_n, + vertex.cex = sna::degree(cibola_n) / 10, + mode = "kamadakawai", + vertex.col = as.factor(cibola_attr$Region), + edge.col = "darkgray", + displayisolates = FALSE +) +``` + + + +### `igraph` package{#igraphpackage} + +The `igraph` package also has a built in plotting function called `plot.igraph`. To call this you again just need to type `plot(yournetworkhere)` and provide an igraph object (R can tell what kind of object you have if you simply type plot). The default igraph plot again uses a Fruchterman-Reingold layout just like `statnet/network` but by default each node is labeled. + + +```r +set.seed(435) +plot(cibola_i) +``` + + + +Let"s take a look at a few of the options we can alter to change this plot. There are again many options to explore here and the help documents for igraph.plotting describe them in detail (type ?igraph.plotting at the console for more). If you want to explore `igraph` further, we suggest you check the [Network Visualization](https://kateto.net/network-visualization) tutorial linked above which provides a discussion of the wide variety of options. + + +```r +set.seed(3463) +plot( + cibola_i, + vertex.size = igraph::eigen_centrality(cibola_i)$vector * 20, + layout = layout_with_kk, + vertex.color = as.factor(cibola_attr$Great.Kiva), + edge.color = "darkblue", + vertex.frame.color = "red", + vertex.label = NA +) +``` + + + +### `ggraph` package{#ggraphpackage} + +The `ggraph` package provides a powerful set of tools for plotting and visualizing network data in R. The format used for this package is a bit different from what we saw above and instead relies on the `ggplot2` style of plots where a plot type is called and modifications are made with sets of lines with additional arguments separated by `+`. Although this takes a bit of getting used to we have found that the ggplot format is often more intuitive for making complex graphics once you understand the basics. + +Essentially, the way the `ggraph` call works is you start with a `ggraph` function call which includes the network object and the layout information. You then provide lines specifying the edges `geom_edge_link` and nodes `geom_node_point` features and so on. Conveniently the `ggraph` function call will take either an `igraph` or a `network` object so you do not need to convert. + +Here is an example. Here we first the call for the igraph network object `Cibola_i` and specify the Fruchterman-Reingold layout using `layout = "fr"`. Next, we call the `geom_edge_link` and specify edge colors. The `geom_node_point` call then specifies many attributes of the nodes including the fill color, outline color, transparency (alpha), shape, and size using the `igraph::degree` function. The `scale_size` call then tells the plot to scale the node size specified in the previous line to range between 1 and 4. Finally `theme_graph` is a basic call to the `ggraph` theme that tells the plot to make the background white and to remove the margins around the edge of the plot. Let's see how this looks. + +In the next section we go over the most common options in `ggraph` in detail. + + +```r +set.seed(4368) +# Specify network to use and layout +ggraph(cibola_i, layout = "fr") + + # Specify edge features + geom_edge_link(color = "darkgray") + + # Specify node features + geom_node_point( + fill = "blue", + color = "red", + alpha = 0.5, + shape = 22, + aes(size = igraph::degree(cibola_i)), + show.legend = FALSE + ) + + # Set the upper and lower limit of the "size" variable + scale_size(range = c(1, 10)) + + # Set the theme "theme_graph" is the default theme for networks + theme_graph() +``` + +``` +## Warning: Using the `size` aesthetic in this geom was deprecated in ggplot2 3.4.0. +## ℹ Please use `linewidth` in the `default_aes` field and elsewhere instead. +## This warning is displayed once every 8 hours. +## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was +## generated. +``` + + + +There are many options for the `ggraph` package and we recommend exploring the help document (`?ggraph`) as well as the [Data Imaginist](https://www.data-imaginist.com/tags/visualization) `ggraph` tutorial online for more. Most of the examples below will use the `ggraph` format. + +## Network Visualization Options{#NetVizOptions} + +In this section we illustrate some of the most useful graphical options for visualizing networks, focusing in particular on the `ggraph` format. In most cases there are similar options available in the plotting functions for both `network` and `igraph`. Where relevant we reference specific figures from the book and this tutorial and the code for all of the figures produced in R is presented in the next session. For all of the examples in this section we will use the [Cibola technological similarity data (click here to download)](data/Peeples2018.Rdata). First we call the required packages and import the data. + + +```r +library(igraph) +library(statnet) +library(intergraph) +library(ggraph) + +load("data/Peeples2018.Rdata") + +# Create igraph object for plots below +net <- asIgraph(brnet) +``` + +### Graph Layout{#GraphLayouts} + +Graph layout simply refers to the placement and organization in 2-dimensional or 3-dimensional space of nodes and edges in a network. + +#### Manual or User Defined Layouts{#ManualLayouts} + +There are a few options for manually defining node placement and graph layout in R and the easiest is to simply provide x and y coordinates directly. In this example, we plot the Cibola technological similarity network with a set of x and y coordinates that group sites in the same region in a grid configuration. For another example of this approach see [Figure 6.1 below](#Figure_6_1). For an example of how you can interactively define a layout see [Figure 6.5](#Figure_6_5) + + +```r +# site_info - site location and attribute data + +# Create xy coordinates grouped by region +xy <- + matrix( + c(1, 1, 3, 3, 2, 1, 2, 1.2, 3, 3.2, 2, 1.4, 1, 1.2, 2, 2.2, 3, + 2, 3, 1, 2.2, 1, 2, 3, 2, 3.2, 3, 1.2, 3, 3.4, 1, 2, 3.2, 3.2, + 3, 1.4, 3, 2.2, 2, 2, 3.2, 3.4, 2.2, 1.2, 3.4, 3.2, 3.2, 1, 2, + 3.4, 3.4, 3.4, 2.2, 3, 2.2, 3.2, 2.2, 3.4, 1, 1.4, 3, 2.4), + nrow = 31, + ncol = 2, + byrow = TRUE +) + +# Plot using "manual" layout and specify xy coordinates +ggraph(net, + layout = "manual", + x = xy[, 1], + y = xy[, 2]) + + geom_edge_link(edge_color = "gray") + + geom_node_point(aes(size = 4, col = site_info$Region), + show.legend = FALSE) + + theme_graph() +``` + + + +#### Geographic Layouts{#GeographicLayouts} + +Plotting networks using a a geographic layout is essentially the same as plotting with a manual layout except that you specify geographic coordinates instead of other coordinates. See [Figure 6.2](#Figure_6_2) for another example. + + +```r +ggraph(net, + layout = "manual", + x = site_info$x, + y = site_info$y) + + geom_edge_link(edge_color = "gray") + + geom_node_point(aes(size = 4, col = site_info$Region), + show.legend = FALSE) + + theme_graph() +``` + + + +When working with geographic data, it is also sometimes useful to plot directly on top of some sort of base map. There are many options for this but one of the most convenient is to use the `sf` and `ggmap` packages to directly download the relevant base map layer and plot directly on top of it. This first requires converting points to latitude and longitude in decimal degrees if they are not already in that format. See the details on the [sf package](https://r-spatial.github.io/sf/) and [ggmap package](https://github.com/dkahle/ggmap) for more details. + +Here we demonstrate the use of the `ggmap` and the `get_stadiamap` function which requires a bit of additional explanation. This function automatically retrieves a background map for you using a few arguments: + +* **`bbox`** - the bounding box which represents the decimal degrees longitude and latitude coordinates of the lower left and upper right area you wish to map. +* **`maptype`** - a name that indicates the style of map to use ([check here for options](https://rdrr.io/github/dkahle/ggmap/man/get_stadiamap.html)). +* **`zoom`** - a variable denoting the detail or zoom level to be retrieved. Higher number give more detail but take longer to detail. + +As of early 2024 the `get_stadiamap` function also requires that you sign up for an account at [stadiamaps.com](https://stadiamaps.com). This account is free and allows you to download a large number of background maps in R per month (likely FAR more than an individual would ever use). There are a few setup steps required to get this to work. You can follow the steps below or [click here for a YouTube video outlining steps 1 thorugh 3 below](https://www.youtube-nocookie.com/embed/6jUSyI6x3xg). + +1) First, you need to sign up for a free account at Stadiamaps. + +2) Once you sign in, you will be asked to create a Property Name, designating where you will be using data. You can simply call it "R analysis" or anything you'd like. + +3) Once you create this property you'll be able to assign an API key to it by clicking the "Add API" button. + +4) Now you simply need to let R know your API to allow map download access. In order to do this copy the API key that is visible on the stadiamaps page from the property you created and then run the following line of code adding your actual API key in the place of [YOUR KEY HERE] + + +```r +library(ggmap) +activate(key="[YOUR KEY HERE]") +``` + +Note, for the ease of demonstration, in the remainder of this online guide (other than the code chunk below) we pre-download the maps and provide them as a file instead of using the `get_stadiamap` function. + +
+

We describe the specifics of spatial data handling, geographic +coordinates, and projection in the section on Spatial Networks. See that section for a +full description and how R deals with geographic information.

+
+ + + + + + +```r +library(sf) +library(ggmap) +library(ggplot2) + +# Convert attribute location data to sf coordinates and change +# map projection +locations_sf <- + st_as_sf(site_info, coords = c("x", "y"), crs = 26912) +loc_trans <- st_transform(locations_sf, crs = 4326) +coord1 <- do.call(rbind, st_geometry(loc_trans)) %>% + tibble::as_tibble() %>% + setNames(c("lon", "lat")) + +xy <- as.data.frame(coord1) +colnames(xy) <- c("x", "y") + +# Get a basemap when a Stadia API key is available; otherwise fall back +# to a simple coordinate plot so the document still renders. +if (nzchar(Sys.getenv("STADIAMAPS_API_KEY"))) { + base_cibola <- get_stadiamap( + bbox = c(-110.2, 33.4, -107.8, 35.3), + zoom = 10, + maptype = "stamen_terrain_background", + color = "bw" + ) + base_plot <- ggmap(base_cibola, darken = 0.35) +} else { + base_plot <- ggplot() + + coord_quickmap(xlim = c(-110.2, -107.8), ylim = c(33.4, 35.3)) + + theme_void() + + theme( + panel.background = element_rect(fill = "grey20", color = NA), + plot.background = element_rect(fill = "grey20", color = NA) + ) +} + +# Extract edge list from network object +edgelist <- igraph::as_edgelist(net) + +# Create data frame of beginning and ending points of edges +edges <- data.frame(xy[edgelist[, 1], ], xy[edgelist[, 2], ]) +colnames(edges) <- c("X1", "Y1", "X2", "Y2") + +# Plot original data on map +base_plot + + geom_segment( + data = edges, + aes( + x = X1, + y = Y1, + xend = X2, + yend = Y2 + ), + col = "white", + alpha = 0.8, + size = 1 + ) + + geom_point( + data = xy, + aes(x, y, col = site_info$Region), + alpha = 0.8, + size = 5, + show.legend = FALSE + ) + + theme_void() +``` + + + +#### Shape-Based and Algorithmic Layouts{#AlgorithmicLayouts} + +There are a wide variety of shape-based and algorithmic layouts available for use in R. In most cases, all it takes to change layouts is to simply modify a single line the `ggraph` call to specify our desired layout. The `ggraph` package can use any of the `igraph` layouts as well as many that are built directly into the package. See `?ggraph` for more details and to see the options. Here we show a few examples. Note that we leave the figures calls the same except for the argument `layout = "yourlayout"` in each `ggraph` call and the `ggtitle` name. For the layouts that involve randomization, we use the `set.seed()` function to make sure they will always plot the same. See the discussion of [Figure 6.8](#Figure_6_8) below for more details. Beyond this [Figure 6.9](#Figure_6_9) provides additional options that can be used for hierarchical network data. + +
+

If you do not specify a graph layout in ggraph, the +plotting function will automatically choose a layout using the +layout_nicely() function. Although this sometimes produces +useful the layout used is not specified in the call so we recommend +supplying a layout argument directly.

+
+ + +```r +# circular layout +circ_net <- ggraph(net, layout = "circle") + + geom_edge_link(edge_color = "gray") + + geom_node_point(aes(size = 4, col = site_info$Region), + show.legend = FALSE) + + ggtitle("Circle") + + theme_graph() + + theme(plot.title = element_text(size = rel(1))) + +# Fruchcterman-Reingold layout +set.seed(4366) +fr_net <- ggraph(net, layout = "fr") + + geom_edge_link(edge_color = "gray") + + geom_node_point(aes(size = 4, col = site_info$Region), + show.legend = FALSE) + + ggtitle("Fruchterman-Reingold") + + theme_graph() + + theme(plot.title = element_text(size = rel(1))) + +# Davidsons and Harels annealing algorithm layout +set.seed(3467) +dh_net <- ggraph(net, layout = "dh") + + geom_edge_link(edge_color = "gray") + + geom_node_point(aes(size = 4, col = site_info$Region), + show.legend = FALSE) + + ggtitle("Davidson-Harel") + + theme_graph() + + theme(plot.title = element_text(size = rel(1))) + +library(ggpubr) +ggarrange(circ_net, fr_net, dh_net, nrow = 1) +``` + + + +
+

In the code above we used the ggarrange function within +the ggpubr package to combine the figures into a single +output. This function works with any ggplot2 or +ggraph format output when you supply the names of each +figure in the order you want them to appear and the number of rows +nrow and number of columns ncol you want the +resulting combined figure to have. If you want to label each figure +using the ggarrange function you can use the +labels argument.

+
+ + +### Node and Edge Options{#NodeEdgeOptions} + +There are many options for altering color and symbol for nodes and edges within R. In this section we very briefly discuss some of the most common options. For more details see the discussion of [figures 6.10 through 6.16](#Figure_6_10) below. + +#### Nodes {#NodeOptions} + +In `ggraph` changing node options mostly consists of changing options within the `geom_node_point` call within the `ggraph` figure call. As we have already seen it is possible to set color for all nodes or by some variable, to change the size of points, and we can also scale points by some metric like centrality. Indeed, it is even possible to make the call to the centrality function in question directly within the figure code. + +When selecting point shapes you can use any of the shapes available in base R using `pch` point codes. Here are all of the available options: + + +```r +library(ggpubr) +ggpubr::show_point_shapes() +``` + + + +There are many options for selecting colors for nodes and edges. These can be assigned using standard color names or can be assigned using rgb or hex codes. It is also possible to use standard palettes in packages like `RColorBrewer` or `scales` to specify categorical or continuous color schemes. This is often done using either the `scale_fill_brewer` or `scale_color_brewer` calls from `RColorBrewer`. Here are a couple of examples. In these examples, colors are grouped by site region, node size is scaled to degree centrality, and node and edge color and shape are specified in each call. Note the `alpha` command which controls the transparency of the relevant part of the plot. The scale_size call specifies the maximum and minimum size of points in the plot. + +The [R Graph Gallery](https://www.r-graph-gallery.com/38-rcolorbrewers-palettes.html) has a good overview of the available color palettes in `RColorBrewer` and when the can be used. The "Set2" palette used here is a good one for people with many kinds of color vision deficiencies. + + +```r +library(RColorBrewer) + +set.seed(347) +g1 <- ggraph(net, layout = "kk") + + geom_edge_link(edge_color = "gray", alpha = 0.7) + + geom_node_point( + aes(fill = site_info$Region), + shape = 21, + size = igraph::degree(net) / 2, + alpha = 0.5 + ) + + scale_fill_brewer(palette = "Set2") + + theme_graph() + + theme(legend.position = "none") + +set.seed(347) +g2 <- ggraph(net, layout = "kk") + + geom_edge_link(edge_color = "blue", alpha = 0.3) + + geom_node_point( + aes(col = site_info$Region), + shape = 15, + size = igraph::degree(net) / 2, + alpha = 1 + ) + + scale_color_brewer(palette = "Set1") + + theme_graph() + + theme(legend.position = "none") + +ggarrange(g1, g2, nrow = 1) +``` + + + +There are also a number of more advanced methods for displaying nodes including displaying figures or other data visualizations in the place of nodes or using images for nodes. There are examples of each of these in the book and code outlining how to create such visuals in the discussions of [Figure 6.3](#Figure_6_3) and [Figure 6.13](#Figure_6_13) below. + +#### Edges{#EdgeOptions} + +Edges can be modified in terms of color, line type, thickness and many other features just like nodes and this is typically done using the `geom_edge_link` call within `ggraph`. Let"s take a look at a couple of additional examples. In this case we"re going to use a weighted network object in the original [Peeples2018.Rdata](data/Peeples2018.Rdata) file to show how we can vary edges in relation to edge attributes like weight. + +In the example here we plot both the line thickness and transparency using the edge weights associated with the network object. We also are using the `scale_edge_color_gradient2` to specify a continuous edge color scheme with three anchors. For more details see `?scale_edge_color` + + +```r +library(intergraph) +net2 <- asIgraph(brnet_w) + +set.seed(436) +ggraph(net2, "stress") + + geom_edge_link(aes(width = weight, alpha = weight, col = weight)) + + scale_edge_color_gradient2( + low = "#440154FF", + mid = "#238A8DFF", + high = "#FDE725FF", + midpoint = 0.8 + ) + + scale_edge_width(range = c(1, 5)) + + geom_node_point(size = 4, col = "blue") + + labs(edge_color = "Edge Weight Color Scale") + + theme_graph() +``` + + + +Another feature of edges that is often important in visualizations is the presence or absence and type of arrows. Arrows can be modified in `ggraph` using the `arrow` argument within a `geom_edge_link` call. The most relevant options are the length of the arrow (which determines size), the `type` argument which specifies an open or closed arrow, and the spacing of the arrow which can be set by the `end_cap` and `start_cap` respectively which define the gap between the arrow point and the node. These values can all be set using absolute measurements as shown in the example below. Since this is an undirected network we use the argument `ends = "first"` to simulated a directed network so that arrowheads will only be drawn the first time an edge appears in the edge list. See `?arrow` for more details on options. + + +```r +set.seed(436) +ggraph(net, "stress") + + geom_edge_link( + arrow = arrow( + length = unit(2, "mm"), + ends = "first", + type = "closed" + ), + end_cap = circle(0, "mm"), + start_cap = circle(3, "mm"), + edge_colour = "black" + ) + + geom_node_point(size = 4, col = "blue") + + theme_graph() +``` + + + +Another common consideration with edges is the shape of the edges themselves. So far we have used examples where the edges are all straight lines, but it is also possible to draw them as arcs or so that they fan out from nodes so that multiple connections are visible. In general, all you need to do to change this option is to use another command in the `geom_edge_` family of commands. For example, in the following chunk of code we produce a network with arcs rather than straight lines. In this case the argument `strength` controls the amount of bend in the lines. + + +```r +set.seed(436) +ggraph(net, "kk") + + geom_edge_arc(edge_colour = "black", strength = 0.1) + + geom_node_point(size = 4, col = "blue") + + theme_graph() +``` + + + +It is also possible to not show edges at all but instead just a gradient scale representing the density of edges using the `geom_edge_density` call. This could be useful in very large and complex networks. + + +```r +set.seed(436) +ggraph(net2, "kk") + + geom_edge_density() + + geom_node_point(size = 4, col = "blue") + + theme_graph() +``` + + + +
+

If you want to see all of the possible options for +geom_edge_ commands, simply use the help command on any one +of the functions (i.e., ?geom_edge_arc) and scroll down in +the help window to the section labeled “See Also.”

+
+ +### Labels {#LabelOptions} + +In many cases you may want to label either the nodes, edges, or other features of a network. This is relatively easy to do in `ggraph` with the `geom_node_text()` command. This will place labels as specified on each node. If you use the `repel = TRUE` argument it will repel the names slightly from the node to make them more readable. As shown in the example for [Figure 6.4](#Figure_6_4) it is also possible to filter labels to label only certain nodes. + + +```r +set.seed(436) +ggraph(net2, "fr") + + geom_edge_link() + + geom_node_point(size = 4, col = "blue") + + geom_node_text(aes(label = vertex.names), size = 3, repel = TRUE) + + theme_graph() +``` + + + +It is also possible to label edges by adding an argument directly into the `geom_edge_` command. In practice, this really only works with very small networks. In the next chunk of code, we create a small network and demonstrate this function. + + +```r +g <- graph(c("A", "B", + "B", "C", + "A", "C", + "A", "A", + "C", "B", + "D", "C")) + +E(g)$weight <- c(3, 1, 6, 8, 4, 2) + +set.seed(4351) +ggraph(g, layout = "stress") + + geom_edge_fan(aes(label = weight), + angle_calc = "along", + label_dodge = unit(2, "mm")) + + geom_node_point(size = 20, col = "lightblue") + + geom_node_text(label = V(g)$name) + + theme_graph() +``` + + + +### Be Kind to the Color Blind{#Colorblind} + +When selecting your color schemes, it is important to consider the impact of a particular color scheme on color blind readers. There is an excellent set of R scripts on GitHub in a package called [colorblindr](https://github.com/clauswilke/colorblindr) by Claus Wilke which can help you do just that. I have slightly modified the code from the `colorblindr` package and created a script called [colorblindr.R](data/colorblindr.R) which you can download and use to test out your network. Simply run the code in the script and then use the `cvd_grid2()` function on a `ggplot` or `ggraph` object to see simulated colors. + +The chunk of code below loads the `colorblindr.R` script and then plots a figure using `RColorBrewer` color `Set2` in its original unmodified format and then as it might look to readers with some of the most common forms of color vision issues. Download the [colorblindr.R script](scripts/colorblindr.R) to follow along. + + +```r +library(colorspace) +source("scripts/colorblindr.R") +cvd_grid2(g1) +``` + + + +### Communities and Groups{#VizCommunities} + +Showing communities or other groups in network visualizations can be as simple as color coding nodes or edges as we have seen in many examples here. It is sometimes also useful to highlight groups by creating a convex hull or circle around the relevant points. This can be done in `ggraph` using the `geom_mark_hull` command within the `ggforce` package. You will also need a package called `concaveman` that allows you to set the concavity of the hulls around points. + +The following chunk of code provides a simple example using the Louvain clustering algorithm. + + +```r +library(ggforce) +library(concaveman) + +# Define clusters +grp <- as.factor(cluster_louvain(net2)$membership) + +set.seed(4343) +ggraph(net2, layout = "fr") + + geom_edge_link0(width = 0.2) + + geom_node_point(aes(fill = grp), + shape = 21, + size = 5, + alpha = 0.75) + + # Create hull around points within group and label + geom_mark_hull( + aes( + x, + y, + group = grp, + fill = grp, + ), + concavity = 4, + expand = ggplot2::unit(2, "mm"), + alpha = 0.25, + ) + + scale_fill_brewer(palette = "Set2") + + theme_graph() +``` + + + +The discussion of [Figure 6.4](#Figure_6_4) below provides another similar example. There are many more complicated ways of showing network groups provided by the examples covering figures from the book. For example, [Figure 6.18](#Figure_6_18) provides an example of the "group-in-a-box" technique using the NodeXL software package. [Figure 6.19](#Figure_6_19) illustrates the use of matrices as visualization tools and [Figure 6.20](#Figure_6_20) provides links to the Nodetrix hybrid visualization software. + +## Replicating the Book Figures{#ReplicatingBookFigures} + +In this section we go through each figure in Chapter 6 of Brughmans and Peeples (2023) and detail how the final graph was created for all figures that were created using R. For those figures not created in R we describe what software and data were used and provide additional resources where available. We hope these examples will serve as inspiration for your own network visualization experiments. Some of these figures are relatively simple while others are quite complex. They are presented in the order they appear in the book. + +### Figure 6.1: Manual Layout {- #Figure_6_1} + +Figure 6.1. An example of an early hand drawn network graph (sociogram) published by Moreno (1932: 101). Moreno noted that the nodes at the top and bottom of the sociogram have the most connections and therefore represent the nodes of greatest importance. These specific “important” points are emphasized through both their size and their placement. + +Note that the hand drawn version of this figure is presented in the book and this digital example is presented only for illustrative purposes. This shows how you can employ user defined layouts by directly supplying coordinates for the nodes in the plot. [Download the Moreno data to follow along]("data/Moreno.csv"). + + +```r +library(igraph) +library(ggraph) + +# Read in adjacency matrix of Moreno data and covert to network +moreno <- + as.matrix(read.csv("data/Moreno.csv", header = TRUE, row.names = 1)) +g_moreno <- graph_from_adjacency_matrix(moreno) + +# Create xy coordinates associated with each node +xy <- matrix( + c(4, 7, 1, 5, 6, 5, 2, 4, 3, 4, 5, 4, 1, 2.5, 6, 2.5, 4, 1), + nrow = 9, + ncol = 2, + byrow = TRUE +) + +# Plot the network using layout = "manual" to place nodes using xy coordinates +ggraph(g_moreno, + layout = "manual", + x = xy[, 1], + y = xy[, 2]) + + geom_edge_link() + + geom_node_point(fill = "white", + shape = 21, + size = igraph::degree(g_moreno)) + + scale_size(range = c(2, 3)) + + theme_graph() +``` + + + +### Figure 6.2: Examples of Common Network Plot Formats {- #Figure_6_2} + +Figure. 6.2. These plots are all different visual representations of the same network data from Peeples’s (2018) data where edges are defined based on the technological similarities of cooking pots from each node which represent archaeological settlements. + +The code below creates each of the individual figures and then compiles them into a single composite figure for plotting. + +First read in the data ([all data are combined in a single RData file here](data/Peeples2018.Rdata)). + + +```r +library(igraph) +library(statnet) +library(intergraph) +library(ggplotify) +library(ggraph) +library(ggpubr) + +load(file = "data/Peeples2018.Rdata") +## contains objects +# site_info - site locations and attributes +# ceramic_br - raw Brainerd-Robinson similarity among sites +# brnet - binary network with similarity values > 0.65 +# defined as edges in statnet/network format +# brnet_w - weighted network with edges (>0.65) given weight +# values based on BR similarity in statnet/network format +## +``` + +Fig 6.2a - A simple network graph with nodes placed based on the Fruchterman-Reingold algorithm + + +```r +## create simple graph with Fruchterman - Reingold layout +set.seed(423) +f6_2a <- ggraph(brnet, "fr") + + geom_edge_link(edge_colour = "grey66") + + geom_node_point(aes(size = 5), col = "red", show.legend = FALSE) + + theme_graph() +f6_2a +``` + + + +Fig 6.2b - Network graph nodes with placed based on the real geographic locations of settlements and are color coded based on sub-regions. + + +```r +## create graph with layout determined by site location and +## nodes color coded by region +f6_2b <- ggraph(brnet, "manual", + x = site_info$x, + y = site_info$y) + + geom_edge_link(edge_colour = "grey66") + + geom_node_point(aes(size = 2, col = site_info$Region), + show.legend = FALSE) + + theme_graph() +f6_2b +``` + + + +Fig 6.2c - A graph designed to show how many different kinds of information can be combined in a single network plot. In this network graph node placement is defined by the stress majorization algorithm (see below), with nodes color coded based on region, with different symbols for different kinds of public architectural features found at those sites, and with nodes scaled based on betweenness centrality scores. The line weight of each edge is used to indicate relative tie-strength. + + +```r +# create vectors of attributes and betweenness centrality and plot +# network with nodes color coded by region, sized by betweenness, +# with symbols representing public architectural features, and +# with edges weighted by BR similarity +col1 <- as.factor((site_info$Great.Kiva)) +col2 <- as.factor((site_info$Region)) +bw <- sna::betweenness(brnet_w) + +f6_2c <- ggraph(brnet_w, "stress") + + geom_edge_link(aes(width = weight, alpha = weight), + edge_colour = "black", + show.legend = FALSE) + + scale_edge_width(range = c(1, 2)) + + geom_node_point(aes( + size = bw, + shape = col1, + fill = col1, + col = site_info$Region + ), + show.legend = FALSE) + + scale_fill_discrete() + + scale_size(range = c(4, 12)) + + theme_graph() +f6_2c +``` + + + +Fig. 6.2d - This network graph is laid out using the Kamada-Kawai force directed algorithm with nodes color coded based on communities detected using the Louvain community detection algorithm. Each community is also indicated by a circle highlighting the relevant nodes. Edges within communities are shown in black and edges between communities are shown in red. + +In this plot we use the `as.ggplot` function to convert a traditional `igraph` plot to a `ggraph` plot to illustrate how this can be done. + + +```r +# convert network object to igraph object and calculate Louvain +# cluster membership plot and convert to grob to combine in ggplot +g <- asIgraph(brnet_w) +clst <- cluster_louvain(g) + +f6_2d <- as.ggplot( + ~ plot( + clst, + g, + layout = layout_with_kk, + vertex.label = NA, + vertex.size = 10, + col = rainbow(4)[clst$membership] + ) +) +f6_2d +``` + + + +Finally, we use the `ggarrange` function from the `ggpubr` package to combine all of these plots into a single composite plot. + + +```r +# Combine all plots into a single figure using ggarrange +figure_6_2 <- ggarrange( + f6_2a, + f6_2b, + f6_2c, + f6_2d, + nrow = 2, + ncol = 2, + labels = c("(a)", "(b)", "(c)", "(d)"), + font.label = list(size = 22) +) + +figure_6_2 +``` + + + +### Figure 6.3: Examples of Rare Network Plot Formats {- #Figure_6_3} + +Figure 6.3. Examples of less common network visuals techniques for Peeples’s (2018) ceramic technological similarity data. + +Fig 6.3a - A weighted heat plot of the underlying similarity matrix with hierarchical clusters shown on each axis. This plot relies on a packages called `superheat` that produces plots formatted as we see here. The required input is a symmetric similarity matrix object. + +
+

In the chunk of code below we use the as.ggplot function +from the ggplotify package. This function converts a non +ggplot2 style function into a ggplot2 format +so that it can be further used with packages like ggpubr +and colorblindr.

+
+ + + +```r +library(igraph) +library(statnet) +library(intergraph) +library(ggraph) +library(ggplotify) +library(superheat) + +ceramic_br_a <- ceramic_br +diag(ceramic_br_a) <- NA + +f6_3a <- as.ggplot( + ~ superheat( + ceramic_br_a, + row.dendrogram = TRUE, + col.dendrogram = TRUE, + grid.hline.col = "white", + grid.vline.col = "white", + legend = FALSE, + left.label.size = 0, + bottom.label.size = 0 + ) +) +f6_3a +``` + + + +Fig. 6.3b - An arcplot with within group ties shown above the plot and between group ties shown below. + +For this plot, we read in a adjacency matrix that is ordered in the order we want it to show up in the final plot. [Download the file here](data/Peeples_arcplot.csv) to follow along. Note that the object `grp` must be produced in the same order that the nodes appear in the original adjacency matrix file. + + + +```r +arc_dat <- read.csv("data/Peeples_arcplot.csv", + header = TRUE, + row.names = 1) +g <- graph_from_adjacency_matrix(as.matrix(t(arc_dat))) + +# set groups for color +grp <- as.factor(c(2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1)) + + +# Make the graph +f6_3b <- ggraph(g, layout = "linear") + + geom_edge_arc( + edge_colour = "black", + edge_alpha = 0.2, + edge_width = 0.7, + fold = FALSE, + strength = 1, + show.legend = FALSE + ) + + geom_node_point( + aes( + size = igraph::degree(g), + color = grp, + fill = grp + ), + alpha = 0.5, + show.legend = FALSE + ) + + scale_size_continuous(range = c(4, 8)) + + theme_graph() +f6_3b +``` + + + +Fig. 6.3c - Network plot with sites in geographic locations and edges bundled using the edge bundling hammer routine. + +
+

This function requires the edgebundle package be +installed along with reticulate and Python 3.8 (see Packages) and uses the Cibola technological similarity data. +Check Data and Workspace Setup section for +more details on getting the edge bundling package and Python up and +running.

+

Be aware that this function may take a long time on your computer +depending on your processing power and RAM.

+
+ + + + +```r +library(edgebundle) +load("data/Peeples2018.Rdata") + +# Create attribute file with required data +xy <- as.data.frame(site_info[, 1:2]) +xy <- cbind(xy, site_info$Region) +colnames(xy) <- c("x", "y", "Region") + +# Run hammer bundling routine +g <- asIgraph(brnet) +if (reticulate::py_module_available("datashader.bundling")) { + hbundle <- edge_bundle_hammer(g, xy, bw = 5, decay = 0.3) + + f6_3c <- ggplot() + + geom_path(data = hbundle, aes(x, y, group = group), + col = "gray66", size = 0.5) + + geom_point(data = xy, aes(x, y, col = Region), + size = 5, alpha = 0.75, show.legend = FALSE) + + theme_void() +} else { + edgelist <- igraph::as_edgelist(g) + edges <- data.frame(xy[edgelist[, 1], 1:2], xy[edgelist[, 2], 1:2]) + colnames(edges) <- c("x", "y", "xend", "yend") + + f6_3c <- ggplot() + + geom_segment(data = edges, aes(x = x, y = y, xend = xend, yend = yend), + col = "gray66", alpha = 0.35, size = 0.4) + + geom_point(data = xy, aes(x, y, col = Region), + size = 5, alpha = 0.75, show.legend = FALSE) + + theme_void() +} +f6_3c +``` + + + +Fig. 6.3d - Network graph where nodes are replaced by waffle plots that show relative frequencies of the most common ceramic technological clusters. + +This is a somewhat complicated plot that requires a couple of specialized libraries and additional steps along the way. We provide comments in the code below to help you follow along. Essentially the routine creates a series of waffle plots and then uses them as annotations to replace the nodes in the final `ggraph`. This plot requires that you install a development package called `ggwaffle`. Run the line of code below before creating the figure if you need to add this package. + + +```r +devtools::install_github("liamgilbey/ggwaffle") +``` + +
+

There are numerous projects that are in the R CRAN archive and those +packages have been peer reviewed and evaluated. There are many other +packages and compendiums designed for use in R that are not yet in the +CRAN archive. Frequently these are found as packages in development on +GitHub. In order to use these packages in development, you can use the +install_github function wrapped inside the +devtools package (though it originates in the +remotes package). In order to install a package from +GitHub, you type supply “username/packagename” inside the +install_github call.

+
+ +Let's now look at the figure code: + + +```r +# Initialize libraries + +library(tidyverse) + +# Create igraph object from data imported above +cibola_adj <- + read.csv(file = "data/Cibola_adj.csv", + header = TRUE, + row.names = 1) +g <- graph_from_adjacency_matrix(as.matrix(cibola_adj), + mode = "undirected") + +# Import raw ceramic data and convert to proportions +ceramic_clust <- read.csv(file = "data/Cibola_clust.csv", + header = TRUE, + row.names = 1) +ceramic_p <- prop.table(as.matrix(ceramic_clust), margin = 1) + +# Assign vertex attributes to the network object g which represent +# columns in the ceramic.p table +V(g)$c1 <- ceramic_p[, 1] +V(g)$c2 <- ceramic_p[, 2] +V(g)$c3 <- ceramic_p[, 3] +V(g)$c4 <- ceramic_p[, 4] +V(g)$c5 <- ceramic_p[, 5] +V(g)$c6 <- ceramic_p[, 6] +V(g)$c7 <- ceramic_p[, 7] +V(g)$c8 <- ceramic_p[, 8] +V(g)$c9 <- ceramic_p[, 9] +V(g)$c10 <- ceramic_p[, 10] + +# Precompute the layout and assign coordinates as x and y in network g +set.seed(345434534) +xy <- layout_with_fr(g) +V(g)$x <- xy[, 1] +V(g)$y <- xy[, 2] + +# Create a data frame that contains the 4 most common +# categories in the ceramic table, the node id, and the proportion +# of that ceramic category at that node +nodes_wide <- igraph::as_data_frame(g, "vertices") +nodes_long <- nodes_wide %>% + dplyr::select(c1:c4) %>% + mutate(id = seq_len(nrow(nodes_wide))) %>% + tidyr::pivot_longer(c1:c4, names_to = "attr", values_to = "value") +nodes_out <- NULL +for (j in seq_len(nrow(nodes_long))) { + temp <- do.call("rbind", replicate(round(nodes_long[j, ]$value * 50, 0), + nodes_long[j, ], simplify = FALSE)) + nodes_out <- rbind(nodes_out, temp) +} + +if (requireNamespace("ggwaffle", quietly = TRUE)) { + library(ggwaffle) + + # Create a list object for the call to each bar chart by node + bar_list <- lapply(1:vcount(g), function(i) { + gt_plot <- ggplotGrob( + ggplot(waffle_iron(nodes_out[nodes_out$id == i, ], + aes_d(group = attr))) + + geom_waffle(aes(x, y, fill = group), size = 10) + + coord_equal() + + labs(x = NULL, y = NULL) + + theme( + legend.position = "none", + panel.background = element_rect(fill = "white", colour = NA), + line = element_blank(), + text = element_blank() + ) + ) + panel_coords <- gt_plot$layout[gt_plot$layout$name == "panel", ] + gt_plot[panel_coords$t:panel_coords$b, panel_coords$l:panel_coords$r] + }) + + # Convert the results above into custom annotation + annot_list <- lapply(1:vcount(g), function(i) { + xmin <- nodes_wide$x[i] - .25 + xmax <- nodes_wide$x[i] + .25 + ymin <- nodes_wide$y[i] - .25 + ymax <- nodes_wide$y[i] + .25 + annotation_custom( + bar_list[[i]], + xmin = xmin, + xmax = xmax, + ymin = ymin, + ymax = ymax + ) + }) + + # create basic network + p <- ggraph(g, "manual", x = V(g)$x, y = V(g)$y) + + geom_edge_link0() + + theme_graph() + + coord_fixed() + + # put everything together by combining with the annotation (bar plots + network) + f6_3d <- Reduce("+", annot_list, p) +} else { + f6_3d <- ggraph(g, "manual", x = V(g)$x, y = V(g)$y) + + geom_edge_link0(alpha = 0.4) + + geom_node_point(aes(fill = c1), shape = 21, size = 8, color = "black") + + scale_fill_gradient(low = "white", high = "steelblue") + + theme_graph() + + coord_fixed() +} +f6_3d +``` + + + +
+

The inspiration for the example above came from a R +blogpost by schochastics (David Schoch). As that post shows, any +figures that can be treated as ggplot2 objects can be used +in the place of nodes by defining them as “annotations.” See the post +for more details.

+
+ +Now let's look at all of the figures together. + +![](images/Figure_6_3.jpg){width=100%} + +### Figure 6.4: Simple Network with Clusters {- #Figure_6_4} + +Figure 6.4. A network among Clovis era sites in the Western U.S. with connections based on shared lithic raw material sources. Nodes are scaled based on betweenness centrality with the top seven sites labelled. Color-coded clusters were defined using the Louvain algorithm. + +
+

This example shows how to define and indicate groups and label points +based on their values. Note the use of the ifelse call in +the geom_node_text portion of the plot. See here for more information on how +ifelse statements work.

+
+ + +```r +library(ggforce) +library(ggraph) +library(statnet) +library(igraph) + +clovis <- read.csv("data/Clovis.csv", header = TRUE, row.names = 1) +colnames(clovis) <- row.names(clovis) +graph <- graph_from_adjacency_matrix(as.matrix(clovis), + mode = "undirected", + diag = FALSE) + +bw <- igraph::betweenness(graph) + +grp <- as.factor(cluster_louvain(graph)$membership) + +set.seed(43643548) +ggraph(graph, layout = "fr") + + geom_edge_link(edge_width = 1, color = "gray") + + geom_node_point(aes(fill = grp, size = bw, color = grp), + shape = 21, + alpha = 0.75) + + scale_size(range = c(2, 20)) + + geom_mark_hull( + aes( + x, + y, + group = grp, + fill = grp, + color = NA + ), + concavity = 4, + expand = unit(2, "mm"), + alpha = 0.25, + label.fontsize = 12 + ) + + scale_color_brewer(palette = "Set2") + + scale_fill_brewer(palette = "Set2") + + scale_edge_color_manual(values = c(rgb(0, 0, 0, 0.3), + rgb(0, 0, 0, 1))) + + # If else statement only labels points that meet the condition + geom_node_text(aes(label = ifelse(bw > 40, + as.character(name), + NA_character_)), + size = 4) + + theme_graph() + + theme(legend.position = "none") +``` + + + +### Figure 6.5: Interactive Layout {- #Figure_6_5} + +Figure 6.5. An example of the same network graph with two simple user defined layouts created interactively. + +Figure 6.5 was produced in [NetDraw](https://sites.google.com/site/netdrawsoftware/download) by creating a simple network and taking screen shots of two configurations of nodes. There are a few options for creating a similar figures in R. The simplest is to use an igraph network object and the `tkplot` function. This function brings up a window that lets you drag and move nodes (with or without an initial algorithmic layout) and when you"re done you can assign the new positions to a variable to use for plotting. [Use these data](data/Peeples2018.Rdata) to follow along. + +
+

Note that if you are running this package in your browser via binder, +the function below will not work as you do not have permission to open +the tkplot on the virtual server. To follow along with the plotting of +this figure you can use the pre-determined locations by reading in this +file load(file="data/Coords.Rdata")

+
+ + + +```r +library(igraph) +library(intergraph) + +load("data/Peeples2018.Rdata") + +cibola_i <- asIgraph(brnet) + +locs <- tkplot(cibola_i) +coords <- tkplot.getcoords(locs) +``` + +This will bring up a window like the example below and when you click "Close" it will automatically create the variables with the node location information for plotting. + +![](images/interactive.jpg){width=60%} + + + +```r +plot(cibola_i, layout = coords) +``` + + + +### Figure 6.6: Absolute Geographic Layout {- #Figure_6_6} + +Fig. 6.6. Map of major Roman roads and major settlements on the Iberian Peninsula, (a) with roads mapped along their actual geographic paths and (b) roads shown as simple line segments between nodes. + +The figure that appears in the book was originally created using GIS software but it is possible to prepare a quite similar figure in R using the tools we outlined above. To reproduce the results presented here you will need to download [the node information file](data/Hispania_nodes.csv) and the [road edge list](data/Hispania_roads.csv). We have created a script called [map_net.R](scripts/map_net.R) which will produce similar maps when supplied with a network object and a file with node locations in lat/long coordinates. For more information on how R works with geographic data see the [spatial networks](#SpatialNetworks) section of this document. + + +```r +library(igraph) +library(ggmap) +library(sf) + +# Load my_map background map +load("data/Figure6_6.Rdata") + +edges1 <- read.csv("data/Hispania_roads.csv", header = TRUE) +edges1 <- edges1[which(edges1$Weight > 25), ] +nodes <- read.csv("data/Hispania_nodes.csv", header = TRUE) +nodes <- nodes[which(nodes$Id %in% c(edges1$Source, edges1$Target)), ] + +road_net <- + graph_from_edgelist(as.matrix(edges1[, 1:2]), directed = FALSE) + +# Convert attribute location data to sf coordinates +locations_sf <- + st_as_sf(nodes, coords = c("long", "lat"), crs = 4326) +coord1 <- do.call(rbind, st_geometry(locations_sf)) %>% + tibble::as_tibble() %>% + setNames(c("lon", "lat")) + +xy <- as.data.frame(coord1) +colnames(xy) <- c("x", "y") + +# Extract edge list from network object +edgelist <- igraph::as_edgelist(road_net) + +# Create data frame of beginning and ending points of edges +edges <- as.data.frame(matrix(NA, nrow(edgelist), 4)) +colnames(edges) <- c("X1", "Y1", "X2", "Y2") +for (i in seq_len(nrow(edgelist))) { + edges[i, ] <- c(nodes[which(nodes$Id == edgelist[i, 1]), 3], + nodes[which(nodes$Id == edgelist[i, 1]), 2], + nodes[which(nodes$Id == edgelist[i, 2]), 3], + nodes[which(nodes$Id == edgelist[i, 2]), 2]) +} + + +ggmap(my_map) + + geom_segment( + data = edges, + aes( + x = X1, + y = Y1, + xend = X2, + yend = Y2 + ), + col = "black", + size = 1 + ) + + geom_point( + data = xy, + aes(x, y), + alpha = 0.8, + col = "black", + fill = "white", + shape = 21, + size = 1.5, + show.legend = FALSE + ) + + theme_void() +``` + + + +### Figure 6.7: Distorted Geographic Layout {- #Figure_6_7} + +Figure 6.7. This ceramic similarity network of the San Pedro River Valley in Arizona shows the challenges of creating geographic network layouts. (a) Shows sites in their original locations whereas (b) shifts locations to improve the visibility of network structure. Note how the distorted geographic layout retains the basic relationships among the nodes while altering their locations slightly. + +Unfortunately as the first map contains real site locations we cannot share those data here. The second map can still be reproduced given nothing but the code below. The only difference required to produce Figure 6.7a would be to replace the `coord` site coordinates with the actual site locations. the `coord` object used here was created by taking the original site locations and applying the `jitter` function, which jitters x and y coordinates by a specified amount. + + + +```r +library(igraph) +library(sf) +library(ggmap) +library(ggrepel) +library(ggpubr) + +load("data/Figure6_7.Rdata") +# g.net - igraph network object of San Pedro sites based on +# ceramic similarity +# base3 - basemap background terrain + +# Define coordinates of "jittered" points +# These points were originally created using the "jitter" function +# until a reasonable set of points were found. +coord <- c(-110.7985, 32.97888, +-110.7472, 32.89950, +-110.6965, 32.83496, +-110.6899, 32.91499, +-110.5508, 32.72260, +-110.4752, 32.60533, +-110.3367, 32.33341, +-110.5930, 32.43487, +-110.8160, 32.86185, +-110.6650, 32.64882, +-110.4558, 32.56866, +-110.6879, 32.60055, +-110.7428, 32.93124, +-110.4173, 32.34401, +-110.7000, 32.73344) + +attr <- c("Swingle's Sample", "Ash Terrace", "Lost Mound", + "Dudleyville Mound", "Leaverton", "High Mesa", + "Elliott Site", "Bayless Ruin", "Flieger", + "Big Bell", "111 Ranch", "Twin Hawks", "Artifact Hill", + "Jose Solas Ruin", "Wright") + + +# Convert coordinates to data frame +zz <- as.data.frame(matrix(coord, nrow = 15, byrow = TRUE)) +colnames(zz) <- c("x", "y") + + +# Extract edge list from network object +edgelist <- igraph::as_edgelist(g.net) + +# Create data frame of beginning and ending points of edges +edges2 <- data.frame(zz[edgelist[, 1], ], zz[edgelist[, 2], ]) +colnames(edges2) <- c("X1", "Y1", "X2", "Y2") + +# Plot jittered coordinates on map +figure_6_7 <- ggmap(base3, darken = 0.35) + + geom_segment( + data = edges2, + aes( + x = X1, + y = Y1, + xend = X2, + yend = Y2 + ), + col = "white", + size = 1 + ) + + geom_point( + data = zz, + aes(x, y), + alpha = 0.8, + col = "red", + size = 5, + show.legend = FALSE + ) + + geom_text_repel(aes(x = x, y = y, label = attr), data = zz, size = 3) + + theme_void() + +figure_6_7 +``` + + + +### Figure 6.8: Graph Layout Algorithms {- #Figure_6_8} + +Fig. 6.8. Several different graph layouts all using the Bronze Age Aegean geographic network (Evans et al. 2011). In each graph, nodes are scaled based on betweenness centrality and color-coded based on clusters defined using modularity maximisation. + +In the code below the only thing we change between each plot is the `layout` argument in `ggraph`. See [the CRAN project page on ggraph](https://cran.r-project.org/web/packages/ggraph/vignettes/Layouts.html) for more information on available layouts. We plot clusters by color here to make it easier to track differences between the layout options. [Use these data](data/aegean.Rdata) to dowload the background map of the Aegean area. + + +```r +library(igraph) +library(ggraph) +library(ggpubr) +library(igraphdata) +library(graphlayouts) +library(sf) +library(ggmap) + +# Load igraph Aegean_net data + +aegean <- read.csv("data/aegean.csv", row.names = 1, header = T) +aegean_dist <- aegean +aegean_dist[aegean_dist > 124] <- 0 +aegean_dist[aegean_dist > 0] <- 1 +aegean_net <- graph_from_adjacency_matrix(as.matrix(aegean_dist)) +load("data/aegean_map.Rdata") + +# Define cluster membership and betweenness centrality for plotting +grp <- as.factor(cluster_optimal(aegean_net)$membership) +bw <- as.numeric(igraph::betweenness(aegean_net)) + +# Create geographic network and plot +nodes <- read.csv("data/aegean_locs.csv") + +# Convert attribute location data to sf coordinates +locations_sf <- + st_as_sf(nodes, + coords = c("Longitude", "Latitude"), + crs = 4326) +coord1 <- do.call(rbind, st_geometry(locations_sf)) %>% + tibble::as_tibble() %>% + setNames(c("lon", "lat")) + +xy <- as.data.frame(coord1) +colnames(xy) <- c("x", "y") + + +# Extract edge list from network object for road_net +edgelist1 <- igraph::as_edgelist(aegean_net) + +# Create data frame of beginning and ending points of edges +edges1 <- as.data.frame(matrix(NA, nrow(edgelist1), 4)) +colnames(edges1) <- c("X1", "Y1", "X2", "Y2") +for (i in seq_len(nrow(edgelist1))) { + edges1[i, ] <- + c(nodes[which(nodes$Name == edgelist1[i, 1]), ]$Longitude, + nodes[which(nodes$Name == edgelist1[i, 1]), ]$Latitude, + nodes[which(nodes$Name == edgelist1[i, 2]), ]$Longitude, + nodes[which(nodes$Name == edgelist1[i, 2]), ]$Latitude) +} + +geo_net <- ggmap(my_map) + + geom_segment( + data = edges1, + aes( + x = X1, + y = Y1, + xend = X2, + yend = Y2 + ), + col = "black", + size = 1 + ) + + geom_point( + data = xy, + aes(x, y, size = bw, fill = grp), + alpha = 0.8, + shape = 21, + show.legend = FALSE + ) + + scale_size(range = c(4, 12)) + + scale_color_brewer(palette = "Set2") + + scale_fill_brewer(palette = "Set2") + + theme_graph() + + ggtitle("Geographic") + + theme(plot.title = element_text(size = rel(1))) + +# Multidimensional Scaling Layout with color by cluster and node +# size by betweenness +set.seed(435353) +g_mds <- ggraph(aegean_net, layout = "mds") + + geom_edge_link0(width = 0.2) + + geom_node_point(aes(fill = grp, size = bw), + shape = 21, + show.legend = FALSE) + + scale_size(range = c(4, 12)) + + scale_color_brewer(palette = "Set2") + + scale_fill_brewer(palette = "Set2") + + scale_edge_color_manual(values = c(rgb(0, 0, 0, 0.3), + rgb(0, 0, 0, 1))) + + theme_graph() + + theme(plot.title = element_text(size = rel(1))) + + ggtitle("Multi-Dimensional Scaling") + + theme(legend.position = "none") + +# Fruchterman-Reingold Layout with color by cluster and node size +# by betweenness +set.seed(435353) +g_fr <- ggraph(aegean_net, layout = "fr") + + geom_edge_link0(width = 0.2) + + geom_node_point(aes(fill = grp, size = bw), + shape = 21, + show.legend = FALSE) + + scale_size(range = c(4, 12)) + + scale_color_brewer(palette = "Set2") + + scale_fill_brewer(palette = "Set2") + + scale_edge_color_manual(values = c(rgb(0, 0, 0, 0.3), + rgb(0, 0, 0, 1))) + + theme_graph() + + theme(plot.title = element_text(size = rel(1))) + + ggtitle("Fruchterman-Reingold") + + theme(legend.position = "none") + +# Kamada-Kawai Layout with color by cluster and node size by betweenness +set.seed(435353) +g_kk <- ggraph(aegean_net, layout = "kk") + + geom_edge_link0(width = 0.2) + + geom_node_point(aes(fill = grp, size = bw), + shape = 21, + show.legend = FALSE) + + scale_size(range = c(4, 12)) + + scale_color_brewer(palette = "Set2") + + scale_fill_brewer(palette = "Set2") + + scale_edge_color_manual(values = c(rgb(0, 0, 0, 0.3), + rgb(0, 0, 0, 1))) + + theme_graph() + + theme(plot.title = element_text(size = rel(1))) + + ggtitle("Kamada-Kawai") + + theme(legend.position = "none") + +# Radial Centrality Layout with color by cluster and node size by +# betweenness +set.seed(435353) +g_cent <- ggraph(aegean_net, + layout = "centrality", + centrality = igraph::betweenness(aegean_net)) + + geom_edge_link0(width = 0.2) + + geom_node_point(aes(fill = grp, size = bw), + shape = 21, + show.legend = FALSE) + + scale_size(range = c(4, 12)) + + scale_color_brewer(palette = "Set2") + + scale_fill_brewer(palette = "Set2") + + scale_edge_color_manual(values = c(rgb(0, 0, 0, 0.3), + rgb(0, 0, 0, 1))) + + theme_graph() + + theme(plot.title = element_text(size = rel(1))) + + ggtitle("Radial Centrality") + + theme(legend.position = "none") + +# Spectral Layout with color by cluster and node size by betweenness +u1 <- layout_with_eigen(aegean_net) +g_spec <- ggraph(aegean_net, + layout = "manual", + x = u1[, 1], + y = u1[, 2]) + + geom_edge_link0(width = 0.2) + + geom_node_point(aes(fill = grp, size = bw), + shape = 21, + show.legend = FALSE) + + scale_size(range = c(4, 12)) + + scale_color_brewer(palette = "Set2") + + scale_fill_brewer(palette = "Set2") + + scale_edge_color_manual(values = c(rgb(0, 0, 0, 0.3), + rgb(0, 0, 0, 1))) + + + theme_graph() + + theme(plot.title = element_text(size = rel(1))) + + ggtitle("Spectral") + + theme(legend.position = "none") + + +figure_6_8 <- + ggarrange(geo_net, + g_mds, + g_fr, + g_kk, + g_cent, + g_spec, + ncol = 2, + nrow = 3) +figure_6_8 +``` + + + +### Figure 6.9: Heirarchical Graph Layouts {- #Figure_6_9} + +Fig. 6.9. Examples of visualisations based on hierarchical graph data. (a) Graph with nodes color-coded by hierarchical level. (b) Bubble plot where nodes are scaled proportional to the sub-group size. (c) Dendrogram of hierarchical cluster data. (d) Radial graph with edges bundled based on similarity in relations. Edges are colour-coded such that they are red at the origin and purple at the destination to help visualise direction. + +These graphs are based on a hierarchical graph that was created by assigning nodes to the leaves of a hierarchical cluster analysis performed on the Cibola ceramic technological cluster data. The data for 6.9d were randomly generated following an example on the R Graph Gallery. [Use these data](data/Figure6_9.Rdata) to follow along. + + +```r +# initialize libraries +library(igraph) +library(ggraph) +library(ape) +library(RColorBrewer) +library(ggpubr) + +load(file = "data/Figure6_9.Rdata") + +set.seed(4353543) +h1 <- ggraph(h_graph, "circlepack") + + geom_edge_link() + + geom_node_point(aes(colour = depth, size = (max(depth) - depth) / 2), + show.legend = FALSE) + + scale_color_viridis() + + theme_graph() + + coord_fixed() + +set.seed(643346463) +h2 <- ggraph(h_graph, "circlepack") + + geom_node_circle(aes(fill = depth), + size = 0.25, + n = 50, + show.legend = FALSE) + + scale_fill_viridis() + + theme_graph() + + coord_fixed() + +h3 <- ggraph(h_graph, "dendrogram") + + geom_node_point(aes(filter = leaf), + color = "blue", + alpha = 0.7, + size = 3) + + theme_graph() + + geom_edge_link() + +h4 <- + ggraph(sub_grp_graph, layout = "dendrogram", circular = TRUE) + + geom_conn_bundle( + data = get_con(from = from, to = to), + alpha = 0.2, + width = 0.9, + tension = 0.9, + aes(colour = ..index..) + ) + + scale_edge_colour_distiller(palette = "RdPu") + + geom_node_point(aes( + filter = leaf, + x = x * 1.05, + y = y * 1.05, + colour = group), + size = 3) + + scale_colour_manual(values = rep(brewer.pal(9, "Paired"), 30)) + + theme_graph() + + theme(legend.position = "none") + +figure_6_9 <- ggarrange( + h1, + h2, + h3, + h4, + ncol = 2, + nrow = 2, + labels = c("(a)", "(b)", "(c)", "(d)") +) +figure_6_9 +``` + + + +### Figure 6.10: Be kind to the color blind {- #Figure_6_10} + +Fig. 6.10. Examples of a simple network graph with color-coded clusters. The top left example shows the unmodified figure and the remaining examples simulate what such a figure might look like to people with various kinds of colour vision deficiencies. + +This function calls a script that we modified from the `colorblindr` package by [Claus Wilke](https://github.com/clauswilke/colorblindr) which is available here. The function `cv2_grid` take any ggplot object and outputs a 2 x 2 grid with the original figure and examples of what the figure might look like to people with three of the most common forms of color vision deficiency. Use [these data](data/Peeples2018) and [this script](scripts/colorblindr.R) to follow along. + + + +```r +library(igraph) +library(statnet) +library(intergraph) +library(ggraph) +library(RColorBrewer) +library(colorspace) +source("scripts/colorblindr.R") + +load("data/Peeples2018.Rdata") + +# Create igraph object for plots below +net <- asIgraph(brnet) + +set.seed(347) +g1 <- ggraph(net, layout = "kk") + + geom_edge_link(edge_color = "gray", alpha = 0.7) + + geom_node_point( + aes(fill = site_info$Region), + shape = 21, + size = igraph::degree(net) / 2, + alpha = 0.5 + ) + + scale_fill_brewer(palette = "Set2") + + theme_graph() + + theme(legend.position = "none") + +cvd_grid2(g1) +``` + + + + +### Figure 6.11: Node Symbol and Color Schemes {- #Figure_6_11} + +Fig. 6.11. Examples of different node color and symbol schemes. Note how adding color and size eases the identification of particular values, in particular with closely spaced points. Using transparency can similarly aid in showing multiple overlapping nodes. + +The version that appears in the book was compiled and labeled in Adobe Illustrator using the output created here. + + +```r +library(scales) + +plot( + x = 1:5, + y = rep(2, 5), + pch = 16, + cex = seq(5:10), + col = "blue", + ylim = c(0, 4), + bty = "n", + xaxt = "n", + yaxt = "n", + xlab = "", + ylab = "" +) +points( + x = 1:5, + y = rep(1.5, 5), + pch = 21, + cex = seq(5:10), + bg = heat.colors(5, rev = TRUE) +) +points( + x = 1:5, + y = rep(1, 5), + pch = c(1, 2, 3, 4, 5), + cex = seq(5:10), + bg = "skyblue", + col = "blue", + lwd = 2 +) +``` + + + +```r +set.seed(34456) +x <- rnorm(15, 1, 0.5) +y <- rnorm(15, 1, 0.5) +xy <- cbind(x, y) +xy2 <- cbind(x + 5, y) +xy3 <- cbind(x + 10, y) +xy4 <- cbind(x + 15, y) +xy5 <- cbind(x + 20, y) + +size <- sample(c(5, 6, 7, 8, 9), size = 15, replace = TRUE) +size <- size - 4 + +h_col <- heat.colors(5, rev = TRUE) + +plot( + xy[order(size, decreasing = TRUE), ], + pch = 16, + col = "blue", + cex = size[order(size, decreasing = TRUE)], + xlim = c(0, 22), + ylim = c(-1, 3), + bty = "n", + xaxt = "n", + yaxt = "n", + xlab = "", + ylab = "" +) +points(xy2[order(size, decreasing = TRUE), ], + pch = 21, + bg = h_col[size[order(size, decreasing = TRUE)]], + cex = size[order(size, decreasing = TRUE)]) +points(xy3[order(size, decreasing = TRUE), ], + pch = size[order(size, decreasing = TRUE)], + col = "blue", + cex = size[order(size, decreasing = TRUE)]) +points( + xy4[order(size, decreasing = TRUE), ], + pch = 21, + col = "gray66", + bg = alpha("blue", 0.7), + cex = size[order(size, decreasing = TRUE)] +) +points(xy5[order(size, decreasing = TRUE), ], + pch = 21, + bg = alpha(h_col[size[order(size, decreasing = TRUE)]], 0.7), + cex = size[order(size, decreasing = TRUE)]) +``` + + + + +### Figure 6.12: Image for Node {- #Figure_6_12} + +Fig. 6.12. Network graph showing similarity among carved faces from Banés, Holguín province, Cuba. Nodes are depicted as the objects in question themselves and edges represent shared attributes with numbers indicating the number of shared attributes for each pair of faces. + +Figure 6.12 was used with permission by Angus Mol and the original was produced for his 2014 book. + +![](images/Fig6_12.jpg){width=100%} + +### Figure 6.13: Images for Nodes {- #Figure_6_13} + +Fig. 6.13. Two-mode network of ceramics and sites in the San Pedro Valley with ceramic ware categories represented by a graphic example of each type. + +The version of Figure 6.13 in the Brughmans and Peeples (2023) book was originally created in NetDraw and modified to add the node pictures in Adobe Photoshop. This approach was preferred as it produced higher resolution and more consistent images than the graphics we could produce directly in R for this particular feature. It is, however, possible to use images in the place of nodes in R networks as the example below illustrates. + +We have found in practice that this feature in R works best for simple icons. If you are using high resolution images or lots of color or detail in your images it works better to create an initial image format in something like R or NetDraw and then to modify the network in a graphical editing software after the fact. + +In the place of the example in the book, we here demonstrate how you can use image files with R to create nodes as pictures. You can [download the data](data/Figure6_13.Rdata) to follow along. This .RData file also includes the images used here in R format and the code used to read in .png images is shown below but commented out. + + +```r +library(png) +library(igraph) + +load("data/Figure6_13.Rdata") +# two_mode_net - igraph two mode network object + +# Set Vector property to images by mode +# Note that if you want to set a different image +# for each node you can simply create a long list +# containing image names for node type 1 followed +# by image names for node type 2. +V(two_mode_net)$raster <- list(img.1, img.2)[V(two_mode_net)$type + 1] + +set.seed(34673) +plot( + two_mode_net, + vertex.shape = "raster", + vertex.label = NA, + vertex.size = 16, + vertex.size2 = 16, + edge.color = "gray" +) +``` + + + +If you want to use images in a one mode network you can follow the sample below using [these data](data/Cibola_adj.csv). Note that in the line with `V(Cibola_i)$raster` you can either assign a single image or an image for each node in the network. + + +```r +library(png) +library(igraph) + +cibola <- + read.csv(file = "data/Cibola_adj.csv", + header = TRUE, + row.names = 1) + +# Create network in igraph format +cibola_i <- igraph::graph_from_adjacency_matrix(as.matrix(cibola), + mode = "undirected") +# Set Vector property to images using a list with a length +# determined by the number of nodes in the network. +# Here we divide the northern and southern portions of the +# study area. +V(cibola_i)$raster <- list(img.2, img.1, img.2, img.2, + img.1, img.2, img.2, img.1, + img.1, img.1, img.2, img.2, + img.2, img.1, img.1, img.2, + img.1, img.1, img.1, img.1, + img.1, img.2, img.1, img.1, + img.2, img.1, img.2, img.2, + img.2, img.2, img.1) + +set.seed(34673) +plot( + cibola_i, + vertex.shape = "raster", + vertex.label = NA, + vertex.size = 16, + vertex.size2 = 16, + edge.color = "gray" +) +``` + + + +### Figure 6.14: Edge Thickness and Color {- #Figure_6_14} + +Fig. 6.14. A random weighted graph where edge line thickness and color are both used to indicate weight in 5 categories. + +You can [download the data](data/Figure6_14.Rdata) to follow along. + + +```r +library(igraph) +library(ggraph) + +load("data/Figure6_14.Rdata") + +edge_cols <- colorRampPalette(c("gray", "darkblue"))(5) + +set.seed(43644) +ggraph(g_net, layout = "fr") + + geom_edge_link0(aes(width = E(g_net)$weight), + edge_colour = edge_cols[E(g_net)$weight]) + + geom_node_point(shape = 21, + size = igraph::degree(g_net) + 3, + fill = "red") + + theme_graph() + + theme(legend.title = element_blank()) +``` + + + +### Figure 6.15: Edge Direction {- #Figure_6_15} + +Fig. 6.15. Two methods of displaying directed ties using arrows (left) and arcs (right). Both of these simple networks represent the same relationships shown in the adjacency matrix in the center. + +See the tutorial on [edges](#EdgeOptions) above for more details on using arrows in `ggraph`. We use the `grid.table` function here from the `gridExtra` package to plot tabular data as a figure. + + +```r +library(igraph) +library(grid) +library(gridExtra) + +g <- graph(c("A", "B", + "B", "C", + "A", "C", + "A", "A", + "C", "B", + "D", "C")) + +layout(matrix(c(1, 1, 2, 3, 3), 1, 5, byrow = TRUE)) + +set.seed(4355467) +plot( + g, + edge.arrow.size = 1, + vertex.color = "black", + vertex.size = 50, + vertex.frame.color = "gray", + vertex.label.color = "white", + edge.width = 2, + vertex.label.cex = 2.75, + vertex.label.dist = 0, + vertex.label.family = "Helvetica" +) + +plot.new() +adj1 <- as.data.frame(as.matrix(as_adjacency_matrix(g))) +tt2 <- ttheme_minimal(base_size = 25) +grid.table(adj1, theme = tt2) + +plot( + g, + edge.arrow.size = 1.25, + vertex.color = "black", + vertex.size = 50, + vertex.frame.color = "gray", + vertex.label.color = "white", + edge.width = 2, + edge.curved = 0.3, + vertex.label.cex = 2.75, + vertex.label.dist = 0, + vertex.label.family = "Helvetica" +) +``` + + + +### Figure 6.16: Edge Binarization{- #Figure_6_16} + +Fig. 6.16. These networks all show the same data based on similarity scores among sites in the U.S. Southwest (ca. AD 1350–1400) but each has a different cutoff for binarization. + +The following chunk of code uses [ceramic similarity data from the SWSN database](data/Figure6_16.Rdata) and defines three different cutoff thresholds for defining edges. Note the only difference is the `thresh` argument in the `event2dichot` function. + + +```r +library(igraph) +library(statnet) +library(intergraph) +library(ggraph) +library(ggpubr) + +load("data/Figure6_16.Rdata") +# Contains similarity matrix AD1350sim + +ad1350sim_cut0_5 <- asIgraph(network( + event2dichot(ad1350sim, + method = "absolute", + thresh = 0.25), + directed = FALSE +)) +ad1350sim_cut0_75 <- asIgraph(network( + event2dichot(ad1350sim, + method = "absolute", + thresh = 0.5), + directed = FALSE +)) +ad1350sim_cut0_9 <- asIgraph(network( + event2dichot(ad1350sim, + method = "absolute", + thresh = 0.75), + directed = FALSE +)) + +set.seed(4637) +g0_50 <- ggraph(ad1350sim_cut0_5, layout = "fr") + + geom_edge_link0(edge_colour = "black") + + geom_node_point(shape = 21, fill = "gray") + + ggtitle("0.25") + + theme_graph() + +set.seed(574578) +g0_75 <- ggraph(ad1350sim_cut0_75, layout = "fr") + + geom_edge_link0(edge_colour = "black") + + geom_node_point(shape = 21, fill = "gray") + + ggtitle("0.50") + + theme_graph() + +set.seed(7343) +g0_90 <- ggraph(ad1350sim_cut0_9, layout = "fr") + + geom_edge_link0(edge_colour = "black") + + geom_node_point(shape = 21, fill = "gray") + + ggtitle("0.75") + + theme_graph() + +ggarrange(g0_50, g0_75, g0_90, nrow = 1, ncol = 3) +``` + + + +### Figure 6.17: Edge Bundling {- #Figure_6_17} + +Fig. 6.17. Network map of ceramic similarity from the U.S. Southwest/Mexican Northwest ca. AD 1350–1400 based on the hammer bundling algorithm. Note that this figure will look somewhat different from the one in the book as the locations of sites have been jittered for data security + +
+

This function relies on the edgebundle package to +combine sets of nodes with similar relations into single paths. This +package also requires that you install the reticulate +package which connects R to Python 3.7 and you must also have Python +installed on your computer with the datashader Python +libraries.

+

Note that this will require about 1.4 GB of disk space and several +minutes so make sure you have adequate space and time before +beginning.

+
+ +To install an instance of Python with all of the required libraries you can use the following call: + + +```r +edgebundle::install_bundle_py(method = "auto", conda = "auto") +``` + +[Use these data](data/Figure6_17.Rdata) to follow along. + + +```r +library(igraph) +library(ggraph) +library(edgebundle) +library(ggmap) +library(sf) + +load("data/Figure6_17.Rdata") +# attr.dat - site attribute data +# g.net - igraph network object +load("data/map.RData") +# map3 - state outlines +# base2 - terrain basemap in black and white + +locations_sf <- st_as_sf(attr.dat, coords = c("V3", "V4"), + crs = 26912) +z <- st_transform(locations_sf, crs = 4326) +coord1 <- do.call(rbind, st_geometry(z)) %>% + tibble::as_tibble() %>% + setNames(c("lon", "lat")) + +xy <- as.data.frame(coord1) +colnames(xy) <- c("x", "y") + +if (reticulate::py_module_available("datashader.bundling")) { + hbundle <- edge_bundle_hammer(g.net, xy, bw = 0.9, decay = 0.2) + + ggmap(base2, darken = 0.15) + + geom_polygon( + data = map3, + aes(x, y, + group = Group.1), + col = "black", + size = 0.5, + fill = NA + ) + + geom_path( + data = hbundle, + aes(x, y, group = group), + color = "white", + show.legend = FALSE + ) + + geom_path( + data = hbundle, + aes(x, y, group = group), + color = "darkorchid4", + show.legend = FALSE + ) + + geom_point( + data = xy, + aes(x, y), + alpha = 0.4, + size = 2.5, + show.legend = FALSE + ) + + theme_graph() +} else { + edgelist <- igraph::as_edgelist(g.net) + edges <- data.frame(xy[edgelist[, 1], ], xy[edgelist[, 2], ]) + colnames(edges) <- c("x", "y", "xend", "yend") + + ggmap(base2, darken = 0.15) + + geom_polygon( + data = map3, + aes(x, y, + group = Group.1), + col = "black", + size = 0.5, + fill = NA + ) + + geom_segment( + data = edges, + aes(x = x, y = y, xend = xend, yend = yend), + color = "darkorchid4", + alpha = 0.3, + show.legend = FALSE + ) + + geom_point( + data = xy, + aes(x, y), + alpha = 0.4, + size = 2.5, + show.legend = FALSE + ) + + theme_graph() +} +``` + + + +### Figure 6.18: Group-in-a-box {- #Figure_6_18} + +Fig. 6.18. Example of a group-in-a-box custom graph layout created in NodeXL based on ceramic similarity data from the U.S. Southwest/Mexican Northwest ca. AD 1350-1400. + +The group-in-a-box network format is, as far as we are aware, currently only implemented in the [NodeXL](https://www.smrfoundation.org/nodexl/) platform. This software package is an add-in for Microsoft Excel that allows for the creation and analysis of network graphs using a wide variety of useful visualization tools. To produce a "Group-in-a-box" layout you simply need to paste a set of edge list values into the NodeXL Excel Template, define groups (based on an algorithm or some vertex attribute), and be sure to select "Layout each of the graph's groups in its own box" in the layout options. + +For more details on how to use NodeXL see the extensive documentation online. There are commercial versions of the software available but the group-in-a-box example shown here can be produced in the free version. + +![](images/group-in-a-box.jpg){width=100%} + +To download an Excel workbook set up for the example provided in the book [click here]("data/NodeXLGraph1.xlsx"). When you open this In Excel, it will ask you if it can install the necessary extensions. Say yes to continue and replicate the results in the book. + +### Figure 6.19: Weighted Adjacency Matrix {- #Figure_6_19} + +Fig. 6.19. Dual display of a network graph and associated weighted adjacency matrix based on Peeples (2018) ceramic technology data. + +This plot uses a sub-set of the [Cibola technological similarity network](#Cibola) data to produce both a typical node-link diagram and an associated weighted adjacency matrix. [Use these data](data/Figure6_19.Rdata) to follow along. + + +```r +library(igraph) +library(ggraph) +library(ggpubr) + +load("data/Figure6_19.Rdata") +# graph6.18 - graph object in igraph format +# node_list - data frame with node details +# edge_list - edge_list which contains information on groups +# and edge weight + +set.seed(343645) +coords <- layout_with_fr(graph6.18) +g1 <- ggraph(graph6.18, "manual", + x = coords[, 1], + y = coords[, 2]) + + geom_edge_link(aes(), + color = "gray75", + alpha = 0.5, + show.legend = FALSE) + + geom_node_point(aes(color = as.factor(V(graph6.18)$comm), size = 5), + show.legend = FALSE) + + scale_color_manual(values = c("#8da0cb", "#66c2a5", "#fc8d62"), + guide = FALSE) + + theme_graph() + +# Set order of nodes to order in which they appear in the y axis in +# the network graph above +name_order <- node_list[order(coords[, 2]), ]$name + +# Adjust the "to" and "from" factor levels so they are equal +# to this complete list of node names +plot_data <- edge_list %>% mutate(to = factor(to, levels = name_order), + from = factor(from, levels = rev(name_order))) + +# Now run the ggplot code again +# Create the adjacency matrix plot +g2 <- ggplot(plot_data, aes( + x = from, + y = to, + fill = group, + alpha = (weight * 1.5) +)) + + geom_tile() + + theme_bw() + + scale_x_discrete(drop = FALSE) + + scale_y_discrete(drop = FALSE) + + theme( + axis.text.x = element_text( + angle = 270, + hjust = 0, + size = rel(0.5) + ), + axis.text.y = element_text(size = rel(0.5)), + aspect.ratio = 1, + legend.position = "none" + ) + + xlab("") + + ylab("") + + scale_fill_manual(values = c("#8da0cb", "#66c2a5", "#fc8d62", "black"), + guide = FALSE) + +# Combine into a single figure +figure6_19 <- ggarrange(g1, g2, nrow = 1) + +figure6_19 +``` + + + +### Figure 6.20: Nodetrix Diagram {- #Figure_6_20} + +Fig. 6.20. Nodetrix visualisation of the Peeples (2018) ceramic technological data showing one dense cluster as an adjacency matrix and the remainder of the graph as a node-link diagram. + +![Nodetrix visualization](images/nodetrix.jpg){width=100%} + +This Nodetrix interactive visualization was created using the Javascript implementation available on [GitHub](https://github.com/IRT-SystemX/nodetrix) by user [jdfekete](https://github.com/jdfekete/), Jean-Daniel Fekete who was one of the original authors of the method (Henry et al. 2007). To see a live demo of the Nodetrix Application in use with the Cibola technological similarity data [click here](https://mattpeeples.net/nodetrix/). + +The details of running the Javascript program are described on the GitHub page and are beyond this scope of this tutorial. We do illustrate below, however, how you can export R in the *.json format required by this program using the `d3r` and `rjson` packages. The code below expects and `igraph` network object. + +
+

Note that the Nodetrix.js application expects node names/designations +with no spaces in a node attribute called “name” so be sure to check +before you run the code below.

+
+ + + +```r +library(d3r) +library(rjson) + +# net <- igraph network object + +data_json <- d3_igraph(net) + + +dj <- jsonlite::fromJSON(data_json) +dj$links[[1]] <- as.numeric(dj$links[[1]]) +dj$links[[2]] <- as.numeric(dj$links[[2]]) +dj <- jsonlite::toJSON(dj) + +write(dj, "network.json") +``` + + +### Figure 6.21: The Filmstrip Approach {- #Figure_6_21} + +Fig. 6.21. A demonstration of the filmstrip approach to plotting longitudinal network data. These data represent networks of ceramic similarity in the San Pedro Valley of Arizona for three consecutive 50-year intervals. + +[Use these data](data/Figure6_21.Rdata) to replicate the figures shown here. + + +```r +library(igraph) +library(ggraph) +library(ggpubr) + +load("data/Figure6_21.Rdata") + +set.seed(4543) +g1 <- ggraph(AD1250net, "kk") + + geom_edge_link(aes(), color = "gray75", show.legend = FALSE) + + geom_node_point(aes(), + size = 1, + show.legend = FALSE, + color = "blue") + + ggtitle("AD1250-1300") + + theme_graph() + +set.seed(4543) +g2 <- ggraph(AD1300net, "kk") + + geom_edge_link(aes(), color = "gray75", show.legend = FALSE) + + geom_node_point(aes(), + size = 1, + show.legend = FALSE, + color = "blue") + + ggtitle("AD1300-1350") + + theme_graph() + + +set.seed(4543) +g3 <- ggraph(AD1350net, "kk") + + geom_edge_link(aes(), color = "gray75", show.legend = FALSE) + + geom_node_point(aes(), + size = 1, + show.legend = FALSE, + color = "blue") + + ggtitle("AD1350-1400") + + theme_graph() + +figure6_21 <- ggarrange(g1, g2, g3, nrow = 1) + +figure6_21 +``` + + + +### Figure 6.22: Similtaneous Display {- #Figure_6_22} + +Fig. 6.22. Examples of simultaneous display of two consecutive intervals for the San Pedro valley ceramic similarity network. (a) A network using the Kamada-Kawai algorithm with edges color-coded based on time period. (b) An arc plot showing ties in consecutive intervals above and below the line. + +[Use these data](data/Figure6_22.Rdata) to follow along. Note in the first plot we add the `colour` argument to the `aes()` statement to include our period designation. + + +```r +library(igraph) +library(ggraph) +library(ggpubr) +library(ggrepel) + +load("data/Figure6_22.Rdata") + +graph <- graph_from_data_frame(net_all) + +xy <- layout_with_kk(graph) +xy <- cbind(sites, xy) +xy <- as.data.frame(xy) +colnames(xy) <- c("site", "x", "y") +xy$x <- as.numeric(xy$x) +xy$y <- as.numeric(xy$y) + +set.seed(6436) +similt_net <- ggraph(graph, layout = "manual", + x = xy$x, y = xy$y) + + geom_edge_link(aes(colour = Period), alpha = 0.3, width = 1) + + geom_node_point(size = 3) + + theme_graph() + + theme(legend.title = element_text(size = rel(1)), + legend.text = element_text(size = rel(1)), + legend.key.height = unit(1, "cm"), + legend.key.width = unit(2, "cm")) + +# Make the graph +lin_net <- ggraph(spgraph, layout = "linear") + + geom_edge_arc(edge_colour = "black", edge_alpha = 0.4, edge_width = 0.3, + fold = FALSE, strength = 1) + + geom_node_point(aes(size = igraph::degree(spgraph)), col = "red", + alpha = 0.5) + + scale_size_continuous(range = c(4, 8)) + + theme_graph() + + theme(legend.title = element_blank(), + plot.margin = unit(c(0, 0, 0.4, 0), "null"), + panel.spacing = unit(c(0, 0, 3.4, 0), "null")) + + annotate("text", x = 3, y = 3, label = "AD 1250-1300", + size = 4) + + annotate("text", x = 3, y = -3, label = "AD 1300-1350", + size = 4) + +similt_net +``` + + + +```r +lin_net +``` + + + +### Figure 6:23: Timelines and Time Prisms {- #Figure_6_23} + +Fig. 6.23. This plot shows two displays of the same ceramic similarity data from the Sonoran Desert in the U.S. Southwest as a time prism (top) and timeline (bottom). + +These examples were drawn from work outline on a workshop focused on temporal networks by Skye Bender-deMoll. [Click here](https://statnet.org/Workshops/ndtv_workshop.html) to see the detailed workshop overview. The functions for animating and plotting temporal networks used here come from the `ndtv` and `networkDynamic` packages. + +
+

Note that the data required is a list object that contains multiple +temporal slices of the same network in network format from +the statnet suite of packages. Each network must have the +same number of nodes and the same node identifiers must be used in every +network in the list.

+
+ +[Use these data](data/Figure6_23.Rdata) to follow along. + + +```r +library(networkDynamic) +library(ndtv) +library(statnet) + +load("data/Figure6_23.Rdata") + +# create networkDynamic object from list containing multiple +# sna network objects +sanpedro <- networkDynamic(network.list = sp_nets) +``` + +``` +## Neither start or onsets specified, assuming start=0 +## Onsets and termini not specified, assuming each network in network.list should have a discrete spell of length 1 +## Argument base.net not specified, using first element of network.list instead +## Created net.obs.period to describe network +## Network observation period info: +## Number of observation spells: 1 +## Maximal time range observed: 0 until 5 +## Temporal mode: discrete +## Time unit: step +## Suggested time increment: 1 +``` + +```r +# Compute animation +compute.animation(sanpedro, default.dist = 7, animation.mode = "kamadakawai") +``` + +``` +## slice parameters: +## start:0 +## end:5 +## interval:1 +## aggregate.dur:1 +## rule:latest +``` + +```r +# Define colors for regions +mycol <- c( + scales::alpha("#1b9e77", 0.75), + scales::alpha("#d95f02", 0.75), + scales::alpha("#7570b3", 0.75), + scales::alpha("#e7298a", 0.75), + scales::alpha("#66a61e", 0.75), + scales::alpha("#e6ab02", 0.75) +) + +if (requireNamespace("scatterplot3d", quietly = TRUE) && + requireNamespace("prettyGraphs", quietly = TRUE)) { + library(scatterplot3d) + library(prettyGraphs) + + # Plot time prism + set.seed(364467) + timePrism( + sanpedro, + at = c(1, 2, 3), + displaylabels = FALSE, + planes = TRUE, + display.isolates = FALSE, + label.cex = 0.5, + usearrows = FALSE, + vertex.cex = 0.5, + edge.col = "gray50", + vertex.col = mycol[factor(sp_attr$SWSN_MacroGroup)] + ) + + # Plot proximity timeline + set.seed(235254) + proximity.timeline( + sanpedro, + default.dist = 10, + mode = "sammon", + labels.at = 17, + vertex.cex = 4, + render.edges = FALSE, + vertex.col = mycol[factor(sp_attr$SWSN_MacroGroup)], + chain.direction = "reverse", + xaxt = "n" + ) +} else { + plot( + sp_nets[[1]], + vertex.col = mycol[factor(sp_attr$SWSN_MacroGroup)], + displaylabels = FALSE + ) +} +``` + + + +### Figure 6.24: Animation {- #Figure_6_24} + +Fig. 6.24. An example of three frames from a network animation. + +Figure 6.24 was created using the `ndtv` package and the same data produced above for figure 6.23. We simply rendered the animation as above and then output to an interactive html widget. The figure in the book represents 3 screen shots from the interactive plot. See the `ndtv` documentation for more details. + + +```r +render.d3movie(sanpedro, vertex.col = mycol[factor(sp_attr$SWSN_MacroGroup)]) +``` + + +```r +render.d3movie(sanpedro, vertex.col = mycol[factor(sp_attr$SWSN_MacroGroup)], + output.mode = "inline") +``` + + + +### Figure 6.25: Interactive Networks {- #Figure_6_25} + +Fig. 6.25. An example of a dynamic network visual created in R. Notice how the nodes and edges are responding to the movement of the edge under the cursor and the drop down menu that allows selection of nodes by group. + +For this example we closely follow an example provided on the [Static and dynamic network visualization with R](https://kateto.net/network-visualization) workshop documents online but using the [Cibola technological similarity data](data/Figure6_25.Rdata) instead. + + +```r +library(visNetwork) +library(networkD3) +library(igraph) + +load("data/Figure6_25.Rdata") # Contains an igraph graph object + +# Use igraph to make the graph and find membership +clust <- cluster_louvain(graph) +members <- membership(clust) + +# Convert to object suitable for networkD3 +graph_d3 <- igraph_to_networkD3(graph, group = members) + +# Modify interactive network to allow highlighting by groups, etc. +links <- graph_d3$links +colnames(links) <- c("from", "to") +links[, 1] <- links[, 1] + 1 +links[, 2] <- links[, 2] + 1 +nodes <- graph_d3$nodes +colnames(nodes)[1] <- "id" + +# Create node and link objects in d3 format +vis_nodes <- nodes +vis_links <- links + +# Set visualization options +vis_nodes$shape <- "dot" +vis_nodes$shadow <- TRUE # Nodes will drop shadow +vis_nodes$borderWidth <- 2 # Node border width +vis_nodes$color.background <- c("slategrey", "tomato", "gold", + "purple")[nodes$group] +vis_nodes$color.border <- "black" +vis_nodes$color.highlight.background <- "orange" +vis_nodes$color.highlight.border <- "darkred" + +# Create network in d3 format +visnet <- visNetwork(vis_nodes, vis_links) + +# View network with visualization options active +visOptions(visnet, highlightNearest = TRUE, selectedBy = "group") +``` + +```{=html} +
+ +``` + +### Figure 6.26: SWSN Example 1{- #Figure_6_26} + +Fig. 6.26. Networks by time for the SWSN project area (from Mills et al. 2013). + +The figure for the original plot in Mills et al. 2013 was produced in R and then compiled and modified using Adobe Illustrator. First a regional color scheme was defined and then each time period was plotted using this color scheme. In Illustrator components were arranged in rough geographic positions and isolates were placed at the margin. Click the link for more info on the [Southwest Social Networks Project](#SWSN.) + +The following chunk of code reproduces Figure 6.26 for one time period (AD1300-1350). [Download these data](data/Figure6_26.Rdata) to follow along. + + +```r +library(statnet) +library(ggraph) + +load("data/Figure6_26.Rdata") + +# Create sna network object +net <- + network(event2dichot(sim, method = "absolute", thresh = 0.75), + directed = FALSE) + +# define color scheme. colors listed in order based on the +# factor attr$Macro +mycols <- c("#000738", "#ffa1a1", "#ad71d8", "#016d1b", "#00ff30", + "#92d8ff", "#ffffff", "#adadad", "#846b00", "#ff0000", + "#5273dd", "#946a43", "#a00000", "#f97c00", "#00ffec", + "#ffff3e", "#824444", "#00ba89", "#00ba89", "#0303ff") + +# Plot network +set.seed(235) +ggraph(net, layout = "fr") + + geom_edge_link(alpha = 0.5) + + geom_node_point(aes(fill = as.factor(attr$Macro), size = evcent(net)), + shape = 21, + show.legend = FALSE) + + scale_size(range = c(1.5, 3)) + + scale_fill_manual(values = mycols) + + theme_graph() +``` + + + +### Figure 6.27: SWSN Example 2{- #Figure_6_27} + +Fig. 6.27. An explicit geographic map network of the SWSN project area through time (Mills et al. 2013). + +The original version of this figure was produced in ArcGIS using data prepared in R. Here we show how these same network maps with edges color coded by geographic length can be produced in R. We provide code to prepare a map for one time period (AD1300-1350). [Use these data](data/Figure6_27.Rdata) to follow along. Note that this figure will differ slightly from the one in the book and in the original Mills et al. 2013 publication as site locations have been jittered. In this example we use geographic coordinates to calculate distance. See the [spatial networks](#SpatialNetworks) section for more details. + +
+

Note for short edges to be visible on top of long edges here we must +first sort the order of edges bu length in the original edge list before +converting it into a igraph network object. In the three lines beginning +with # Order edges so shorest will plot last, we use the +order function and set decreasing = TRUE so +that edges will be listed from longest to shortest. The order of the +edge list is the order that edges will be plotted.

+
+ + + + +```r +library(statnet) +library(igraph) +library(intergraph) +library(geosphere) +library(ggmap) +library(sf) +library(tidyverse) +library(ggraph) + +# Load in network and map data +load("data/Figure6_27.Rdata") + +# prepare network object +net <- network(event2dichot(sim, method = "absolute", thresh = 0.75), + directed = FALSE) +r_net <- asIgraph(net) + +# convert coordinates to lat/long and covert to sf object +locations_sf <- st_as_sf(attr, + coords = c("EASTING", "NORTHING"), + crs = 26912) +z <- st_transform(locations_sf, crs = 4326) +coord1 <- do.call(rbind, st_geometry(z)) %>% + tibble::as_tibble() %>% + setNames(c("lon", "lat")) + +# output coordinates in data frame +xy <- as.data.frame(coord1) +colnames(xy) <- c("x", "y") + +# Create edge list with xy coordinates for each source and target +edgelist2 <- igraph::as_edgelist(r_net) +edges2 <- data.frame(xy[edgelist2[, 1], ], xy[edgelist2[, 2], ]) +colnames(edges2) <- c("X1", "Y1", "X2", "Y2") + +# Determine the geographic distances of edges using the distm +# function in the geosphere package +dist_meas <- NULL +for (i in seq_len(nrow(edges2))) { + temp <- as.matrix(edges2[i, ]) + dist_meas[i] <- distm(temp[1, 1:2], temp[1, 3:4]) +} + +# Order edges so shortest will plot last +net_dat <- as.data.frame(cbind(edges2, dist_meas)) +net_dat <- net_dat[order(net_dat$dist_meas, decreasing = TRUE), ] + +# Create bins in distance measurement +net_dat <- net_dat %>% + mutate(DistBins = cut(dist_meas, + breaks = c(-Inf, 25000, 100000, 250000, Inf))) + +# Plot network map +ggmap(base2, darken = 0.5) + + geom_segment( + data = net_dat, + aes( + x = X1, + y = Y1, + xend = X2, + yend = Y2, + col = DistBins + ), + size = 0.15, + show.legend = FALSE + ) + + scale_color_manual(values = c("white", "skyblue", "dodgerblue", + "darkblue")) + + theme_graph() +``` + + + diff --git a/06-spatial-networks.Rmd b/06-spatial-networks.Rmd index eee2aa5..c1e1bf7 100644 --- a/06-spatial-networks.Rmd +++ b/06-spatial-networks.Rmd @@ -65,7 +65,9 @@ activate(key="YOUR KEY HERE") Note, for the ease of demonstration, in the remainder of this online guide we pre-download the maps and provide them as a file instead of using the `get_stadiamap` function. ```{r, echo=F, warning=F} -source("stadia_API.R") +if (file.exists("stadia_API.R")) { + source("stadia_API.R") +} ``` Now you're ready to run code that can download the stadia map backgrounds automatically: @@ -73,9 +75,14 @@ Now you're ready to run code that can download the stadia map backgrounds automa ```{r, warning=F, message=F, cache=T} library(ggmap) -map <- get_stadiamap(bbox = c(-9.5, 36, 3, 43.8), - maptype = "stamen_terrain_background", - zoom = 6) +if (nzchar(Sys.getenv("STADIAMAPS_API_KEY"))) { + map <- get_stadiamap(bbox = c(-9.5, 36, 3, 43.8), + maptype = "stamen_terrain_background", + zoom = 6) +} else { + load("data/road_base.Rdata") + map <- my_map +} ggmap(map) ``` @@ -89,6 +96,7 @@ First we map the basic road network. We have commented the code below to explain library(igraph) library(ggmap) library(sf) +library(ggplot2) # Read in edge list and node location data and covert to network object edges1 <- read.csv("data/Hispania_roads.csv", header = TRUE) @@ -104,7 +112,7 @@ locations_sf <- xy <- data.frame(x = nodes$long, y = nodes$lat) # Extract edge list from network object -edgelist <- get.edgelist(road_net) +edgelist <- igraph::as_edgelist(road_net) # Create data frame of beginning and ending points of edges edges <- as.data.frame(matrix(NA, nrow(edgelist), 4)) @@ -118,10 +126,15 @@ for (i in seq_len(nrow(edgelist))) { nodes[which(nodes$Id == edgelist[i, 2]), 2]) } -# Download stamenmap background data. -my_map <- get_stadiamap(bbox = c(-9.5, 36, 3, 43.8), - maptype = "stamen_terrain_background", - zoom = 6) +# Download basemap data when a Stadia API key is available; otherwise +# use the bundled fallback map included with the repo. +if (nzchar(Sys.getenv("STADIAMAPS_API_KEY"))) { + my_map <- get_stadiamap(bbox = c(-9.5, 36, 3, 43.8), + maptype = "stamen_terrain_background", + zoom = 6) +} else { + load("data/road_base.Rdata") +} # Produce map starting with background ggmap(my_map) + @@ -336,7 +349,7 @@ ggraph(mst_net, layout = "kk") + geom_node_point(size = 4) + theme_graph() # Extract edge list from network object -edgelist <- get.edgelist(mst_net) +edgelist <- igraph::as_edgelist(mst_net) # Create data frame of beginning and ending points of edges edges <- as.data.frame(matrix(NA, nrow(edgelist), 4)) colnames(edges) <- c("X1", "Y1", "X2", "Y2") @@ -441,11 +454,11 @@ nn1 <- nng(x = nodes[, c(3, 2)], k = 1) # Calculate k=6 nearest neighbor graph nn6 <- nng(x = nodes[, c(3, 2)], k = 6) el1 <- as.data.frame( - rbind(cbind(get.edgelist(nn6), - rep("K=6", nrow(get.edgelist(nn1)) + rbind(cbind(igraph::as_edgelist(nn6), + rep("K=6", nrow(igraph::as_edgelist(nn1)) )), - cbind(get.edgelist(nn1), - rep("K=1", nrow(get.edgelist(nn1)) + cbind(igraph::as_edgelist(nn1), + rep("K=1", nrow(igraph::as_edgelist(nn1)) )))) colnames(el1) <- c("from", "to", "K") g <- graph_from_data_frame(el1) @@ -828,14 +841,25 @@ coord1 <- do.call(rbind, st_geometry(z)) %>% setNames(c("lon", "lat")) xy <- as.data.frame(cbind(attr$SWSN_Site, coord1)) colnames(xy) <- c("site", "x", "y") -base <- get_stadiamap( - bbox = c(-110.75, 33.5, -107, 38), - zoom = 8, - maptype = "stamen_terrain_background", - color = "bw" -) +if (nzchar(Sys.getenv("STADIAMAPS_API_KEY"))) { + base <- get_stadiamap( + bbox = c(-110.75, 33.5, -107, 38), + zoom = 8, + maptype = "stamen_terrain_background", + color = "bw" + ) + base_plot <- ggmap(base, darken = 0.15) +} else { + base_plot <- ggplot() + + coord_quickmap(xlim = c(-110.75, -107), ylim = c(33.5, 38)) + + theme_void() + + theme( + panel.background = element_rect(fill = "grey92", color = NA), + plot.background = element_rect(fill = "white", color = NA) + ) +} # Extract edge list from network object -edgelist <- get.edgelist(g36_net) +edgelist <- igraph::as_edgelist(g36_net) # Create data frame of beginning and ending points of edges edges <- as.data.frame(matrix(NA, nrow(edgelist), 4)) colnames(edges) <- c("X1", "Y1", "X2", "Y2") @@ -845,7 +869,7 @@ for (i in seq_len(nrow(edgelist))) { xy[which(xy$site == edgelist[i, 2]), 2], xy[which(xy$site == edgelist[i, 2]), 3]) } -figure7_8 <- ggmap(base, darken = 0.15) + +figure7_8 <- base_plot + geom_segment( data = edges, aes( diff --git a/06-spatial-networks.md b/06-spatial-networks.md new file mode 100644 index 0000000..c0a40d2 --- /dev/null +++ b/06-spatial-networks.md @@ -0,0 +1,1128 @@ +# Spatial Networks{#SpatialNetworks} + +![](images/image_break.png){width=100%} + +This section follows along with Chapter 7 of Brughmans and Peeples (2023) to provide information on how to implement spatial network models and analyses in R. Spatial networks are one of the most common kinds of networks used in archaeological research. Many network studies rely on GIS tools to conduct spatial network research, but R is quite capable of spatial analysis. Note that we have created a separate section on [spatial interaction models](#SpatialInteraction) in the "Going Beyond the Book" section of this document as those approaches in particular require extended discussion. + +Working with geographic data in R can be a bit complicated and we cannot cover all aspects in this brief tutorial. If you are interested in exploring geospatial networks more, we suggest you take a look at the excellent and free [*Geocomputation With R*](https://geocompr.robinlovelace.net/) book by Robin Lovelace, Jakob Nowosad, and Jannes Muenchow. The book is a bookdown document just like this tutorial and provides excellent and up to date coverage of spatial operations and the management of spatial data in R. + +## Working with Geographic Data in R{#GeoData} + +
+

There are a number of packages for R that are designed explicitly for +working with spatial data. Before we get into the spatial analyses it is +useful to first briefly introduce these packages and aspects of spatial +data analysis in R.

+
+ +The primary packages include: + +* **`sf`** - This package is designed for plotting and encoding simple spatial features and vector data and converting locations among different map projections. Check [here](https://r-spatial.github.io/sf/) for a good brief overview of the package. +* **`ggmap`** - This package is a visualization tool that allows you to combine typical R figures in the `ggplot2` format with static maps available online through services like Google Maps, Stamen Maps, OpenStreet Maps, and others. This package is useful for quickly generating maps with a background layer and that is how we use it here. +* **`cccd`** - This is a package that is designed explicitly for working with spatial data and has a number of functions for defining networks based on relative neighborhoods and other spatial network definitions. +* **`deldir`** - This package is designed to create spatial partitions including calculating Delaunay triangulation and Voronoi tessellations of spatial planes. +* **`geosphere`** - This is a package focused on spherical trigonometry and has functions which allow us to calculate distances between points in spherical geographic space across the globe. +* **`RBGL`** - This is an R implementation of a package called the Boost Graph Library. This package has a number of functions but we use it here as it provides a function to test for graph planarity. + +The spatial data we use in this document consists of vector data. This simply means that our mapping data re not images or pixels representing space but instead spatial coordinates that define locations and distances. One key aspect of spatial data in R, especially at large scales, is that we often need to define a projection or coordinate reference system to produce accurate maps. + +A coordinate reference system (CRS) is a formal definition of how spatial points relate to the surface of the globe. CRS typically fall into two categories: geographic coordinate systems and projected coordinate systems. The most common geographic coordinate system is the latitude/longitude system which describes locations on the surface of the Earth in terms of angular coordinates from the Prime Meridian and Equator. A projected data set refers to the process through which map makers take a spherical Earth and create a flat map. Projections distort and move the area, distance, and shape to varying degrees and provide xy location coordinates in linear units. The advantages and disadvantages of these systems are beyond the scope of this document but it is important to note that R often requires us to define our coordinate reference system when working with spatial data. + +In the code below and in several other sections of the book you have seen function calls that include an argument called `crs`. This is the coordinate reference system object used by R which provides a numeric code denoting the CRS used by a given data set. Just like we take external .csv data and covert them into network objects R understands, we need to import spatial data and convert to an object R recognizes. We do this in the `sf` package using the `st_as_sf` function. + +To use this function you take a data frame which includes location xy information, you use the `coords` argument to specify which fields are the x and y coordinates, and then use the `crs` code to specify the coordinate reference system used. In this example we use code `4326` which refers to the WGS84 World Geodetic System of geographic coordinates. [See this website](https://epsg.io/) to look up many common `crs` code options. + + +```r +library(sf) +nodes <- read.csv("data/Hispania_nodes.csv", header = T) +locs <- st_as_sf(nodes, coords = c("long", "lat"), crs = 4326) + +locs +``` + +``` +## Simple feature collection with 122 features and 2 fields +## Geometry type: POINT +## Dimension: XY +## Bounding box: xmin: -9.1453 ymin: 36.0899 xmax: 3.1705 ymax: 43.5494 +## Geodetic CRS: WGS 84 +## First 10 features: +## Id name geometry +## 1 n0 "Bracara" POINT (-8.427 41.5501) +## 2 n1 "Iria Flavia" POINT (-8.5974 42.8101) +## 3 n2 "Saltigi" POINT (-1.7228 38.9186) +## 4 n3 "Bilbilis" POINT (-1.6083 41.3766) +## 5 n4 "Scallabis" POINT (-8.6871 39.2362) +## 6 n5 "Mercablum/Merifabion" POINT (-6.0886 36.2765) +## 7 n6 "Valentia (Hispania Tarraconensis) (1)" POINT (-0.3755 39.4758) +## 8 n7 "Italica" POINT (-6.0449 37.4411) +## 9 n8 "Acci/Col. Iulia Gemella" POINT (-3.1346 37.3003) +## 10 n9 "Toletum" POINT (-4.0245 39.8567) +``` + +When working with geographic data, it is also sometimes useful to plot directly on top of some sort of base map. There are many options for this but one of the most convenient is to use the `sf` and `ggmap` packages to directly download the relevant base map layer and plot directly on top of it. This first requires converting points to latitude and longitude in decimal degrees if they are not already in that format. See the details on the [sf package](https://r-spatial.github.io/sf/) and [ggmap package](https://github.com/dkahle/ggmap) for more details. + +Here we demonstrate the use of the `ggmap` and the `get_stadiamap` function which requires a bit of additional explanation. This function automatically retrieves a background map for you using a few arguments: + +* **`bbox`** - the bounding box which represents the decimal degrees longitude and latitude coordinates of the lower left and upper right area you wish to map. +* **`maptype`** - a name that indicates the style of map to use ([check here for options](https://rdrr.io/github/dkahle/ggmap/man/get_stadiamap.html)). +* **`zoom`** - a variable denoting the detail or zoom level to be retrieved. Higher number give more detail but take longer to detail. + +As of early 2024 the `get_stadiamap` function also requires that you sign up for an account at [stadiamaps.com](https://stadiamaps.com). This account is free and allows you to download a large number of background maps in R per month (likely FAR more than an individual would ever use). There are a few setup steps required to get this to work. You can follow the steps below or [click here for a YouTube video outlining steps 1 thorugh 3 below](https://www.youtube-nocookie.com/embed/6jUSyI6x3xg). + +1) First, you need to sign up for a free account at Stadiamaps. + +2) Once you sign in, you will be asked to create a Property Name, designating where you will be using data. You can simply call it "R analysis" or anything you'd like. + +3) Once you create this property you'll be able to assign an API key to it by clicking the "Add API" button. + +4) Now you simply need to let R know your API to allow map download access. In order to do this copy the API key that is visible on the stadiamaps page from the property you created and then run the following line of code adding your actual API key in the place of [YOUR KEY HERE] + + + +```r +library(ggmap) +activate(key="YOUR KEY HERE") +``` + + +Note, for the ease of demonstration, in the remainder of this online guide we pre-download the maps and provide them as a file instead of using the `get_stadiamap` function. + + + +Now you're ready to run code that can download the stadia map backgrounds automatically: + + + +```r +library(ggmap) +if (nzchar(Sys.getenv("STADIAMAPS_API_KEY"))) { + map <- get_stadiamap(bbox = c(-9.5, 36, 3, 43.8), + maptype = "stamen_terrain_background", + zoom = 6) +} else { + load("data/road_base.Rdata") + map <- my_map +} +ggmap(map) +``` + + + +## Example Data{#ExampleData} + +For the initial examples in this section we will use the Roman Road data from the Iberian Peninsula. This data set consists of a [csv file of a set of Roman settlements](data/Hispania_nodes.csv) and a [csv file of an edge list](data/Hispania_roads) defining connections among those settlements in terms of roads. + +First we map the basic road network. We have commented the code below to explain what is happening at each stage. + + +```r +library(igraph) +library(ggmap) +library(sf) +library(ggplot2) + +# Read in edge list and node location data and covert to network object +edges1 <- read.csv("data/Hispania_roads.csv", header = TRUE) +nodes <- read.csv("data/Hispania_nodes.csv", header = TRUE) +road_net <- + graph_from_edgelist(as.matrix(edges1[, 1:2]), directed = FALSE) + +# Convert attribute location data to sf coordinates +locations_sf <- + st_as_sf(nodes, coords = c("long", "lat"), crs = 4326) +# We also create a simple set of xy coordinates as this is used +# by the geom_point function +xy <- data.frame(x = nodes$long, y = nodes$lat) + +# Extract edge list from network object +edgelist <- igraph::as_edgelist(road_net) + +# Create data frame of beginning and ending points of edges +edges <- as.data.frame(matrix(NA, nrow(edgelist), 4)) +colnames(edges) <- c("X1", "Y1", "X2", "Y2") +# Iterate across each edge and assign lat and long values to +# X1, Y1, X2, and Y2 +for (i in seq_len(nrow(edgelist))) { + edges[i, ] <- c(nodes[which(nodes$Id == edgelist[i, 1]), 3], + nodes[which(nodes$Id == edgelist[i, 1]), 2], + nodes[which(nodes$Id == edgelist[i, 2]), 3], + nodes[which(nodes$Id == edgelist[i, 2]), 2]) +} + +# Download basemap data when a Stadia API key is available; otherwise +# use the bundled fallback map included with the repo. +if (nzchar(Sys.getenv("STADIAMAPS_API_KEY"))) { + my_map <- get_stadiamap(bbox = c(-9.5, 36, 3, 43.8), + maptype = "stamen_terrain_background", + zoom = 6) +} else { + load("data/road_base.Rdata") +} + +# Produce map starting with background +ggmap(my_map) + + # geom_segment plots lines by the beginning and ending + # coordinates like the edges object we created above + geom_segment( + data = edges, + aes( + x = X1, + y = Y1, + xend = X2, + yend = Y2 + ), + col = "black", + size = 1 + ) + + # plot site node locations + geom_point( + data = xy, + aes(x, y), + alpha = 0.8, + col = "black", + fill = "white", + shape = 21, + size = 2, + show.legend = FALSE + ) + + theme_void() +``` + + + +## Planar Networks and Trees{#PlanarTrees} + +### Evaluating Planarity{#EvaluatingPlanarity} + +A planar network is a network that can be drawn on a plane where the edges do not cross but instead always end in nodes. In many small networks it is relatively easy to determine whether or not a network is planar by simply viewing a network graph. In larger graphs, this can sometimes be difficult. + +
+

There is a package available for R called RBGL which is +an R implementation of something called the Boost Graph Library. This +set of routines includes many powerful tools for characterizing network +topology including planarity. This package is not, however, in the CRAN +archive where the packages we have worked with so far reside so it needs +to be installed from another archive called Bioconductor.

+
+ +In order to install the `RBGL` and `BiocManager` libraries (if required), run the following lines of code. + + +```r +if (!requireNamespace("BiocManager", quietly = TRUE)) + install.packages("BiocManager") +BiocManager::install("RBGL") +``` + +With this in place we can now preform an analysis called the Boyer-Myrvold planarity test (Boyer and Myrvold 2004). This analysis performs a set of operations on a graph structure to evaluate whether or not it can be defined as a planar graph (see publication for more details). + +Let's take a look at our Roman Road data. + + +```r +library(RBGL) +# First convert to a graphNEL object for planarity test +g <- as_graphnel(road_net) +# Implement test +boyerMyrvoldPlanarityTest(g) +``` + +``` +## [1] FALSE +``` + +This results suggests that our Roman Road data is not planar. We can plot the data to evaluate this and do see crossed edges that could not be re-positioned. + + +```r +library(ggraph) +set.seed(5364) +ggraph(road_net, layout = "kk") + + geom_edge_link() + + geom_node_point(size = 3) + + ggtitle("Network of Roman Roads") + + theme_graph() +``` + + + +Now, by way of example, we can generate a small random network that is planar and see the results of the test. Note that in the network graph that is produced the visual is not planar but could be a small number of nodes were moved. Unfortunately planar graph drawing is not currently implemented into `igraph` or other packages so you cannot automatically plot a graph as planar even if it meets the criteria of a planar graph. + + +```r +set.seed(49) +g <- erdos.renyi.game(20, 1 / 8) +set.seed(939) +plot(g) +``` + + + +```r +g <- as_graphnel(g) +boyerMyrvoldPlanarityTest(g) +``` + +``` +## [1] TRUE +``` + +Here is another example where the graph layout algorithm happens to produce a planar graph. + + +```r +set.seed(4957) +g <- erdos.renyi.game(20, 1 / 8) +set.seed(939) +plot(g) +``` + + + +```r +g <- as_graphnel(g) +boyerMyrvoldPlanarityTest(g) +``` + +``` +## [1] TRUE +``` + +### Defining Trees {#DefiningTrees} + +A tree is a network that is connected and acyclic. Trees contain the minimum number of edges for a set of nodes to be connected, which results in an acyclic network with some interesting properties: + +* Every edge in a tree is a bridge, in that its removal would increase the number of components. +* The number of edges in a tree is equal to the number of nodes minus one. +* There can be only one single path between every pair of nodes in a tree. + +In R using the igraph package it is possible to both generate trees and also to take an existing network and define what is called the minimum spanning tree of that graph or the minimum acyclic component. + +Let's create a simple tree using the `make_tree` function in igraph. + + +```r +tree1 <- make_tree(n = 50, children = 5, mode = "undirected") +tree1 +``` + +``` +## IGRAPH 1f3d7e8 U--- 50 49 -- Tree +## + attr: name (g/c), children (g/n), mode (g/c) +## + edges from 1f3d7e8: +## [1] 1-- 2 1-- 3 1-- 4 1-- 5 1-- 6 2-- 7 2-- 8 2-- 9 2--10 2--11 +## [11] 3--12 3--13 3--14 3--15 3--16 4--17 4--18 4--19 4--20 4--21 +## [21] 5--22 5--23 5--24 5--25 5--26 6--27 6--28 6--29 6--30 6--31 +## [31] 7--32 7--33 7--34 7--35 7--36 8--37 8--38 8--39 8--40 8--41 +## [41] 9--42 9--43 9--44 9--45 9--46 10--47 10--48 10--49 10--50 +``` + +```r +plot(tree1) +``` + + + +In the example here you can see the branch and leaf structure of the network where there are central nodes that are hubs to a number of other nodes and so on, but there are no cycles back to the previous nodes. Thus, such a tree is inherently hierarchical. In the next sub-section, we will discuss the use of minimum spanning trees. + +It is also possible to plot trees with a hierarchical network layout where nodes are arranged at levels of the hierarchy. In this case you need to specify the node or nodes that represent the first layer using the `root` call within the `ggraph` call. + + +```r +ggraph(tree1, + layout = "igraph", + algorithm = "tree", + root = 1) + + geom_edge_diagonal(edge_width = 0.5, alpha = .4) + + geom_node_text(aes(label = V(tree1)), size = 3.5) + + theme_void() +``` + + + +## Spatial Network Models {#SpatialNetworkModels} + +In Chapter 7.5 in Brughmans and Peeples (2023) we go over a series of spatial network models that provide a number of different ways of defining networks from spatial data. In this sub-section we demonstrate how to define and analyze networks using these approaches. + +### Relative Neighborhood Networks {#RelativeNeighborhoods} + +Relative neighborhood graph: a pair of nodes are connected if there are no other nodes in the area marked by the overlap of a circle around each node with a radius equal to the distance between the nodes. + +
+

The R package cccd contains functions to define relative +neighborhood networks from distance data using the rng +function. This function can either take a distance matrix object as +created above or a set of coordinates to calculate the distance within +the call. The output of this function is an igraph object. For large +graphs it is also possible to limit the search for possible neighbors to +\(k\) neighbors.

+
+ +Let's use our previously created distance matrix and plot the results. + + + +```r +library(cccd) +rng1 <- rng(nodes[, c(3, 2)]) +ggraph(rng1, layout = "kk") + + geom_edge_link() + + geom_node_point(size = 2) + + theme_graph() +``` + + + +We can also plot the results using geographic coordinates. + + +```r +ggraph(rng1, + layout = "manual", + x = nodes[, 3], + y = nodes[, 2]) + + geom_edge_link() + + geom_node_point(size = 2) + + theme_graph() +``` + + + +### Gabriel Graphs{#GabrialGraphs} + +Gabriel graph: a pair of nodes are connected in a Gabriel graph if no other nodes lie within the circular region with a diameter equal to the distance between the pair of nodes. + +Again we can use a function in the `cccd` package to define Gabriel Graph igraph objects from x and y coordinates. Let's take a look using the Roman Road data. See `?gg` for details on the options including different algorithms for calculating Gabriel Graphs. We define a Gabriel graph here and plot it using an algorithmic layout and then geographic coordinates. + + +```r +gg1 <- gg(x = nodes[, c(3, 2)]) +ggraph(gg1, layout = "stress") + + geom_edge_link() + + geom_node_point(size = 2) + + theme_graph() +``` + + + +```r +ggraph(gg1, + layout = "manual", + x = nodes[, 3], + y = nodes[, 2]) + + geom_edge_link() + + geom_node_point(size = 2) + + theme_graph() +``` + + + +### Beta Skeletons{#BetaSkeletons} + +Beta skeleton: a Gabriel graph in which the diameter of the circle is controlled by a parameter beta. + +In R the `gg` function for producing Gabriel Graphs has the procedure for beta skeletons built directly in. The argument r in the gg function controls the beta parameter. When r = 1 a traditional Gabriel graph is returned. When the parameter r > 1 there is a stricter definition of connection resulting in fewer ties and when r < 1 link criteria are loosened. See `?gg` for more details. + + +```r +beta_s <- gg(x = nodes[, c(3, 2)], r = 1.5) +ggraph(beta_s, + layout = "manual", + x = nodes[, 3], + y = nodes[, 2]) + + geom_edge_link() + + geom_node_point(size = 2) + + theme_graph() +``` + + + +### Minimum Spanning Trees{#MinSpanningTrees} + +Minimum spanning tree: in a set of nodes in the Euclidean plane, edges are created between pairs of nodes to form a tree where each node can be reached by each other node, such that the sum of the Euclidean edge lengths is less than the sum for any other spanning tree. + +Perhaps the most common use-case for trees in archaeological networks is to define the minimum spanning tree of a given graph or the minimum set of nodes and edges required for a fully connected graph. The `igraph` package has a function called `mst` that defines the minimum spanning tree for a given graph. Let's try this with the Roman Road and then plot it as a node-link diagram and a map. + + +```r +mst_net <- igraph::mst(road_net) +set.seed(4643) +ggraph(mst_net, layout = "kk") + + geom_edge_link() + + geom_node_point(size = 4) + + theme_graph() +``` + + + +```r +# Extract edge list from network object +edgelist <- igraph::as_edgelist(mst_net) +# Create data frame of beginning and ending points of edges +edges <- as.data.frame(matrix(NA, nrow(edgelist), 4)) +colnames(edges) <- c("X1", "Y1", "X2", "Y2") +for (i in seq_len(nrow(edgelist))) { + edges[i, ] <- c(nodes[which(nodes$Id == edgelist[i, 1]), 3], + nodes[which(nodes$Id == edgelist[i, 1]), 2], + nodes[which(nodes$Id == edgelist[i, 2]), 3], + nodes[which(nodes$Id == edgelist[i, 2]), 2]) +} +ggmap(my_map) + + geom_segment( + data = edges, + aes( + x = X1, + y = Y1, + xend = X2, + yend = Y2 + ), + col = "black", + size = 1 + ) + + geom_point( + data = nodes[, c(3, 2)], + aes(long, lat), + alpha = 0.8, + col = "black", + fill = "white", + shape = 21, + size = 1.5, + show.legend = FALSE + ) + + theme_void() +``` + + + +Note that minimum spanning trees can also be used for weighted graphs such that weighted connections will be preferred in defining tree structure. See `?mst` for more details. + +### Delaunay Triangulation{#DelaunayTri} + +Delaunay triangulation: a pair of nodes are connected by an edge if and only if their corresponding regions in a Voronoi diagram share a side. + +Voronoi diagram or Thiessen polygons: for each node in a set of nodes in a Euclidean plane, a region is created covering the area that is closer or equidistant to that node than it is to any other node in the set. + +
+

The package deldir in R allows for the calculation of +Delaunay triangles with x and y coordinates as input. By default the +deldir function will define a boundary that extends +slightly beyond the xy coordinates of all points included in the +analysis. This boundary can also be specified within the call using the +rw argument. See ?deldir for more details.

+
+ +The results of the `deldir` function can be directly plotted and the output also contains coordinates necessary to integrate the results into another type of figure like a `ggmap`. Let's take a look. + + +```r +library(deldir) +dt1 <- deldir(nodes[, 3], nodes[, 2]) +plot(dt1) +``` + + + +```r +# Extract Voronoi polygons for plotting +mapdat <- as.data.frame(dt1$dirsgs) +# Extract network for plotting +mapdat2 <- as.data.frame(dt1$delsgs) +ggmap(my_map) + + geom_segment( + data = mapdat, + aes( + x = x1, + y = y1, + xend = x2, + yend = y2 + ), + col = "black", + size = 1 + ) + + geom_segment( + data = mapdat2, + aes( + x = x1, + y = y1, + xend = x2, + yend = y2 + ), + col = "red", + size = 1 + ) + + geom_point( + data = nodes, + aes(long, lat), + alpha = 0.8, + col = "black", + fill = "white", + shape = 21, + size = 3, + show.legend = FALSE + ) + + theme_void() +``` + + + +### K-nearest Neighbors {#KNN} + +K-nearest neighbor network: each node is connected to K other nodes closest to it. + +The `cccd` package has a routine that allows for the calculation of K-nearest neighbor graphs from geographic coordinates or a precomputed distance matrix. In this example we use the Roman Road data and calculate K=1 and K=6 nearest neighbor networks and plot the both simultaneously. + + +```r +# Calculate k=1 nearest neighbor graph +nn1 <- nng(x = nodes[, c(3, 2)], k = 1) +# Calculate k=6 nearest neighbor graph +nn6 <- nng(x = nodes[, c(3, 2)], k = 6) +el1 <- as.data.frame( + rbind(cbind(igraph::as_edgelist(nn6), + rep("K=6", nrow(igraph::as_edgelist(nn1)) + )), + cbind(igraph::as_edgelist(nn1), + rep("K=1", nrow(igraph::as_edgelist(nn1)) + )))) +colnames(el1) <- c("from", "to", "K") +g <- graph_from_data_frame(el1) +# Plot both graphs +ggraph(g, layout = "manual", + x = nodes[, 3], y = nodes[, 2]) + + geom_edge_link(aes(color = factor(K)), width = 1.5) + + geom_node_point(size = 2) + + labs(edge_color = "K") + + theme_graph() +``` + + + +### Maximum Distance Networks{#MaxDist} + +Maximum distance network: each node is connected to all other nodes at a distance closer than or equal to a threshold value. In order to define a maximum distance network we simply need to define a threshold distance and define all nodes greater than that distance as unconnected and nodes within that distance as connected. This can be done in base R using the dist function we used above. + +
+

Since the coordinates we are using here are in decimal degrees we +need to calculate distances based on “great circles” across the globe +rather than Euclidean distances on a projected plane. There is a +function called distm in the geosphere package +that allows us to do this. If you are working with projected data, you +can simply use the dist function in the place of +distm like the example below.

+
+ +Next, in order to define a minimum distance network we simply binarize this matrix. We can do this using the `event2dichot` function within the `statnet` package and easily create an R network objects. Let's try it out with the Roman Road data for thresholds of 100,000 and 250,000 meters. + + +```r +library(statnet) +``` + +``` +## Installed ReposVer Built +## ergm "4.6.0" "4.12.0" "4.2.3" +## ergm.count "4.1.1" "4.1.3" "4.2.3" +## ndtv "0.13.3" "0.13.4" "4.2.3" +## network "1.18.2" "1.20.0" "4.2.3" +## networkDynamic "0.11.4" "0.12.0" "4.2.3" +## sna "2.7-2" "2.8" "4.2.3" +## statnet.common "4.9.0" "4.13.0" "4.2.3" +## tergm "4.2.0" "4.2.2" "4.2.3" +## tsna "0.3.5" "0.3.6" "4.2.3" +``` + +```r +library(geosphere) +d1 <- distm(nodes[, c(3, 2)]) +# Note we use the leq=TRUE argument here as we want nodes less than +# the threshold to count. +net100 <- network(event2dichot( + d1, + method = "absolute", + thresh = 100000, + leq = TRUE +), +directed = FALSE) +net250 <- network(event2dichot( + d1, + method = "absolute", + thresh = 250000, + leq = TRUE +), +directed = FALSE) +# Plot 100 Km network +ggraph(net100, + layout = "manual", + x = nodes[, 3], + y = nodes[, 2]) + + geom_edge_link() + + geom_node_point(size = 2) + + theme_graph() +``` + + + +```r +# Plot 250 Km network +ggraph(net250, + layout = "manual", + x = nodes[, 3], + y = nodes[, 2]) + + geom_edge_link() + + geom_node_point(size = 2) + + theme_graph() +``` + + + +## Case Studies{#SpaceCaseStudies} + +### Proximity of Iron Age sites in Southern Spain{#IronAgeSpain} + +The first case study in Chapter 7 of Brughmans and Peeples (2023) is an example of several of the methods for defining networks using spatial data outlined above using the locations of 86 sites in the Guadalquivir river valley in Southern Spain. In the code chunks below, we replicate the analyses presented in the book. + +First we read in the data which represents site location information in lat/long decimal degrees. + + +```r +guad <- read.csv("data/Guadalquivir.csv", header = TRUE) +``` + +Next we create a distance matrix based on the decimal degrees locations using the "distm" function. + + +```r +library(geosphere) +g_dist1 <- as.matrix(distm(guad[, c(2, 3)])) +g_dist1[1:4, 1:4] +``` + +``` +## [,1] [,2] [,3] [,4] +## [1,] 0.00 69995.82 42265.58 51296.53 +## [2,] 69995.82 0.00 28240.50 29202.84 +## [3,] 42265.58 28240.50 0.00 23692.10 +## [4,] 51296.53 29202.84 23692.10 0.00 +``` + +From here we can create maximum distance networks at both the 10km and 18km distance and plot it using the geographic location of nodes for node placement. + + +```r +library(intergraph) +# Note we use the leq=TRUE argument here as we want nodes +# less than the threshold to count. +net10 <- asIgraph(network( + event2dichot( + g_dist1, + method = "absolute", + thresh = 10000, + leq = TRUE + ), + directed = FALSE +)) +net18 <- asIgraph(network( + event2dichot( + g_dist1, + method = "absolute", + thresh = 18000, + leq = TRUE + ), + directed = FALSE +)) +g10_deg <- as.data.frame(igraph::degree(net10)) +colnames(g10_deg) <- "degree" +g18_deg <- as.data.frame(igraph::degree(net18)) +colnames(g18_deg) <- "degree" +# Plot histogram of degree for 10km network +h10 <- ggplot(data = g10_deg) + + geom_histogram(aes(x = degree), bins = 15) +# Plot histogram of degree for 18km network +h18 <- ggplot(data = g18_deg) + + geom_histogram(aes(x = degree), bins = 15) +# Plot 10 Km network +g10 <- ggraph(net10, + layout = "manual", + x = guad[, 2], + y = guad[, 3]) + + geom_edge_link() + + geom_node_point(size = 2) + + theme_graph() +# Plot 18 Km network +g18 <- ggraph(net18, + layout = "manual", + x = guad[, 2], + y = guad[, 3]) + + geom_edge_link() + + geom_node_point(size = 2) + + theme_graph() +g18 +``` + + + +
+

If we want to combine the degree distribution plot and the network +into the same frame, we can use the inset_element function +in the patchwork package. This function lets us place one +plot inside another in the ggplot2 format.

+
+ + + +```r +library(patchwork) +plot_a <- g10 + inset_element( + h10, + left = 0, + bottom = 0.7, + right = 0.25, + top = 0.99 +) +plot_b <- g18 + inset_element( + h18, + left = 0, + bottom = 0.7, + right = 0.25, + top = 0.99 +) +plot_a +``` + + + +```r +plot_b +``` + + + +Next, we calculate a relative neighborhood graph for the site locations and plot it with nodes positioned in geographic space. + + +```r +rng1 <- rng(guad[, 2:3]) +g_rng <- ggraph(rng1, + layout = "manual", + x = guad[, 2], + y = guad[, 3]) + + geom_edge_link() + + geom_node_point(size = 2) + + theme_graph() +g_rng_deg <- as.data.frame(igraph::degree(rng1)) +colnames(g_rng_deg) <- "degree" +# Plot histogram of degree for relative neighborhood network +h_rng <- ggplot(data = g_rng_deg) + + geom_histogram(aes(x = degree), bins = 3) +plot_c <- g_rng + inset_element( + h_rng, + left = 0, + bottom = 0.7, + right = 0.25, + top = 0.99 +) +plot_c +``` + + + +The chunk of code below then calculates and plots the Gabrial graph with the associated degree distribution plot. + + +```r +gg1 <- gg(x = guad[, 2:3]) +g_gg <- ggraph(gg1, + layout = "manual", + x = guad[, 2], + y = guad[, 3]) + + geom_edge_link() + + geom_node_point(size = 2) + + theme_graph() +g_gg_deg <- as.data.frame(igraph::degree(gg1)) +colnames(g_gg_deg) <- "degree" +# Plot histogram of degree for relative neighborhood network +h_gg <- ggplot(data = g_gg_deg) + + geom_histogram(aes(x = degree), bins = 5) +plot_d <- g_gg + inset_element( + h_gg, + left = 0, + bottom = 0.7, + right = 0.25, + top = 0.99 +) +plot_d +``` + + + +Next, we'll plot the K-nearest neighbors graphs for k= 2, 3, 4, and 6 with the associated degree distribution for each. + + +```r +# Calculate k=2,3,4, and 6 nearest neighbor graphs +nn2 <- nng(x = guad[, 2:3], k = 2) +nn3 <- nng(x = guad[, 2:3], k = 3) +nn4 <- nng(x = guad[, 2:3], k = 4) +nn6 <- nng(x = guad[, 2:3], k = 6) +# Initialize network graph for each k value +g_nn2 <- ggraph(nn2, + layout = "manual", + x = guad[, 2], + y = guad[, 3]) + + geom_edge_link() + + geom_node_point(size = 2) + + theme_graph() +g_nn3 <- ggraph(nn3, + layout = "manual", + x = guad[, 2], + y = guad[, 3]) + + geom_edge_link() + + geom_node_point(size = 2) + + theme_graph() +g_nn4 <- ggraph(nn4, + layout = "manual", + x = guad[, 2], + y = guad[, 3]) + + geom_edge_link() + + geom_node_point(size = 2) + + theme_graph() +g_nn6 <- ggraph(nn6, + layout = "manual", + x = guad[, 2], + y = guad[, 3]) + + geom_edge_link() + + geom_node_point(size = 2) + + theme_graph() +# Set up data frames of degree distribution for each network +nn2_deg <- as.data.frame(igraph::degree(nn2)) +colnames(nn2_deg) <- "degree" +nn3_deg <- as.data.frame(igraph::degree(nn3)) +colnames(nn3_deg) <- "degree" +nn4_deg <- as.data.frame(igraph::degree(nn4)) +colnames(nn4_deg) <- "degree" +nn6_deg <- as.data.frame(igraph::degree(nn6)) +colnames(nn6_deg) <- "degree" +# Initialize histogram plot for each degree distribution +h_nn2 <- ggplot(data = nn2_deg) + + geom_histogram(aes(x = degree), bins = 5) + + scale_x_continuous(limits = c(0, max(nn2_deg))) +h_nn3 <- ggplot(data = nn3_deg) + + geom_histogram(aes(x = degree), bins = 6) + + scale_x_continuous(limits = c(0, max(nn3_deg))) +h_nn4 <- ggplot(data = nn4_deg) + + geom_histogram(aes(x = degree), bins = 6) + + scale_x_continuous(limits = c(0, max(nn4_deg))) +h_nn6 <- ggplot(data = nn6_deg) + + geom_histogram(aes(x = degree), bins = 5) + + scale_x_continuous(limits = c(0, max(nn6_deg))) +plot_a <- g_nn2 + inset_element( + h_nn2, + left = 0, + bottom = 0.7, + right = 0.25, + top = 0.99 +) +plot_b <- g_nn3 + inset_element( + h_nn3, + left = 0, + bottom = 0.7, + right = 0.25, + top = 0.99 +) +plot_c <- g_nn4 + inset_element( + h_nn4, + left = 0, + bottom = 0.7, + right = 0.25, + top = 0.99 +) +plot_d <- g_nn6 + inset_element( + h_nn6, + left = 0, + bottom = 0.7, + right = 0.25, + top = 0.99 +) +plot_a +``` + + + +```r +plot_b +``` + + + +```r +plot_c +``` + + + +```r +plot_d +``` + + + + +### Networks in Space in the U.S. Southwest{#SpaceSW} + +The second case study in Chapter 7 of Brughmans and Peeples (2023) provides an example of how we can use spatial network methods to analyze material cultural network data. We use the Chaco World data here and you can download the [map data](data/map.RData), [the site attribute data](data/AD1050attr.csv), and [the ceramic frequency data](data/AD1050cer.csv) to follow along. + +The first analysis explores the degree to which similarities in ceramics (in terms of Brainerd-Robinson similarity based on wares) can be explained by spatial distance. To do this we simply define a ceramic similarity matrix, a Euclidean distance matrix, and the fit a model using distance to explain ceramic similarity using a general additive model (`gam`) approach. The `gam` function we use here is in the `mgcv` package. Note that the object `dmat` is created using the `dist` function as the data we started with are already projected site locations using UTM coordinates. + + +```r +library(mgcv) +load("data/map.RData") +attr <- read.csv("data/AD1050attr.csv", row.names = 1) +cer <- read.csv("data/AD1050cer.csv", + header = T, + row.names = 1) +sim <- + (2 - as.matrix(vegan::vegdist(prop.table( + as.matrix(cer), 1), + method = "manhattan"))) / 2 +dmat <- as.matrix(dist(attr[, 9:10])) +fit <- gam(as.vector(sim) ~ as.vector(dmat)) +summary(fit) +``` + +``` +## +## Family: gaussian +## Link function: identity +## +## Formula: +## as.vector(sim) ~ as.vector(dmat) +## +## Parametric coefficients: +## Estimate Std. Error t value Pr(>|t|) +## (Intercept) 7.979e-01 2.547e-03 313.3 <2e-16 *** +## as.vector(dmat) -2.487e-06 1.448e-08 -171.8 <2e-16 *** +## --- +## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 +## +## +## R-sq.(adj) = 0.372 Deviance explained = 37.2% +## GCV = 0.082702 Scale est. = 0.082699 n = 49729 +``` + +As these results show and as described in the book, spatial distance is a statistically significant predictor of ceramic similarity and distance appear to explain about 37.2% of the variation in ceramic similarity. + +The next analysis presented the book creates a series of minimum distance networks from 36Kms all the way out to nearly 400Kms in concentric days travel (36Kms is about one day of travel on foot) and explore the proportion of variance explained by networks constrained on each distance. + + +```r +# Create a sequence of distances from 36km to 400kms by concentric +# days travel on foot +kms <- seq(36000, 400000, by = 36000) +# Define minimum distance networks for each item in "kms" and the +# calculate variance explained +temp_out <- NULL +for (i in seq_len(length(kms))) { + dmat_temp <- dmat + dmat_temp[dmat > kms[i]] <- 0 + dmat_temp[dmat_temp > 0] <- 1 + # Calculate gam model and output r^2 value + temp <- gam(as.vector(sim[lower.tri(sim)]) ~ + as.vector(dmat_temp[lower.tri(dmat_temp)])) + temp_out[i] <- summary(temp)$r.sq +} +# Create data frame of output +dat <- as.data.frame(cbind(kms / 1000, temp_out)) +colnames(dat) <- c("Dist", "Cor") +library(ggplot2) +# Plot the results +ggplot(data = dat) + + geom_line(aes(x = Dist, y = Cor)) + + geom_point(aes(x = Dist, y = Cor), size = 3) + + xlab("Maximum Distance Network Threshold (Km)") + + ylab("Proportion of Variance Explained") + + theme_bw() + + theme( + axis.text.x = element_text(size = rel(1.5)), + axis.text.y = element_text(size = rel(1.5)), + axis.title.x = element_text(size = rel(1.5)), + axis.title.y = element_text(size = rel(1.5)) + ) +``` + + + + +Finally, let's recreate figure 7.8 from the book to display the 36km minimum distance network for the Chaco region ca. AD 1050-1100. This follows the same basic format for plotting minimum distance networks we defined above. + + +```r +d36 <- as.matrix(dist(attr[, 9:10])) +d36[d36 < 36001] <- 1 +d36[d36 > 1] <- 0 +g36_net <- graph_from_adjacency_matrix(d36, mode = "undirected") +locations_sf <- st_as_sf(attr, + coords = c("EASTING", "NORTHING"), + crs = 26912) +z <- st_transform(locations_sf, crs = 4326) +coord1 <- do.call(rbind, st_geometry(z)) %>% + tibble::as_tibble() %>% + setNames(c("lon", "lat")) +xy <- as.data.frame(cbind(attr$SWSN_Site, coord1)) +colnames(xy) <- c("site", "x", "y") +if (nzchar(Sys.getenv("STADIAMAPS_API_KEY"))) { + base <- get_stadiamap( + bbox = c(-110.75, 33.5, -107, 38), + zoom = 8, + maptype = "stamen_terrain_background", + color = "bw" + ) + base_plot <- ggmap(base, darken = 0.15) +} else { + base_plot <- ggplot() + + coord_quickmap(xlim = c(-110.75, -107), ylim = c(33.5, 38)) + + theme_void() + + theme( + panel.background = element_rect(fill = "grey92", color = NA), + plot.background = element_rect(fill = "white", color = NA) + ) +} +# Extract edge list from network object +edgelist <- igraph::as_edgelist(g36_net) +# Create data frame of beginning and ending points of edges +edges <- as.data.frame(matrix(NA, nrow(edgelist), 4)) +colnames(edges) <- c("X1", "Y1", "X2", "Y2") +for (i in seq_len(nrow(edgelist))) { + edges[i, ] <- c(xy[which(xy$site == edgelist[i, 1]), 2], + xy[which(xy$site == edgelist[i, 1]), 3], + xy[which(xy$site == edgelist[i, 2]), 2], + xy[which(xy$site == edgelist[i, 2]), 3]) +} +figure7_8 <- base_plot + + geom_segment( + data = edges, + aes( + x = X1, + y = Y1, + xend = X2, + yend = Y2 + ), + col = "white", + size = 0.10, + show.legend = FALSE + ) + + geom_point( + data = xy, + aes(x, y), + alpha = 0.65, + size = 1, + col = "red", + show.legend = FALSE + ) + + theme_void() +figure7_8 +``` + + diff --git a/07-ergm.md b/07-ergm.md new file mode 100644 index 0000000..eda1b15 --- /dev/null +++ b/07-ergm.md @@ -0,0 +1,788 @@ +# (PART) **PART III: Going Beyond the Book**{-} + +# Exponential Random Graph Models{#ERGM} + +![](images/image_break.png){width=100%} + +Exponential Random Graph Models (ERGM; typically pronounced "UR-gum") are a class of statistical models designed to help represent, evaluate, and simulate ideas about network generating processes and structural properties (for a good introductions to the method see Lusher et al. 2013; and for archaeological cases see Amati et al. 2020; Brughmans et al. 2014; Wang and Marwick 2021). These models allow us to formally represent our theories about how particular patterns of relationships (such as paths of a given length or triads of a specific configuration) or associations (such as mutuality or connections among nodes that share an attribute) emerge and persist in our networks. Further ERGMs help us evaluate how well such theories account for our observed network data. Specifically, an ERGM can be used to generate large numbers of networks in a random process targeted towards particular configurations and associations that represent our theories of interest. We can then compare those simulated networks to our observed network to generate perspectives on the plausibility of our theory. Essentially, ERGMs help us determine how the local tendencies in network formation generate the global properties and structures of our networks. + +In many ways, ERGMs are similar to logistic regression models where we predict the presence or absence of ties between pairs of nodes with edge formation modeled as dependent on network structure and properties (e.g., density, transitivity, centralization, etc.). Such models help us assess the probability that the observed network is a product of specified properties or generative processes that may be more or less likely to occur than we would expect by chance in a random network. + +The details of ERGMs and the underlying mathematics are beyond the scope of this document, but we present a brief overview of the highlights based heavily on [a workshop on ERGM by the `statnet` team](http://statnet.org/Workshops/ergm_tutorial.html) (Krivitsky et al. 2021). See that workshop for more details. + +The general model form for an ERGM can be written as: + +$$P(Y=y) = \frac{\text{exp}(\theta' g(y))}{k(\theta)}$$ + +where + +* $P(Y=y)$ is the probability that the network will take a given state $y$ among random possibilities $Y$. +* $g(y)$ is the set of model ERGM terms considered. These are essential the covariates in the model. +* $\theta$ is the set of coefficients for model terms. +* $k(\theta)$ is a normalizing constant defined as numerator summed overall all possible networks constrained on the node set $y$. In other words, all possible network configurations that could exist with the given node set. + +The general form for an ERGM expressed in terms of the entire network as we see above can also be expressed in terms of the conditional log odds of an edge existing between any two nodes as follows: + +$$\text{logit}(Y_{ij}=y_{ij}) = \theta'\delta(y_{ij})$$ + +where + +* $Y_{ij}$ is the random variable for the state of the edge (present or absent) for a given pair of nodes $i$ and $j$ and $y_{ij}$ is the observed state. +* $\delta (y_{ij})$ is the change statistic representing how $g(y)$ (the state of the graph and associated terms) changes if the edge between $i$ and $j$ is active or not. +* $\theta$ describes the contribution of a term to the log odds of an individual edge between $i$ and $j$ conditioned on the state of all other edges remaining the same (we explain this in more detail below with examples). + +The coefficient estimates in ERGM models are returned in log odds which indicates the change in the likelihood of an edge per unit change in the given predictor (this is where the "change statistic") comes in. For example a coefficient estimate $\theta$ of 1.5 for a given term would indicate that the likelihood of an edge is 1.5 times higher for every change of that term by 1 unit. Conversely, an coefficient estimate for a term of -5.5 would suggest that the likelihood of an edge is 5.5 times *less* likely for every unit change of the term. In general, positive coefficients suggest that a given network feature denoted by the term is more common than we would expect by chance and a negative value suggests it is less common than we would expect by chance (given the constraints placed on network construction). The magnitude of the coefficients further provides an indication of how much more or fewer of a given features we see than we would expect. We explain how this works in more detail in the examples below. + +
+

The log odds is the logarithm of the odds ratio. The odds ratio +refers to the probability that an event occurs divided by the +probability that an event does not occur (1 minus the probability that +it occurs). This can be written formally as:

+

\(\text{log}(A) = \text{log} +\frac{(P(A))}{(1-P(A))}\)

+

where

+ +

Negative log odds values indicate that probability of an event +occurring is < 0.5 and positive log odds values indicate that the +probability of an event occurring is > 0.5. Log odds will be exactly +0 when the probability of an event occurring is 0.5.

+
+ + +## ERGMs in R {#ERGMsInR} + +In general, the analysis of ERGMs in R is conducted in three basic steps: + +* First, we asses the general properties of interest in our network using exploratory network statistics described [in the Exploratory Network Analysis section of this document](#Exploratory). +* Next, we define our network terms of interest and fit one or more ERGMs to our observed network and assess the results. +* Finally, we assess the goodness of fit of our models and assess the diagnostic statistics for our model generating processes. + +If all goes well in the steps above, we can then evaluate our network theory or property of interest in relation to the ERGM that we created. + +
+

The statnet suite of packages includes a package called +ergm that facilitates the analysis of ERGMs in R and an +additional package called tergm that provides terms and +methods for analyzing temporal networks using ERGMs. Networks need to be +in the network format to be analysed using the +statnet suite of packages.

+
+ +Let's initialize our `statnet` suite to get started: + + +```r +library(statnet) # initialize statnet library +``` + +``` +## Installed ReposVer Built +## ergm "4.6.0" "4.12.0" "4.2.3" +## ergm.count "4.1.1" "4.1.3" "4.2.3" +## ndtv "0.13.3" "0.13.4" "4.2.3" +## network "1.18.2" "1.20.0" "4.2.3" +## networkDynamic "0.11.4" "0.12.0" "4.2.3" +## sna "2.7-2" "2.8" "4.2.3" +## statnet.common "4.9.0" "4.13.0" "4.2.3" +## tergm "4.2.0" "4.2.2" "4.2.3" +## tsna "0.3.5" "0.3.6" "4.2.3" +``` + +In many ways it is easiest to describe what ERGMs do and how they work by example. In the next sections we provide a couple of archaeological examples that highlight some of the ways ERGMs have or could be used in archaeology. We further provide additional resources for taking these methods further. + +## Cranborne Chase Visibility Network Example{#CranborneChase} + +We start with an example that was described briefly in the Brughmans and Peeples (2023) book in Chapter 4, but not covered in detail. Specifically, we explore the potential generative processes involved in the development of the intervisibility network among long barrows in the Cranborne Chase area in southern England. As this example is only briefly described in the Brughmans and Peeples (2023) book, you may also want to read and follow along with the original article where that analyses first appeared ([Brughmans and Brandes 2017](https://www.frontiersin.org/articles/10.3389/fdigh.2017.00017/)). + +Briefly, the network consists of a set of nodes which represent long barrows and edges among them which represent ground-truthed ties of intervisbility between pairs of barrows. The original data came from work by Chris Tilley (1994). These data were used by Brughmans and Brandes (2017) to formally test the notion put forth by Tilley that highly visible barrows "attracted" others over time. In network terms this could be characterized as a "preferential attachment" process. Brughmans and Brandes created an ERGM model with particular properties drawn from Tilley's theoretical model of network development and found that networks simulated with those properties using ERGMs had substantially similar properties to the observed network. Based on this, they considered Tilley's theoretical model plausible. + +![Photograph of a long barrow at Cranborne Chase. [Original image by Jim Champion: CC 3.0](https://commons.wikimedia.org/wiki/File:Gussage_down_long_barrow.jpg)](images/long_barrow.jpg){width=100%} + +The original ERGM analysis published by Brughmans and Brandes was conducted in a Java program designed for ERGM analysis called [PNet](http://www.melnet.org.au/pnet). Here we replicate some of their results and a few additional analyses using slightly different methods and assumptions in R by way of demonstration. Our results differ slightly from the published results because of the randomness inherent in fitting ERGMs but all coefficient retain the same sign and magnitude suggesting a good replication of the most important results. + +### Assessments of Network Properties{#NetProperties} + +Let's start by bringing in our Cranborne Chase network data (as a `network` object) and looking at the general properties of the network object. To follow along you can download the data [by clicking here](data/cranborne.Rdata). + + +```r +load("data/cranborne.Rdata") +cranborne +``` + +``` +## Network attributes: +## vertices = 32 +## directed = FALSE +## hyper = FALSE +## loops = FALSE +## multiple = FALSE +## bipartite = FALSE +## total edges= 49 +## missing edges= 0 +## non-missing edges= 49 +## +## Vertex attribute names: +## vertex.names +## +## No edge attributes +``` + +This network is an undirected, unweighted network object with 32 nodes and 49 edges. Let's look at a few properties of the network including density, mean degree, degree centralization, and number of isolates. + + +```r +sna::gden(cranborne) # density +``` + +``` +## [1] 0.09879032 +``` + +```r +mean(sna::degree(cranborne)) # mean degree +``` + +``` +## [1] 6.125 +``` + +```r +sna::centralization(cranborne, g = 1, degree) # degree centralization +``` + +``` +## [1] 0.2043011 +``` + +```r +length(sna::isolates(cranborne)) # number of isolates +``` + +``` +## [1] 3 +``` + +This is a fairly sparse network with few isolates and a low degree centralization. + +Let's plot it with nodes scaled by degree: + + +```r +set.seed(4367) +plot(cranborne, vertex.cex = (sna::degree(cranborne) / 4) + 1) +``` + + + +This network has 3 components and a few isolates. In general many of the nodes have similar degree centrality values but there are a few nodes which appear to have higher degree. We can look at a histogram of degree centrality to further assess the distribution. + + +```r +hist(sna::degree(cranborne), + breaks = 10, + main = "Degree Distribution", + xlab = "Degree Centrality") +``` + + + +### Fitting Models with `ergm`{#FitModels} + +Now that we've explored some of the basic properties of our network, the next step is to begin to fit ERGMs to our observed network. The first thing we are going to do is fit a very simple model with only one term. In the `ergm` package "terms" refer to the specific constraints placed on our randomly generated networks (see `?ergm.terms` for a list of the many built-in terms). The most basic term that is included in many models is `edges` which simply refers to the number of edges in a network. An ERGM with a single `edges` term is conceptually equivalent to a typical GLM regression model where the only predictor is the intercept. + +In the chunk of code below we see the form that `ergm` model objects take in R. Inside the `ergm` call we have our network on the left hand size `cranborne` followed by `~` and then followed by `edges` which is a built-in "term" in the `ergm` package. As we will see below, when we use multiple terms we separate them by a `+`. Once we have crated our `ergm` model object we then explore the output using the `summary()` function. + + +```r +mod_null <- ergm(cranborne ~ edges) +summary(mod_null) +``` + +``` +## Call: +## ergm(formula = cranborne ~ edges) +## +## Maximum Likelihood Results: +## +## Estimate Std. Error MCMC % z value Pr(>|z|) +## edges -2.2107 0.1505 0 -14.69 <1e-04 *** +## --- +## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 +## +## Null Deviance: 687.6 on 496 degrees of freedom +## Residual Deviance: 319.8 on 495 degrees of freedom +## +## AIC: 321.8 BIC: 326 (Smaller is better. MC Std. Err. = 0) +``` + +In the output above there are a number of important features that need explanation. + +The summary output includes the call/model formula we used followed by the Maximum Likelihood Results. The output we will focus on here includes the estimates of each model term, the standard error of the estimates, and the p-value associated with that term: + +* First, in the example here, we get an estimate of `edges` as `-2.2107` which is the conditional log odds of two nodes having an edge between them (explained further below) +* Next, we have the standard error of the coefficient estimate. +* And we also have "Pr(>|z|) which is the p-value associated with a particular term. The p-value here is calculated as a function of the relative size of the coefficient estimate and the standard error. + +What the estimate (and associated standard error and p-value) indicates is how much a change in the term by one unit changes the likelihood that a particular edge is present. In this case, a change by one unit in the term `edges` refers to the addition of exactly 1 edge to the network ($\delta(g(y)) = 1$) so the coefficient is an estimate of how much the addition of 1 edge to the network changes the likelihood of any particular edge: + +$$\begin{aligned} +\text{logit}(p(y)) & = \theta \times \delta(g(y))\\ +& = -2.2107 \times \text{change in the number of edges}\\ +& = -2.2107 \times 1\\ +& = -2.2107 +\end{aligned}$$ + +So in this example, the likelihood of an edge between two particular nodes is `2.2107` times *less* likely for every additional increase in the number of network by 1 edge in the network as a whole. So for every edge added the probability that a particular edge is present decreases. What this negative coefficient means is that an edge is more likely absent than present (and a positive coefficient would suggest the opposite) and thus, if we add an edge elsewhere in the network it is even less likely that our target edge will be active. We can calculate the probability of that an edge is present by taking the inverse logit of $\theta$: + + +```r +exp(-2.2107) / (1 + exp(-2.2107)) +``` + +``` +## [1] 0.09879373 +``` + +As we would expect, this number is very close to the density of the network which is what the `edges` term uses as a constraint (number of edges is a function of density): + + +```r +sna::gden(cranborne) +``` + +``` +## [1] 0.09879032 +``` + +What this indicates is that if we are trying to predict a given network state (a given set of present an absent edges) and the only information we know is the network density, the probability that a particular edge is present is roughly equal to the network density. As the coefficient is statistically significant, this means that there is a low probability (p-value) of obtaining a model with no terms at random that provides as good or better predictions of the observed than the model including the `edges` term. + +Finally we can see our model fit statistics at the bottom with [AIC (Akaike Information Criterion)](https://en.wikipedia.org/wiki/Akaike_information_criterion) and [BIC (Bayesian Information Criterion)](https://en.wikipedia.org/wiki/Bayesian_information_criterion). These are both model fit statistics that can be used to compare competing models where lower values represent better fit between the model and the data. Further, the Null deviance is a measure of how well the network was predicted by a model with no covariates vs. the residual deviance which is a measure of how well the network is predicted by a model with the covariates. Residual deviance will be lower than Null deviance and bigger gap between the two is better. In general, the absolute values of these model fit terms do not matter but rather they provide a means for comparing multiple models for predicting the same observations as we will see below. + +### Building a Model Based on Theory{#ModelTheory} + +The simple example above built an ERGM predicated on nothing but network density using the `edges` term. As outlined by Brughmans and Brandes (2017) there are specific features of the Cranborne Chase network development process theorized by Tilley which could be converted into a formal ERGM model using specific `ergm.terms`. Specifically, Tilley suggested that long barrows tended to be clustered into groups and intervisibility was a primary concern for some, but not all long barrows. Further, he suggested that long barrows tended to be clustered in sets and include straight paths where multiple barrows were visible from a single point. Finally, Tilley suggested that barrows that were already highly visible tended to attract new visibility connections through time. To capture this theory of network development in formal terms, Brughmans and Brandes (2017) create a set of terms to match Tilley's expectations. They include the following terms: + +* **`edges`** - the number of active edges: this term represents the tendency for long barrows to have visibility connections. +* **`triangle`** - the number of closed triangles: this term represents the clustering that Tilley expects in the network as networks with many closed triangles often have distinct clusters. +* **`threetrail`** - the number of paths or trails of 3 (`threepath` and `threetrail` are used equivalently in the `ergm` here) in the network: this term is meant to capture Tilley's visual pathways where multiple barrows are visible in a specific direction. +* **`altkstar`** - alternating stars: this term is used to represent certain nodes with high degree distribution representing the prominent nodes in the network generated through a process of preferential attachment. +* **`isolates`** - the number of isolates in the network: this term is here to capture the tendency for nodes to not be isolate as Tilley describes. + +Here are visual representations of these network configurations from Brughmans and Brandes (2017): + +![Network terms included in ERGM](images/BrughmansBrandes.jpg){width=60%} + +Brughmans and Brandes present two versions of the model in the article. The first excludes the `isolates` term and the second includes it. Let's replicate their results here. Note that we are using different software and terms may be defined slightly differently so our results may differ a bit from their published results. Further, ERGMs include random simulation to two runs of the same model will not return the same results unless we supply a random seed. To do that in the `ergm` call we use a `control` argument as we see below. + +Let's first go over what it is to be included in the terms. We want to first create a model with the terms `edges`, `triangle`, `threetrail`, and `altkstar`. Most of the terms can be used without further arguments but the `altkstar` term needs an additional weight parameter `lambda` and for us to define that weight parameter as fixed (see [term descriptions here](https://zalmquist.github.io/ERGM_Lab/ergm-terms.html#:~:text=ergm%20functions%20such%20as%20ergm,valued%20mode%20and%20vice%20versa.) for more details). + +
+

ERGMs can sometimes take quite a bit of time to run as they involve +generating lots of estimates of random variables using the MCMC process. +In order to control the behavior of the MCMC sampling process, we can +use the control argument within the ergm +function. In the examples here we have opted for a fairly large sample +size per chain and a relatively large interval between samples. As we +will see further below, this will help with our coefficient estimates +and model fit but the trade off is time. If you want to simply run the +models in the examples below quickly, you simply remove these three +arguments within the control.ergm function call: +MCMC.burnin, MCMC.interval, and +MCMC.samplesize.

+
+ +Let's fit the model and look at the summary. Note when you run this on your own computer you will see additional verbose output on the console as the sampling process proceeds. We have eliminated that here to avoid visual clutter: + + +```r +mod1 <- ergm( + cranborne ~ edges + triangle + threetrail + + altkstar(lambda = 2, fixed = TRUE), + control = control.ergm( + MCMC.burnin = 1000, + MCMC.interval = 15000, + MCMC.samplesize = 25000, + seed = 34526 + ) +) +summary(mod1) +``` + +``` +## Call: +## ergm(formula = cranborne ~ edges + triangle + threetrail + altkstar(lambda = 2, +## fixed = TRUE), control = control.ergm(MCMC.burnin = 1000, +## MCMC.interval = 15000, MCMC.samplesize = 25000, seed = 34526)) +## +## Monte Carlo Maximum Likelihood Results: +## +## Estimate Std. Error MCMC % z value Pr(>|z|) +## edges -3.74166 0.91423 0 -4.093 <1e-04 *** +## triangle 1.59951 0.21921 0 7.297 <1e-04 *** +## threetrail -0.04077 0.01622 0 -2.513 0.012 * +## altkstar.2 0.59694 0.39283 0 1.520 0.129 +## --- +## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 +## +## Null Deviance: 687.6 on 496 degrees of freedom +## Residual Deviance: 286.0 on 492 degrees of freedom +## +## AIC: 294 BIC: 310.8 (Smaller is better. MC Std. Err. = 0.08471) +``` + +As our results show, we have three significant predictors: `edges`, `triangle`, and `threetrail` and `altkstar` is not significant (at $\alpha = 0.05$) just as Brughmans and Brandes (2017) found. Looking at our coefficients, our negative `edges` term suggests that edges are more likely absent than present in our model as we would expect given the density. For `triangle` we have a positive coefficient suggesting that `triangles` are more likely than we would expect by chance. Finally, `threetrails` are slightly less common than we would expect in a random network. The difference is small but statistically significant. + +Brughmans and Brandes (2017) generated similar results but their assessments of the goodness of fit of their model (see discussion below) caused them to create a second model with an additional term to capture the tendency for nodes to be connected to other nodes (and thus not be `isolates`). + +Let's run the second model and look at the results: + + +```r +mod2 <- ergm( + cranborne ~ edges + triangle + threetrail + + altkstar(2, fixed = TRUE) + isolates, + control = control.ergm( + MCMC.burnin = 1000, + MCMC.interval = 15000, + MCMC.samplesize = 25000, + seed = 1346 + ) +) +summary(mod2) +``` + +``` +## Call: +## ergm(formula = cranborne ~ edges + triangle + threetrail + altkstar(2, +## fixed = TRUE) + isolates, control = control.ergm(MCMC.burnin = 1000, +## MCMC.interval = 15000, MCMC.samplesize = 25000, seed = 1346)) +## +## Monte Carlo Maximum Likelihood Results: +## +## Estimate Std. Error MCMC % z value Pr(>|z|) +## edges -7.60337 2.69813 0 -2.818 0.00483 ** +## triangle 1.62452 0.20215 0 8.036 < 1e-04 *** +## threetrail -0.05946 0.02256 0 -2.635 0.00841 ** +## altkstar.2 1.92114 0.98390 0 1.953 0.05087 . +## isolates -2.57763 1.61226 0 -1.599 0.10987 +## --- +## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 +## +## Null Deviance: 687.6 on 496 degrees of freedom +## Residual Deviance: 283.4 on 491 degrees of freedom +## +## AIC: 293.4 BIC: 314.4 (Smaller is better. MC Std. Err. = 0.04119) +``` + +In this model we again obtain results that mirror those of Brughmans and Brandes (2017). We see with our `edges` term a tendency for edges to be absent as we would expect. For `triangle` we see a strong tendency for closed triangles in our network as Tilley's model predicted. We do not however see a tendency towards visual pathways beyond what we would expect by chance as our `threetrail` term suggests a slight tendency away from these configurations. With the addition of the `isolates` term our `altkstar` term is significant and positive suggesting a tendency for some nodes to have higher degree than most. Finally, `isolates` is negative suggesting a tendency against isolated nodes but the p-value is a bit higher so we should not put too much interpretive weight in this coefficient estimate. + +### Assessing Goodness-of-Fit{#GOF} + +If we compare model fit statistics we can see that the AIC for model 2 is slightly lower than for model 1. Further, the difference between the Null and residual deviance is slightly greater for model 2. At the same time, the BIC for model 2 is slightly higher than for model 1. Overall this suggests that the two models are quite similar in terms of their improvement over a model with no predictors but we don't have strong statistical argument from these terms alone for picking one over the other (and thus it probably makes sense to evaluate fit statistics as we show here or theoretical arguments for preferring one model or the other). + +To take this further we can use the `gof` or goodness-of-fit function in `ergm` to assess the degree to which our models provide reasonable descriptions of our observations. We can start by running the `gof` function for both models. This function provides visualizations and other statistics to help assess the degree to which model statistics, node degree, edge-wise shared partners, and geodesic distance between nodes are preserved in the networks simulated in the ERGM. If you run this function on a directed network, you additionally get assessments of in-degree and out-degree. + + +```r +mod1_gof <- gof(mod1) +mod2_gof <- gof(mod2) + +mod1_gof$summary.model +``` + +``` +## obs min mean max MC p-value +## edges 49.0000 32.00 49.9100 74.0000 1.00 +## triangle 22.0000 5.00 24.2100 143.0000 0.80 +## threetrail 628.0000 224.00 689.5200 3028.0000 0.80 +## altkstar.2 102.7891 59.25 106.3128 206.5781 0.98 +``` + +```r +mod2_gof$summary.model +``` + +``` +## obs min mean max MC p-value +## edges 49.0000 31.000 50.140 63.0000 0.86 +## triangle 22.0000 5.000 22.590 76.0000 0.84 +## threetrail 628.0000 226.000 654.230 1568.0000 0.98 +## altkstar.2 102.7891 58.875 106.187 150.7266 0.86 +## isolates 3.0000 0.000 2.780 11.0000 0.98 +``` + +The summary output for each model shows the observed feature value for a given term and then the min, max, and mean value in the simulated networks. In general, we want the mean values to match closely with relatively small ranges around them. The MC p-value (Markov Chain p-value) provides and indication of fit here where higher numbers generally indicate a better fit. This is essentially the proportion of the steps in the chain where a given term met certain criteria. In general the results here suggest that the model terms generally provide a better fit for model 2 than model 1 (as Brughmans and Brandes also suggested using somewhat different goodness-of-fit statistics not directly calculated in `ergm`). + +It is also instructive to compare the properties of our randomly generated networks under each model to the observed network for properties that weren't directly included in our model. The `gof` function can be plotted directly to provide this information. Let's look at the four plots provided for both models: + + +```r +par(mfrow = c(2, 2)) +plot(mod1_gof) +``` + + + +```r +plot(mod2_gof) +``` + + + + + +In each of these plots the solid black line represents the values for a given property in our observed network and the box plots represent the distribution of values obtained in our randomly generated networks. As both plots show the median model statistics are quite similar to the observed in both models. In general we want to see the observed values to fall within the densest portion of the values for our randomly generated networks (i.e., near the middle of the box plots and certainly within the range). In our example here, both the observed degree distribution and edge-wise shared partners (the number of nodes with a specific number of partners) are quite similar to the simulated range of values. Importantly, we did not include terms for degree or edge-wise shared partners in our model but it still generated networks that closely match our observed in terms of these properties. This is evidence of a good fit. For minimum geodesic distance (length of shortest paths) however, we see that both models consistently over-estimated the geodesic distance for nodes for middling values. Overall, this suggests a fairly good (but not perfect) match between our simulated and observed network properties despite these properties not be directly included in our models. Importantly, our interpretation of our network doesn't hinge on geodesic distance so this mismatch is not a huge problem. No model is perfect but these results suggest that that model we tested here at least approximates the features of our observed network most relevant to our theoretical model. + +### Assessing Models and MCMC Diagnostics{#Diagnostics} + +Another important consideration we have not yet discussed is the need to assess the diagnostics of our model generating process to evaluate if it operated as expected. The `ergm` package generates our random networks using a [Markov Chain Monte Carlo (MCMC)](https://en.wikipedia.org/wiki/Markov_chain_Monte_Carlo) process. MCMC is a means for efficiently randomly sampling from a high-dimensional probability distribution. We want to ensure that as our MCMC process explores the parameter space fully and that it does not generate problematic data such as temporally correlated estimates or highly skewed distributions of coefficient estimates. Problems like these would be an indication of poor model specification (the inappropriate inclusion or exclusion of relevant terms for predicting our network). + +In order to assess our models, we can use the `mcmc.diagnostics` function. Here we run it for model 2 and look at the results. We also call the `latticeExtra` package here as that helps make the visual output look a bit better. + + + +```r +library(latticeExtra) +mcmc.diagnostics(mod2) +``` + + + +``` +## Sample statistics summary: +## +## Iterations = 4691250:93746250 +## Thinning interval = 22500 +## Number of chains = 1 +## Sample size per chain = 3959 +## +## 1. Empirical mean and standard deviation for each variable, +## plus standard error of the mean: +## +## Mean SD Naive SE Time-series SE +## edges 0.72038 7.406 0.11770 0.11770 +## triangle 2.41248 13.898 0.22089 0.22089 +## threetrail 45.44759 281.422 4.47265 4.47265 +## altkstar.2 2.70105 23.460 0.37285 0.37285 +## isolates 0.03612 1.787 0.02839 0.02839 +## +## 2. Quantiles for each variable: +## +## 2.5% 25% 50% 75% 97.5% +## edges -14.00 -4.00 1.000 6.00 15.0 +## triangle -15.00 -7.00 -1.000 8.00 39.0 +## threetrail -389.00 -153.00 6.000 198.00 731.1 +## altkstar.2 -41.17 -13.45 1.945 18.28 50.0 +## isolates -3.00 -1.00 0.000 1.00 4.0 +## +## +## Are sample statistics significantly different from observed? +## edges triangle threetrail altkstar.2 isolates +## diff. 7.203839e-01 2.412478e+00 4.544759e+01 2.701053e+00 0.03612023 +## test stat. 6.120280e+00 1.092165e+01 1.016122e+01 7.244263e+00 1.27207754 +## P-val. 9.341108e-10 9.082771e-28 2.953452e-24 4.347972e-13 0.20334557 +## (Omni) +## diff. NA +## test stat. 1.623332e+02 +## P-val. 1.625963e-32 +## +## Sample statistics cross-correlations: +## edges triangle threetrail altkstar.2 isolates +## edges 1.0000000 0.70673620 0.8584509 0.9829232 -0.46961121 +## triangle 0.7067362 1.00000000 0.9168967 0.7916034 -0.01589291 +## threetrail 0.8584509 0.91689667 1.0000000 0.9263438 -0.14024854 +## altkstar.2 0.9829232 0.79160337 0.9263438 1.0000000 -0.32274942 +## isolates -0.4696112 -0.01589291 -0.1402485 -0.3227494 1.00000000 +## +## Sample statistics auto-correlation: +## Chain 1 +## edges triangle threetrail altkstar.2 isolates +## Lag 0 1.0000000000 1.0000000000 1.000000000 1.000000000 1.0000000000 +## Lag 22500 -0.0083986888 -0.0217929879 -0.009160269 -0.007558365 -0.0003860693 +## Lag 45000 0.0075076340 -0.0001967035 0.002179431 0.006750565 -0.0011776979 +## Lag 67500 -0.0204937479 -0.0223823019 -0.029173230 -0.022747752 -0.0071113820 +## Lag 90000 -0.0003116772 -0.0194452929 -0.011057746 -0.003769094 -0.0045814633 +## Lag 112500 0.0266253199 -0.0040683265 -0.002186196 0.019438426 0.0190821812 +## +## Sample statistics burn-in diagnostic (Geweke): +## Chain 1 +## +## Fraction in 1st window = 0.1 +## Fraction in 2nd window = 0.5 +## +## edges triangle threetrail altkstar.2 isolates +## -1.5485670 -0.9941408 -1.1091346 -1.5489431 0.6388872 +## +## Individual P-values (lower = worse): +## edges triangle threetrail altkstar.2 isolates +## 0.1214859 0.3201543 0.2673721 0.1213954 0.5228963 +## Joint P-value (lower = worse): 0.5028517 +## +## Note: To save space, only one in every 6 iterations of the MCMC sample +## used for estimation was stored for diagnostics. Sample size per chain +## was originally around 23754 with thinning interval 3750. +## +## Note: MCMC diagnostics shown here are from the last round of +## simulation, prior to computation of final parameter estimates. +## Because the final estimates are refinements of those used for this +## simulation run, these diagnostics may understate model performance. +## To directly assess the performance of the final model on in-model +## statistics, please use the GOF command: gof(ergmFitObject, +## GOF=~model). +``` + +In this output the particularly relevant parts include: + +* **sample statistic auto-correlation** - This is a measure of the correlation between values in the MCMC chain for each term across the number of steps (lags) indicated. Ideally, we would want to see low values for all but the Lag 0 and our example here looks good in that respect. +* **sample statistic burn-in diagnostic (Geweke)** - Burn-in refers to the number of points calculated before the MCMC starts recording points that will be included in our coefficient estimates. A burn-in helps deal with "start up effects" that can sometimes appear when we have a poor initial estimate of a parameter. For the Geweke statistics we actually want to obtain p-values close to 1 which, again this example satisfies. +* **MCMC plots** - The plots presented above show two plots for each term. The plot on the left is called the trace plot and it displays every retained value in the MCMC sampling chain included in the estimate. For this plot, we want to see values with roughly even distributions above and below 0 and with no obvious trends. The second plot shows the density of estimates for each term as a simple density plot. For these we want to see roughly bell-shaped curves centered close to 0, which indicates good convergence of our model. In our example here most of our terms look good though `triangle` is slightly skewed. This is not particularly egregious but if working on this model to make a specific argument about our `triangle` term we might choose to run a much longer MCMC chain to improve our fit. For some very complex models this may take many hours so it is often a good idea to run initial models and then set up longer runs overnight or when you will not be using your computer. + +
+

In the example above we noted that all of our terms appeared to look +good in our model diagnostics though the term triangle +produced a slightly skewed distribution with a long-tail of randomly +generated networks that had more closed triangles than the mean and the +observed. Why might this be?

+

Although we are often interested in transitivity and other properties +of networks that rely on triangles, the number of closed triads in a +network is actually highly constrained on other lower-level features +already included in the model: specifically the number of nodes and +density. There is considerable experimental work that demonstrates that +the majority of the variation in triad configurations can be explained +by these two simple terms in many networks (see Faust 2007, 2008, 2010). +The inclusion of these related terms can confound the MCMC algorithm +designed to generate estimates of model parameters and sometimes lead to +the exploration of unlikely parameter combinations. This is not uncommon +for ERGM terms that include dyadic or triadic relationships. In a +section below we discuss model degeneracy +(which refers to models that fail to converge) and what can be done +about it, including alternatives to the triangle model +term.

+
+ + +## Simulating Networks from ERGMs{#SimERGMs} + +It is possible to generate and explore network simulated using a particular ERGM using the `simulate` function. Let's generate some random networks from model 2 used above and then look at them along with the original network. + +In the code below we simply run a single `simulate` function with the model object, the argument `nsim` representing the number of networks we wish to generate, and `seed` which is the random seed for reproducability. The output is a `list()` object containing multiple `network` format objects. + + +```r +sim_nets <- simulate(mod2, nsim = 9, seed = 3464524) + +par(mfrow = c(3, 3)) # set up for multipanel plotting +for (i in 1:9) { + plot(sim_nets[[i]], + vertex.cex = (sna::degree(sim_nets[[i]]) / 4) + 1) +} +``` + + + +```r +par(mfrow = c(1, 1)) # return to single panel +plot(cranborne, + vertex.cex = (sna::degree(cranborne) / 4) + 1) +``` + + + +These simulations help us better understand the model we have created. There are a obvious similarities between the original network and the simulations but there are also key differences. In particular, most of the random simulations created networks with a single large component whereas the original network has multiple components. This likely explains the mismatch in our goodness-of-fit statistics for geodesic distance. We could perhaps deal with this by including additional terms such as terms defined in relation to geographic location or clustering, but that is an experiment for another day. + +## Additional Info on ERGM Terms{#ERGMterms} + +In the Cranborne Chase example above, we were working with a published example so the hard part (thinking about how a particular theory can be conceptualized in formal network model terms) was done for us. In practice, choosing terms to use can be quite difficult and confusing. This is particularly true because there are multiple terms that do essentially the same thing in different ways. In this section we first walk through a few of the other common options that were not covered above and then provide some advice on where to go next. + +In the first example below we will be using the [Cibola technological similarity networks](#Cibola) used in several other portions of this guide. The data imported below includes a `network` object and a data frame that contains attributes relating to the nodes in that network. We load in the data and then assign attributes to the `Cibola_n` object. You can download the [data here](data/Cibola_n.Rdata) to follow along. + +The attributes we assign include: + +* **region** - A nominal regional designation for each node. +* **pubarch** - A nominal identification of the type of public architecture present at each settlement. Note that model terms cannot include `NA` data so empty values should include names like "none" +* **d_mat** - An edge attribute which is defined by a distance matrix among all settlements in meters. + + +```r +load("data/Cibola_n.RData") +# Cibola_n network object +# Cibola_attr - attribute data frame + +# add node attribute based on region +cibola_n %v% "region" <- cibola_attr$Region +# add node attribute based on public architecture +cibola_n %v% "pubarch" <- cibola_attr$Great.Kiva + +# matrix of distances among settlements +d_mat <- as.matrix(dist(cibola_attr[, 2:3])) +``` + +In many cases we want to use attributes of nodes or edges as predictors in our ERGMs rather than simply network structures. This can be done a few different ways but in the example below we use the `nodematch` term which calculates a coefficient for nodes that share values for a given attribute. We can also set an additional argument in `nodematch` which specifies coefficient for each unique value in the node attribute (`diff = TRUE`). Finally, we use a matrix of geographic distances of edges as a `edgecov` (edge co-variate) term. This term expects a square matrix of `n x n` for where `n` is the number of nodes in the network and helps us assess the degree to which the distance between settlements is predictive of the presence or absence of an edge. + +Let's take a look at an example using all three of these terms: + + +```r +mod_cibola <- ergm(cibola_n ~ edges + nodematch("region") + + nodematch("pubarch", diff = TRUE) + + edgecov(d_mat)) +summary(mod_cibola) +``` + +``` +## Call: +## ergm(formula = cibola_n ~ edges + nodematch("region") + nodematch("pubarch", +## diff = TRUE) + edgecov(d_mat)) +## +## Maximum Likelihood Results: +## +## Estimate Std. Error MCMC % z value +## edges 1.196e+00 3.513e-01 0 3.405 +## nodematch.region 1.299e+00 4.593e-01 0 2.828 +## nodematch.pubarch.Cicular Great Kiva 2.843e-01 4.439e-01 0 0.640 +## nodematch.pubarch.none -7.750e-01 2.763e-01 0 -2.805 +## nodematch.pubarch.Rectangular Great Kiva -6.913e-01 5.448e-01 0 -1.269 +## edgecov.d_mat -2.323e-05 3.847e-06 0 -6.039 +## Pr(>|z|) +## edges 0.000662 *** +## nodematch.region 0.004688 ** +## nodematch.pubarch.Cicular Great Kiva 0.521864 +## nodematch.pubarch.none 0.005026 ** +## nodematch.pubarch.Rectangular Great Kiva 0.204441 +## edgecov.d_mat < 1e-04 *** +## --- +## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 +## +## Null Deviance: 644.6 on 465 degrees of freedom +## Residual Deviance: 489.2 on 459 degrees of freedom +## +## AIC: 501.2 BIC: 526.1 (Smaller is better. MC Std. Err. = 0) +``` + +This creates output just like our example above and this gives you a sense of how categorical and co-variate ERGM terms work. In this example we have a positive coefficient for `edges` suggesting that there more edges are active than are not. Further, we have a positive coefficient for `nodematch.region` indicating that there are more edges between pairs of sites in the same region than would be expected by chance. If we skip down to `edgecov.d_mat` we can see the impact of distance on edges. We have a negative coefficient (which is very close to zero: `-2.323e-05`)which suggests that there are slight more longer distance connections than shorter ones in this network (because although there is a tendency for connections within regions there are also many connections between regions). Finally, we have the `nodematch.pubarch` variables for each value in `pubarch`. The only term that is statistically significant here is `nodematch.pubarch.none` which is negative suggesting that sites without public architecture have fewer connections than we would expect by chance. + +The examples above basically cover all of the common applications of `ergm` terms. There are terms that are specific to directed networks, weighted networks, bipartite networks, and even multilayer networks but the basic procedures of using them are covered in the examples above. Everything else is finding the right model to fit your data (and this really is the hard part). There is no magic bullet here but in general we suggest you carefully read the [ERGM term descriptions](https://zalmquist.github.io/ERGM_Lab/ergm-terms.html#:~:text=ergm%20functions%20such%20as%20ergm,valued%20mode%20and%20vice%20versa.) and consider how these different terms relate to your data and network theories. Your efforts will be better spent when your model is designed in relation to a specific and well-described network theory/hypothesis. We suggest reading the archaeological examples of ERGMs cited in this document and in the broader networks literature to get a sense of what is possible before diving into your own ERGM project. + +### Avoiding Model Degeneracy and Poor Convergence{#Degeneracy} + +Model degeneracy refers to when a specified ERGM never converges. What this means is there is some term or combination of terms in the model that have created a situation where no networks with the given properties can be obtained. What this typically looks like when this happens in R is that you enter your `ergm` call at the command line and things appear to be going okay but then you eventually get hung up with something like "Estimating equations are not within tolerance region. Iteration 2 of at most 60" and nothing happens for a long time. + +As described above in our assessment of MCMC diagnostics, this can sometimes happen because you have specified a term that essentially does not allow for for simulated networks that approximate the observed. A classic example is a network with terms for `edges` and `triangle` for triadic closure and no other terms. + +If you were to run a model using our Cranborne data using on the `edges` and `triangle` term, it would never converge despite the fact that the `triangle` term was included in the successful model above. As this suggests, poorly specified models are not just about the presence or absence of a single term but about the combination of terms used. + +
+

Do not run the chunk of code below. We promise, it doesn’t go +anywhere and will just waste your time.

+
+ + +```r +mod_fail <- ergm(cranborne ~ edges + triangle) +``` + +What then, can we do in the place of including terms that cause model degeneracy? Luckily there are a number of additional terms that have been designed to deal with exactly this issue. These include the "geometrically weighted" terms that are already built right into the `ergm` package. For example, the term `gwesp` or geometrically weighted shared partners is a measure of triadic closure that doesn't rely on the specific count of triangles, but instead on the tendency towards closing individual triads in the network. + +Let's try our model again substituting the `gwesp` term in the place of `triangle`. + + +```r +mod_win <- ergm(cranborne ~ edges + gwesp(0.25, fixed = TRUE), + control = control.ergm(seed = 2362)) +summary(mod_win) +``` + +``` +## Call: +## ergm(formula = cranborne ~ edges + gwesp(0.25, fixed = TRUE), +## control = control.ergm(seed = 2362)) +## +## Monte Carlo Maximum Likelihood Results: +## +## Estimate Std. Error MCMC % z value Pr(>|z|) +## edges -3.4856 0.3307 0 -10.539 <1e-04 *** +## gwesp.fixed.0.25 1.1627 0.2596 0 4.479 <1e-04 *** +## --- +## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 +## +## Null Deviance: 687.6 on 496 degrees of freedom +## Residual Deviance: 289.1 on 494 degrees of freedom +## +## AIC: 293.1 BIC: 301.5 (Smaller is better. MC Std. Err. = 0.2945) +``` + +So the model converges and we get `gwesp` as a statistically significant predictor with a positive coefficient estimate just as we saw with `triangle` in the complete model. Indeed if we include `gwesp` in the complete model we get results that largely mirror those above suggesting that this term is playing a similar role. + + +```r +mod_win2 <- + ergm( + cranborne ~ edges + gwesp(0.25, fixed = TRUE) + threetrail + + altkstar(2, fixed = TRUE) + isolates, + control = control.ergm(seed = 1346) + ) +summary(mod_win2) +``` + +``` +## Call: +## ergm(formula = cranborne ~ edges + gwesp(0.25, fixed = TRUE) + +## threetrail + altkstar(2, fixed = TRUE) + isolates, control = control.ergm(seed = 1346)) +## +## Monte Carlo Maximum Likelihood Results: +## +## Estimate Std. Error MCMC % z value Pr(>|z|) +## edges -9.04117 3.36219 0 -2.689 0.00717 ** +## gwesp.fixed.0.25 1.51929 0.36396 0 4.174 < 1e-04 *** +## threetrail -0.03945 0.02866 0 -1.376 0.16874 +## altkstar.2 1.91799 1.22533 0 1.565 0.11752 +## isolates -3.58093 1.88523 0 -1.899 0.05750 . +## --- +## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 +## +## Null Deviance: 687.6 on 496 degrees of freedom +## Residual Deviance: 284.6 on 491 degrees of freedom +## +## AIC: 294.6 BIC: 315.6 (Smaller is better. MC Std. Err. = 0.4782) +``` + +What about the numbers we're providing to the `gwesp` term argument `(0.25, fixed = TRUE)`. These number specify the so-called decay parameter in the model and whether or not that parameter should be fixed or allowed to vary across steps in the MCMC process. The details of this are well beyond the scope of this tutorial but suffice it to say that the general advice is to select the decay value that produces the best fit model in your given analysis. If you run your model without `fixed = TRUE` the model will attempt to estimate the decay parameter and you will get an additional result in our output the specifies the coefficient for that decay term as well. Keep in mind that this is essentially adding a term to the model so it may then be harder or take longer to fit your models. + +Here is an example: + + +```r +mod_nofix <- ergm(cranborne ~ edges + gwesp, + control = control.ergm(seed = 23642)) +summary(mod_nofix) +``` + +``` +## Call: +## ergm(formula = cranborne ~ edges + gwesp, control = control.ergm(seed = 23642)) +## +## Monte Carlo Maximum Likelihood Results: +## +## Estimate Std. Error MCMC % z value Pr(>|z|) +## edges -3.3989 0.3120 0 -10.892 < 1e-04 *** +## gwesp 0.9357 0.2915 0 3.210 0.00133 ** +## gwesp.decay 0.5738 0.3125 0 1.836 0.06636 . +## --- +## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 +## +## Null Deviance: 687.6 on 496 degrees of freedom +## Residual Deviance: 288.2 on 493 degrees of freedom +## +## AIC: 294.2 BIC: 306.8 (Smaller is better. MC Std. Err. = 0.3811) +``` + + +In addition to the `gwesp` term, there are many additional terms [listed here](https://zalmquist.github.io/ERGM_Lab/ergm-terms.html#:~:text=ergm%20functions%20such%20as%20ergm,valued%20mode%20and%20vice%20versa.) which fill similar roles and help you build models that avoid degeneracy. For more information see Hunter and Handcock (2006). diff --git a/08-spatial-interaction.Rmd b/08-spatial-interaction.Rmd index cce8fa7..2a69c39 100644 --- a/08-spatial-interaction.Rmd +++ b/08-spatial-interaction.Rmd @@ -31,8 +31,8 @@ First, let's read in the data and create a quick plot showing site locations wit ```{r, fig.width=7, fig.height=3, warning=F, message=F} wankarani <- read.csv("data/Wankarani_siteinfo.csv") -wankarani <- wankarani[which(wankarani$Period == "Late Intermediate"), ] -wankarani <- wankarani[which(wankarani$Type == "habitation"), ] +wankarani <- wankarani[which(wankarani[["Period"]] == "Late Intermediate"), ] +wankarani <- wankarani[which(wankarani[["Type"]] == "habitation"), ] load("data/bolivia.Rdata") library(ggmap) @@ -55,7 +55,7 @@ colnames(xy) <- c("x", "y") ggmap(base_bolivia, darken = 0.35) + geom_point( data = xy, - aes(x, y, size = wankarani$Area), + aes(x, y, size = wankarani[["Area"]]), color = "red", alpha = 0.8, show.legend = FALSE @@ -95,7 +95,7 @@ Now let's try an example using the Wankarani data. For this first example we wil d_mat <- as.matrix(dist(wankarani[, 5:6])) / 1000 test1 <- - grav_mod(attract = wankarani$Area, + grav_mod(attract = wankarani[["Area"]], B = 1, d = d_mat) @@ -103,7 +103,7 @@ library(superheat) superheat(test1) library(scales) -df <- data.frame(Flow = rowSums(test1), Area = wankarani$Area) +df <- data.frame(Flow = rowSums(test1), Area = wankarani[["Area"]]) ggplot(data = df) + geom_point(aes(x = Area, y = Flow)) + @@ -136,7 +136,7 @@ net <- graph_from_adjacency_matrix(test1_plot, weighted = TRUE) # Extract edge list from network object -edgelist <- get.edgelist(net) +edgelist <- igraph::as_edgelist(net) # Create data frame of beginning and ending points of edges edges <- data.frame(xy[edgelist[, 1], ], xy[edgelist[, 2], ]) @@ -181,14 +181,14 @@ d_mat <- as.matrix(dist(wankarani[, 5:6])) / 1000 test2 <- grav_mod( - attract = wankarani$Area, + attract = wankarani[["Area"]], B = 0.1, d = d_mat ) superheat(test2) -df <- data.frame(Flow = rowSums(test2), Area = wankarani$Area) +df <- data.frame(Flow = rowSums(test2), Area = wankarani[["Area"]]) ggplot(data = df) + geom_point(aes(x = Area, y = Flow)) + @@ -209,13 +209,15 @@ ggplot(data = df) + ```{r, message=F, warning=F, cache=T, fig.height = 3, fig.width=7} sel_edges <- event2dichot(test2, method = "quantile", thresh = 0.25) test2_plot <- test2 * sel_edges +test2_plot <- (test2_plot + t(test2_plot)) / 2 +test2_plot[is.na(test2_plot)] <- 0 net2 <- graph_from_adjacency_matrix(test2_plot, mode = "undirected", weighted = TRUE) # Extract edge list from network object -edgelist <- get.edgelist(net2) +edgelist <- igraph::as_edgelist(net2) # Create data frame of beginning and ending points of edges edges <- data.frame(xy[edgelist[, 1], ], xy[edgelist[, 2], ]) @@ -690,12 +692,12 @@ In order to test this model, we will use the same Wankarani settlement data we u ```{r} wankarani <- read.csv("data/Wankarani_siteinfo.csv") -wankarani <- wankarani[which(wankarani$Period == "Late Intermediate"), ] -wankarani <- wankarani[which(wankarani$Type == "habitation"), ] +wankarani <- wankarani[which(wankarani[["Period"]] == "Late Intermediate"), ] +wankarani <- wankarani[which(wankarani[["Type"]] == "habitation"), ] d <- as.matrix(dist(wankarani[, 5:6])) -rad_test <- radiation(pop = wankarani$Area / 500, d_mat = d) +rad_test <- radiation(pop = wankarani[["Area"]] / 500, d_mat = d) ``` Now let's plot the resulting network with each node scaled by the total incident flow (row sums of the output of the function above). We plot network edges with weights indicated by color (blue indicates low weight and yellow indicates high weight). @@ -724,7 +726,7 @@ net <- weighted = TRUE) # Extract edge list from network object -edgelist <- get.edgelist(net) +edgelist <- igraph::as_edgelist(net) # Create data frame of beginning and ending points of edges edges <- @@ -765,7 +767,7 @@ This map shows clusters of higher and lower edge weights and again variation in ```{r, fig.width = 7, fig.heigh = 7, warning=F, message=F} dg_grav <- rowSums(grav_mod( - attract = wankarani$Area / 1000, + attract = wankarani[["Area"]] / 1000, B = 1, d = d_mat )) diff --git a/08-spatial-interaction.md b/08-spatial-interaction.md new file mode 100644 index 0000000..f0a636f --- /dev/null +++ b/08-spatial-interaction.md @@ -0,0 +1,931 @@ +# Spatial Interaction Models{#SpatialInteraction} + +![](images/image_break.png){width=100%} + +In the [Spatial Networks](#SpatialNetworks) section of this document we cover most of the simple network models for generating spatial networks based on absolute distance, configurations of locations, and territories. There is one general class of spatial network model that we described briefly in the Brughmans and Peeples (2023) book but did not cover in detail as the specifics require considerably more discussion. This includes a wide variety of spatial interaction models such as gravity models, truncated power functions, radiation models, and similar custom derivations of these approaches. In general, a spatial interaction model is a formal mathematical model that is used to predict the movement of people (or other sorts of entities) between origins and destinations. These models typically use information on the relative sizes or "attractiveness" of origins and destinations or other additional information on volumes of flows in and out. Such models have long been popular in geography, economics, and other fields for doing things like predicting the amount of trade between cities or nations or predicting or improving the location of services in a given geographic extent. Statistical spatial interaction models have been used in archaeology as well for both empirical and simulation studies (e.g., Bevan and Wilson 2013; Evans et al. 2011; Gauthier 2020; Paliou and Bevan 2016; Rihll and Wilson 1987) though they have not had nearly the impact they have had in other fields. We suggest that there is considerable potential for these models, in particular in contexts where we have other independent information for evaluating network flows across a study area. + +In this section, we briefly outline a few common spatial interaction models and provide examples. For additional detailed overview and examples of these models there are several useful publications (see [Evans et al. 2011](https://www.researchgate.net/publication/277221754_Interactions_In_Space_For_Archaeological_Models); [Rivers et al. 2011](https://plato.tp.ph.ic.ac.uk/~time/networks/arch/BevanRewriteFigTableInText110727.pdf); and [Amati et al. 2018](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5797198/)). + +## Simple Gravity Models{#GravityModel} + +We'll start here with the very simple gravity model. This model is built on the notion that the "mass" of population at different origins and destinations creates attractive forces between them that is further influenced by the space and travel costs between them. This model takes many forms but the simplest can be written as: + +$$x_{ij} = cv_iv_jf(d_{ij})$$ + +where + +* $x_{ij}$ is the number or strength of connection between nodes $i$ and $j$ +* $c$ is a proportionality constant (gravitational constant) which balances the units of the formula. For our purposes we can largely ignore this value as it changes all absolute values but not relative values between nodes. +* $v_i$ and $v_j$ are attributes of nodes $i$ and $j$ contributing to their "mass" (attractiveness). This could be population or resource catchment or area or anything other factor that likely influences the attractiveness of the node in terms of the interaction that is the focus of the network. +* $f(d_{ij})$ is the cost or "deterrence" function defining the travel costs between $i$ and $j$. Frequently, this cost function is further specified using inverse power law or exponential decay functions defined respectively by the equations below: + +$$f(d_{ij}) = \frac{1}{(1+\beta d{ij})^\gamma} \text{ and } f(d_{ij}) = \frac{1}{e^{\beta d{ij}}}$$ + +where $\beta$ is a scaling factor for both models sand $\gamma$ determines the weight of the tail in the power law distribution. + +There are numerous different configurations of the simple gravity model in the literature. Some versions add exponents to $V_i$ or $V_j$ to vary the importance of inflow and outflow independently, other versions use different derivations of deterrence, and many define inflows and outflows using different sources of empirical information with additional terms to scale the units. Calculating models like these is relatively easy but determining how to parameterize such models is typically the hard part as we discuss further below. + +In order to demonstrate simple gravity models, we're going to use a regional data set of Late Intermediate periods Wankarani sites in the Bolivian Altiplano provided online on the [Comparative Archaeology Database at the University of Pittsburgh](http://www.cadb.pitt.edu/mcandrews/index.html) (McAndrews 2005). The details of this database and all of the variables are described at the link provided. This data set has more variables than we will use here but this provides a good example to explore because it includes location and size information for all sites. + +First, let's read in the data and create a quick plot showing site locations with points scaled by site area (as a measure of potential attractiveness and outflow potential). We will then select sites dating to the Late Intermediate period for analysis. We further limit consideration to habitation sites. [Download the data here to follow along](data/Wankarani_siteinfo.csv) and [download the base map here](data/bolivia.Rdata). + + +```r +wankarani <- read.csv("data/Wankarani_siteinfo.csv") +wankarani <- wankarani[which(wankarani[["Period"]] == "Late Intermediate"), ] +wankarani <- wankarani[which(wankarani[["Type"]] == "habitation"), ] +load("data/bolivia.Rdata") + +library(ggmap) +library(sf) + +# Convert attribute location data to sf coordinates and change +# map projection +locations_sf <- + st_as_sf(wankarani, coords = c("Easting", "Northing"), crs = 32721) +loc_trans <- st_transform(locations_sf, crs = 4326) +coord1 <- do.call(rbind, st_geometry(loc_trans)) %>% + tibble::as_tibble() %>% + setNames(c("lon", "lat")) + +xy <- as.data.frame(coord1) +colnames(xy) <- c("x", "y") + + +# Plot original data on map +ggmap(base_bolivia, darken = 0.35) + + geom_point( + data = xy, + aes(x, y, size = wankarani[["Area"]]), + color = "red", + alpha = 0.8, + show.legend = FALSE + ) + + theme_void() +``` + + + +As this plot shows, there is one site that is considerably larger than the others and a few other clusters of large sites in the eastern portion of the study area. + +Now we're going to build a small function called `grav_mod` that includes 3 arguments: + +* **`attract`** which is the measure of settlement attractiveness in the network. In our example here we will use area as a measure of attractiveness for both the sending and receiving site, though this can be varied. +* **`B`** which is our $\beta$ parameter for the exponential decay cost function outlined above. +* **`d`** which is a matrix of distances among our nodes. Note that this could be something other than physical distance like travel time as well. It helps to have this in units that don't result in very large numbers to keep the output manageable (though the actual absolute numbers don't matter) + + +```r +grav_mod <- function(attract, B, d) { + res <- matrix(0, length(attract), length(attract)) + + for (i in seq_len(length(attract))) { + for (j in seq_len(length(attract))) { + res[i, j] <- + attract[i] * attract[j] * exp(-B * d[i,j]) + } + } + diag(res) <- 0 + return(res) +} +``` + +Now let's try an example using the Wankarani data. For this first example we will set `B` to `1`. We'll take a look at the results by first, creating a heat map of the gravity model for every pair of nodes using the `superheat` package. We will then plot the site size against the estimated flow from our model with both axes transformed to base-10 logarithms to see how those variables relate. We use the packages `scales` to provide exponential notation for the axis labels. + + +```r +# First calculate a distance matrix. We divide by +# 1000 so results are in kilometers. +d_mat <- as.matrix(dist(wankarani[, 5:6])) / 1000 + +test1 <- + grav_mod(attract = wankarani[["Area"]], + B = 1, + d = d_mat) + +library(superheat) +superheat(test1) +``` + + + +```r +library(scales) +df <- data.frame(Flow = rowSums(test1), Area = wankarani[["Area"]]) + +ggplot(data = df) + + geom_point(aes(x = Area, y = Flow)) + + scale_x_log10( + breaks = trans_breaks("log10", function(x) + 10 ^ x), + labels = trans_format("log10", math_format(10 ^ .x)) + ) + + scale_y_log10( + breaks = trans_breaks("log10", function(x) + 10 ^ x), + labels = trans_format("log10", math_format(10 ^ .x)) + ) + + theme_bw() +``` + + + +As these plots illustrate, there are clusters in the estimated flows among nodes, which is not surprising given that the sites themselves form a few clusters. Further, the plot comparing area to flow shows that there is a roughly positive linear relationship between the two but there is also variation with nodes with more or less flow than would be expected based on distance alone. + +Next, let's plot a network showing all connections scaled and colored based on their strength in terms of estimated flow and with nodes scaled based on weighted degree (the total volume of flow incident on that node). The blue colored ties are weaker and the yellow colored ties are stronger. For the sake of visual clarity we omit the 25% weakest edges. + + +```r +library(statnet) +``` + +``` +## Installed ReposVer Built +## ergm "4.6.0" "4.12.0" "4.2.3" +## ergm.count "4.1.1" "4.1.3" "4.2.3" +## ndtv "0.13.3" "0.13.4" "4.2.3" +## network "1.18.2" "1.20.0" "4.2.3" +## networkDynamic "0.11.4" "0.12.0" "4.2.3" +## sna "2.7-2" "2.8" "4.2.3" +## statnet.common "4.9.0" "4.13.0" "4.2.3" +## tergm "4.2.0" "4.2.2" "4.2.3" +## tsna "0.3.5" "0.3.6" "4.2.3" +``` + +```r +library(igraph) +library(ggraph) + +sel_edges <- event2dichot(test1, method = "quantile", thresh = 0.25) +test1_plot <- test1 * sel_edges + +net <- + graph_from_adjacency_matrix(test1_plot, weighted = TRUE) + +# Extract edge list from network object +edgelist <- igraph::as_edgelist(net) + +# Create data frame of beginning and ending points of edges +edges <- data.frame(xy[edgelist[, 1], ], xy[edgelist[, 2], ]) +colnames(edges) <- c("X1", "Y1", "X2", "Y2") + +# Calculate weighted degree +dg_grav <- rowSums(test1) / 10000 + +# Plot data on map +ggmap(base_bolivia, darken = 0.35) + + geom_segment( + data = edges, + aes( + x = X1, + y = Y1, + xend = X2, + yend = Y2, + col = log(E(net)$weight), + alpha = log(E(net)$weight)), + show.legend = FALSE + ) + + scale_alpha_continuous(range = c(0, 0.5)) + + scale_color_viridis() + + geom_point( + data = xy, + aes(x, y, size = dg_grav), + alpha = 0.8, + color = "red", + show.legend = FALSE + ) + + theme_void() +``` + + + +As this plot shows, there are areas characterized by higher and lower flow throughout this study area. The largest site in the study area (shown in the first map above) is characterized by a high weighted degree but there are other smaller sites that also have high weighted degree, especially in the eastern half of the study area. + +Let's now take a look at the same data again, this time we set `B` or $\beta$ to `0.1`. + + +```r +# First calculate a distance matrix. We divide by +# 1000 so results are in kilometers. +d_mat <- as.matrix(dist(wankarani[, 5:6])) / 1000 + +test2 <- + grav_mod( + attract = wankarani[["Area"]], + B = 0.1, + d = d_mat + ) + +superheat(test2) +``` + + + +```r +df <- data.frame(Flow = rowSums(test2), Area = wankarani[["Area"]]) + +ggplot(data = df) + + geom_point(aes(x = Area, y = Flow)) + + scale_x_log10( + breaks = trans_breaks("log10", function(x) + 10 ^ x), + labels = trans_format("log10", math_format(10 ^ .x)) + ) + + scale_y_log10( + breaks = trans_breaks("log10", function(x) + 10 ^ x), + labels = trans_format("log10", math_format(10 ^ .x)) + ) + + theme_bw() +``` + + + + +```r +sel_edges <- event2dichot(test2, method = "quantile", thresh = 0.25) +test2_plot <- test2 * sel_edges +test2_plot <- (test2_plot + t(test2_plot)) / 2 +test2_plot[is.na(test2_plot)] <- 0 + +net2 <- + graph_from_adjacency_matrix(test2_plot, mode = "undirected", + weighted = TRUE) + +# Extract edge list from network object +edgelist <- igraph::as_edgelist(net2) + +# Create data frame of beginning and ending points of edges +edges <- data.frame(xy[edgelist[, 1], ], xy[edgelist[, 2], ]) +colnames(edges) <- c("X1", "Y1", "X2", "Y2") + +# Calculate weighted degree +dg <- rowSums(test2) / 10000 + +# Plot data on map +ggmap(base_bolivia, darken = 0.35) + + geom_segment( + data = edges, + aes( + x = X1, + y = Y1, + xend = X2, + yend = Y2, + col = log(E(net2)$weight), + alpha = log(E(net2)$weight)), + show.legend = FALSE + ) + + scale_alpha_continuous(range = c(0, 0.5)) + + scale_color_viridis() + + geom_point( + data = xy, + aes(x, y, size = dg), + alpha = 0.8, + color = "red", + show.legend = FALSE + ) + + theme_void() +``` + + + +When we lower the `B` $\beta$ parameter we get a stronger linear relationship between site area and flow. Further, when we look at the network, we see a more even degree distribution (though there are still nodes with higher degree) and more distributed edge weights across the network, though the high values are still. concentrated in the eastern cluster. + +### Parameterizing the Gravity Model{#ParameterizeGravity} + +The simple model above uses just a single parameter $\beta$ and is largely based on empirical information on the distance among settlements and their sizes. The basic assumption here is that larger sizes "attract" more flow and also have more flow to provide to other sites. Distance is also important and the $\beta$ parameter determines the decay rate of distance as the plot below illustrates. + + +```r +brk <- seq(0.01, 1, by = 0.01) +out <- as.data.frame(matrix(0, length(brk), 6)) +out[, 1] <- brk + +for (i in seq_len(length(brk))) { + out[i, 2] <- exp(-brk[i]) + out[i, 3] <- exp(-brk[i] * 2) + out[i, 4] <- exp(-brk[i] * 5) + out[i, 5] <- exp(-brk[i] * 10) + out[i, 6] <- exp(-brk[i] * 20) +} + +colnames(out) <- c("beta", "D=1", "D=2", "D=5", "D=10", "D=20") + +library(reshape2) +df <- melt(out[, 2:6]) +df$beta <- rep(brk, 5) + +ggplot(data = df) + + geom_line(aes(x = beta, y = value, color = variable)) + + ylab("Decay") + + xlab(expression( ~ beta)) + + theme_bw() +``` + + + +As we see in the plot above, the decay rate varies at different values of $\beta$ and also in relation to the distance between points. How then, do we select the appropriate $\beta$ in a model like this? There are a few ways to address this question depending on data availability and the nature of the issue such networks are being used to address. First, do we have some independent measure of network flow among our sites? For example, perhaps we could take information on the number or diversity of trade wares recovered at each site. We might expect sites with greater flow to have higher numbers or more diverse trade ware assemblages. We could evaluate this proposition using regression models and determine which $\beta$ provides the best fit for the data and theory. Often, however, when we are working with simple gravity models we have somewhat limited data and cannot make such direct comparisons. It is possible that we have a theoretical expectation for the shape of the decay curve as shown above (note that distances could also be things like cost-distance here so perhaps we have a notion of maximal travel times or a caloric budget for movement) and we can certainly factor that into our model. As we will see below, however, there are alternatives to the simple gravity model that provide additional avenues for evaluating model fit. + +## The Rihll and Wilson "Retail" Model{#RihllWilson} + +One of the most popular extensions of the gravity model in archaeology was published by Rihll and Wilson in 1987 for their study of Greek city states in the 9th through 8th centuries B.C. They used a spatial interaction model that is sometimes called the "retail" model. This approach was originally designed for assessing the likely flows of resources into retail shops and how that can lead to shop growth/increased income and the establishment of a small number of super-centers that receive a large share of the overall available flow (often at the expense of smaller shops). When thinking about this model in a settlement context, the "flows" are people and resources and the growth of highly central nodes in the network is used to approximate the development of settlement hierarchy and the growth of large settlement centers. + +One of the big advantages of this model is that it only requires the locations of origins and destinations and some measure of the cost of movement among sites. From that, an iterative approach is used to model the growth or decline of nodes based on their configurations in cost space and a couple of user provided parameters. Versions of this model have been used in a number of archaeological studies (e.g., [Bevan and Wilson 2013](https://www.researchgate.net/publication/257154745_Models_of_settlement_hierarchy_based_on_partial_evidence); [Evans and Rivers 2016](https://arxiv.org/abs/1611.07839); [Filet 2017](https://www.frontiersin.org/articles/10.3389/fdigh.2016.00010/full); [Rihll and Wilson 1987](https://www.persee.fr/doc/hism_0982-1783_1987_num_2_1_1300)). Here we present a version of the model inspired by the recent work by these scholars. The code below was based in part on an [R function](https://rdrr.io/github/CRC1266-A2/moin/src/R/Rhill_Wilson_algebraic.R) created as part of an [ISAAKiel](https://isaakiel.github.io/) Summer School program. + +Let's first formally describe the Rihll and Wilson model. The model of interaction $T_{ij}$ among a set of sites $k$ can be represented as: + +$$T_{ij} = \frac{O_iW_j^\alpha e^{-\beta c_{ij}}}{\Sigma_k W_k^\alpha e^{-\beta c_{jk}}}$$ + +where + +* $T_{ij}$ is the matrix of flows or interaction between nodes $i$ and $j$ (which may or may not be the same set) +* $O_i$ is the estimated weight of flow or interaction out of the origins $i$ +* $W_j$ is the estimated weight of flow or interaction into the destinations $j$. In most archaeological applications, this is used to represent some measurement of settlement size. +* $\alpha$ is a parameter which defines the importance of resource flow into destinations. Numbers greater than 1 essentially model increasing returns to scale for every unit of flow. +* $e^{-\beta c_{ij}}$ is the "deterrence" function where $e$ is an exponential ($exp(-\beta c_{ij})$), $c_ij$ is a measure of the cost of travel between nodes $i$ and $j$ and $\beta$ is the decay parameter that describes the rate of decay in interaction at increasing distance. +* $\Sigma_k W_k^\alpha e^{-\beta c_{jk}}$ is the sum for all nodes $k$ of the expression using the $W_j$, $\alpha$, and deterrence function terms as described above. + +In the case here (and in many archaeological examples) we start with a simple naive assumption of flow by setting all values of $O$ and $W$ equal to 1. Our goal, indeed, is to estimate $W$ for all nodes iteratively. In order to do this after each calculation of $T_{ij}$ we calculate two new values $D_j$ and $\Delta W_j$ defined as: + +$$\begin{aligned} D_j & = \Sigma_i T_{ij}\\ +\\ +\Delta W_j & = \epsilon(D_j - KW_j) \end{aligned}$$ + +where + +* $D_j$ is a vector of values defined as the sum weights incident on a given node (column sums of $T_{ij}$) +* $\Delta W_j$ is the change in values of $W$ (or estimated settlement size) +* $\epsilon$ is a control parameter that determines how quickly $W$ can change at each iterative step. +* $K$ is a factor that is used to convert size $W$ to the sum of flows $D_j$ + +For the purposes of our examples here, we set $K$ to 1 to assume that sum of flows and size are in equal units and we set $\epsilon$ to 0.01 so that the model does not converge too rapidly. + +By way of example, we will use the original Greek city states data used by Rihll and Wilson (1987) and [put online](https://figshare.com/articles/dataset/Locations_of_108_Archaeic_Greek_settlements_used_in_Rihll_and_Wilson_1987_/868961) by Tim Evans based on his own work using these data and related spatial interaction models. [Download the data here to follow along](data/Rihll_Wilson.csv) and [download the Greece basemap here]("data/greece.Rdata"). + +Let's first map the Greek city states data. + + +```r +library(sf) +library(ggmap) +load("data/greece.Rdata") + +dat <- read.csv(file = "data/Rihll_Wilson.csv") +locs <- + st_as_sf(dat, + coords = c("Longitude_E", "Latitude_N"), + crs = 4326) + +ggmap(map) + + geom_point(data = dat, aes(x = Longitude_E, y = Latitude_N)) + + theme_void() +``` + + + +In the code below, we define a vector for the initial values of $O_i$ and $W_j$ setting the value to 1 for every site. We then define an initial $\alpha = 1.05$ (to suggest that flows in provide slight increasing returns) and $\beta = 0.1$ (which is a low decay rate such that long distance connections will retain some importance). The code then iteratively calculates values of $T_{ij}$ until the sum of weights stops changing by more than some very low threshold for a number of time steps. In the chunk of code below we calculate $T_{ij}$ and then plot histogram of $W_j$ in final state of model. + + +```r +# Set model parameters and initial variable states +Oi <- rep(1, nrow(dat)) +Wj <- rep(1, nrow(dat)) +alpha <- 1.05 +beta <- 0.1 +eps <- 0.01 +K <- 1 + +# Define distance among points in kilometers. Because our +# points are in geographic coordinates we use the distm +# function. See the section on Spatial Networks for more. +library(geosphere) +d <- as.matrix(distm(dat[, c(7, 6)])) / 1000 + +# Dj is initial set as a vector of 1s like Wj +Dj <- Wj + +# Create objects for keeping track of the number +# of iterations and the conditions required to end +# the loop. +end_condition <- 1 +iter <- 0 + +# Define the deterrence function as a exponential +det <- exp(-beta * d) + +# Create while loop that will continue to iterate Tij +# until 10,000 iterations or until the end_condition +# object is less than the threshold indicated. +while (!(end_condition < 1e-5) & iter < 10000) { + # Set Wj to Dj + Wj <- Dj + # Calculate Tij as indicated above + Tij <- + apply(det * Oi %o% Wj ^ alpha, 2, '/', + (Wj ^ alpha %*% det)) + # Calculate change in W using equation above + delta_W <- eps * (colSums(Tij) - (K * Wj)) + + # Calculate new Dj + Dj <- delta_W + Wj + + # Add to iterator and check for end conditions + iter <- iter + 1 + end_condition <- sum((Dj - Wj) ^ 2) +} + +hist(Wj, breaks = 15) +``` + + + +As this shows, the Rihll and Wilson model generates flow weights with a heavy tailed distribution with these parameters. This means that a small number of nodes are receiving lots of flow but most are receiving very little. + +In order to look at this geographically, Rihll and Wilson defined what they call "Terminal Sites". Terminal sites in the network are nodes where the total flow of inputs into the site $Wj$ is bigger than the largest flow out of the site. Let's define our terminal sites for our model run above and then examine them. + + +```r +terminal_sites <- NULL +for (i in 1:109) terminal_sites[i] <- sum(Tij[-i, i]) > max(Tij[i, ]) + +knitr::kable(dat[terminal_sites,]) +``` + + + +| | SiteID| ShortName|Name | XPos| YPos| Latitude_N| Longitude_E| +|:---|------:|---------:|:-----------|---------:|---------:|----------:|-----------:| +|14 | 13| 14|Aulis | 0.6366255| 0.2547325| 38.40000| 23.60000| +|18 | 17| 18|Onchestos | 0.3962963| 0.2967078| 38.37327| 23.15027| +|22 | 21| 22|Alalkomenai | 0.3098765| 0.2860082| 38.41000| 22.98600| +|25 | 24| 25|Thebes | 0.4876543| 0.3197531| 38.32872| 23.32191| +|45 | 44| 45|Megara | 0.5098765| 0.5197531| 38.00000| 23.33333| +|50 | 49| 50|Menidi | 0.7255144| 0.4761317| 38.08333| 23.73333| +|56 | 55| 56|Markopoulo | 0.8415638| 0.5740741| 37.88333| 23.93333| +|70 | 69| 70|Athens | 0.7164609| 0.5320988| 37.97153| 23.72573| +|78 | 77| 78|Kromna | 0.3098765| 0.5823045| 37.90479| 22.94820| +|80 | 79| 80|Lekhaion | 0.2777778| 0.5666667| 37.93133| 22.89293| +|97 | 96| 97|Prosymnia | 0.2119342| 0.7074074| 37.70555| 22.76298| +|101 | 100| 101|Argos | 0.1930041| 0.7444444| 37.63092| 22.71955| +|103 | 102| 103|Magoula | 0.1773663| 0.7740741| 37.59310| 22.70769| +|104 | 103| 104|Tiryns | 0.2349794| 0.7748971| 37.59952| 22.79959| + +Interesting, our terminal sites include many historically important and larger centers (such as Athens, Thebes, and Megara), despite the fact that we included no information about size in our model. + +Now let's map them. Points are scaled based on the weight of inflow and terminal sites are colored in blue. + + +```r +ggmap(map) + + geom_point( + data = dat, + aes( + x = Longitude_E, + y = Latitude_N, + size = Wj, + color = terminal_sites + ), + show.legend = FALSE + ) + + theme_void() +``` + + + +As this map illustrates, the terminal sites are roughly evenly distributed across the study area rather than clustered together. Now, let's see what happens when we vary our parameters $\alpha$ and $\beta$. + +For the next map, we will set $alpha = 1.15$ and leave $\beta$ as is. To make it easier to calculate everything, we're going to call an R script using `source()` that includes the full function and outputs $W_j$, $T_{ij}$, the number of iterations, and a logical vector indicating which nodes are terminals. + + + +```r +source("scripts/rihll_wilson.R") + +rw2 <- rihll_wilson(alpha = 1.15, beta = 0.1, dist_mat = d) + +ggmap(map) + + geom_point( + data = dat, + aes( + x = Longitude_E, + y = Latitude_N, + size = rw2$Wj, + color = rw2$terminals + ), + show.legend = FALSE + ) + + theme_void() +``` + + + +```r +knitr::kable(dat[rw2$terminals,]) +``` + + + +| | SiteID| ShortName|Name | XPos| YPos| Latitude_N| Longitude_E| +|:--|------:|---------:|:----------|---------:|---------:|----------:|-----------:| +|14 | 13| 14|Aulis | 0.6366255| 0.2547325| 38.40000| 23.60000| +|18 | 17| 18|Onchestos | 0.3962963| 0.2967078| 38.37327| 23.15027| +|45 | 44| 45|Megara | 0.5098765| 0.5197531| 38.00000| 23.33333| +|56 | 55| 56|Markopoulo | 0.8415638| 0.5740741| 37.88333| 23.93333| +|70 | 69| 70|Athens | 0.7164609| 0.5320988| 37.97153| 23.72573| +|78 | 77| 78|Kromna | 0.3098765| 0.5823045| 37.90479| 22.94820| +|97 | 96| 97|Prosymnia | 0.2119342| 0.7074074| 37.70555| 22.76298| + +Increasing $\alpha$ increases the importance of inflow so we end up with fewer terminal sites. Notably, we are still retaining many large and historically important cities despite not including information on site size in our model. + +Now let's set $\alpha = 1.05$ and increase $\beta = 0.35$: + + +```r +rw3 <- rihll_wilson(alpha = 1.05, beta = 0.35, dist_mat = d) + +ggmap(map) + + geom_point( + data = dat, + aes( + x = Longitude_E, + y = Latitude_N, + size = rw3$Wj, + color = rw3$terminals + ), + show.legend = FALSE + ) + + theme_void() +``` + + + +```r +knitr::kable(dat[rw3$terminals,]) +``` + + + +| | SiteID| ShortName|Name | XPos| YPos| Latitude_N| Longitude_E| +|:---|------:|---------:|:--------------|---------:|---------:|----------:|-----------:| +|4 | 3| 4|Ay.Marina | 0.4267490| 0.2201646| 38.48208| 23.20793| +|50 | 49| 50|Menidi | 0.7255144| 0.4761317| 38.08333| 23.73333| +|68 | 67| 68|Phaleron | 0.6958848| 0.5650206| 37.93731| 23.70625| +|71 | 70| 71|Kallithea | 0.7074074| 0.5419753| 37.95589| 23.70209| +|78 | 77| 78|Kromna | 0.3098765| 0.5823045| 37.90479| 22.94820| +|80 | 79| 80|Lekhaion | 0.2777778| 0.5666667| 37.93133| 22.89293| +|88 | 87| 88|Kleonai | 0.2168724| 0.6341564| 37.81142| 22.77429| +|90 | 89| 90|Zygouries | 0.2316872| 0.6489712| 37.80300| 22.78015| +|95 | 94| 95|Mykenai | 0.2069959| 0.6884774| 37.73079| 22.75638| +|97 | 96| 97|Prosymnia | 0.2119342| 0.7074074| 37.70555| 22.76298| +|98 | 97| 98|Argive_Heraion | 0.2185185| 0.7172840| 37.69194| 22.77472| +|100 | 99| 100|Pronaia | 0.2333333| 0.7880658| 37.57682| 22.79954| +|102 | 101| 102|Kephalari | 0.1609054| 0.7666667| 37.59736| 22.69123| +|103 | 102| 103|Magoula | 0.1773663| 0.7740741| 37.59310| 22.70769| +|104 | 103| 104|Tiryns | 0.2349794| 0.7748971| 37.59952| 22.79959| +|105 | 104| 105|Prof.Elias | 0.2514403| 0.7683128| 37.60818| 22.81920| +|106 | 105| 106|Nauplia | 0.2292181| 0.7946502| 37.56812| 22.80871| + +As this map illustrates, increasing $\beta$ increases the distance decay meaning that local interactions are more important leading to a more even distribution of $W_j$ values and more terminal sites (which are further somewhat spatially clustered). + +### Parameterizing the Retail Model{#ParameterizingRetail} + +How might we select appropriate values for $\alpha$ and $\beta$ in our model? The approach Rihll and Wilson and many subsequent researchers have taken (see Bevan and Wilson 2013; Filet 2017) is to use our knowledge of the archaeological record and regional settlement patterns and to select a model that is most consistent with that knowledge. Our model creates more or fewer highly central nodes depending on how we set our parameters. As we noted above, the terminal nodes we defined consistently include historically important and large sites like Athens suggesting that our model is likely doing something reasonable. One potential approach would be to run comparisons for a plausible range of values for $\alpha$ and $\beta$ and to evaluate relationships with our own archaeological knowledge of settlement hierarchy and which sites/places are defined as terminal sites or highly central places in our model. + +In order to test a broader range of parameter values and their impact on the number of terminal sites, we have created a function that takes the parameters, data, and distance matrix and outputs just the number of terminals. + +Let's run this for a range of plausible parameter values: + +
+

If you attempt to run this on your own note that it takes quite a +while to complete.

+
+ + + +```r +source("scripts/terminals_by_par.R") + +alpha_ls <- seq(0.90, 1.25, by = 0.01) +beta_ls <- seq(0.05, 0.40, by = 0.01) + +res <- matrix(NA, length(alpha_ls), length(beta_ls)) +row.names(res) <- alpha_ls +colnames(res) <- beta_ls + +for (i in seq_len(length(alpha_ls))) { + for (j in seq_len(length(beta_ls))) { + res[i, j] <- + terminals_by_par( + alpha = alpha_ls[i], + beta = beta_ls[j], + dist_mat = d + ) + } +} +``` + +In case you want to see the data but don't want to wait for the above chunk to run, we have created an Rdata object with the output. Let's load those data and plot them as a heat map/tile plot: + + +```r +load(file = "data/retail_pars.Rdata") + +library(reshape2) +library(ggraph) +res_df <- melt(res) + +ggplot(data = res_df) + + geom_tile(aes(x = Var2, y = Var1, fill = value)) + + scale_fill_viridis(option = "turbo") + + xlab(expression( ~ beta)) + + ylab(expression( ~ alpha)) + + theme_bw() +``` + + + +As this plot shows low values for both $\alpha$ and $\beta$ tend to generate networks with lots of terminals but the relationship between these parameters is not linear. Based on this and our knowledge of the archaeological record, we could make an argument for evaluating a particular combination of parameters but there is certainly no single way to make that decision. To see further expansions of such an approach that attempts to deal with incomplete survey data and other kinds of considerations of settlement prominence, see the published example by [Bevan and Wilson (2013)](https://www.researchgate.net/publication/257154745_Models_of_settlement_hierarchy_based_on_partial_evidence). + +## Truncated Power Functions{#TruncatedPower} + +Another similar spatial interaction model was used in a study by [Menze and Ur (2012)](https://dash.harvard.edu/handle/1/8523994) in their exploration of networks in northern Mesopotamia. Their model is quite similar to the simple gravity model we saw above but with a couple of additional parameters and constraints. We leave the details of the approach to the published article but briefly describe their model here. This truncated power function requires information on settlement location, some measure of size or "attraction," and three parameter values. Edge interaction $E_{ij}$ in this model can be formally defined as: + +$$E_{ij}(\alpha,\beta,\gamma) = V_i^\gamma V_j^\gamma d_{ij}^{-\alpha} e^{(-d_{ij}/\beta)}$$ + +where + +* $V$ is some measure of the attractiveness of node $i$ or $j$, typically defined in terms of settlement size. +* $d_{ij}$ is the distance between nodes $i$ and $j$. Again, this can use measures of distance other than simple Euclidean distances. +* $\alpha$ is a constraint on the distance between nodes. +* $\beta$ is the physical distance across which distance decay should be considered (defined in units of $d$). +* $\gamma$ is used to define the importance of $V$ on interaction where values above 1 suggest increasing returns to scale. + +The model output $E_{ij}$ is, according to Menze and Ur, meant to approximate the movement of people among nodes across the landscape. In order to evaluate this function, we replicate the results of the Menze and Ur paper with one small change. We drop the bottom 50% smallest sites from consideration due to the large sample size to keep run times manageable (but this could certainly be changed in the code below). We use the replication data set provided by Menze and Ur online [here](https://dataverse.harvard.edu/dataset.xhtml;jsessionid=adeb8ff43c833f1efe447dc9e8ba?persistentId=hdl%3A1902.1%2F17731&version=&q=&fileTypeGroupFacet=%22Text%22&fileAccess=&fileTag=&fileSortField=type&fileSortOrder=). + +Let's read in the data, omit the rows without site volume estimates, and then remove the lowest 50% of sites in terms of volume values. We then plot the sites with points scaled by site volume. [Download the data here to follow along](data/menze_ur_sites.csv). + + +```r +mesop <- read.csv("data/menze_ur_sites.csv") +mesop <- mesop[-which(is.na(mesop$volume)),] +mesop <- mesop[which(mesop$volume > quantile(mesop$volume, 0.5)),] + +ggplot(mesop) + + geom_point(aes(x = x, y = y, size = volume), + show.legend = FALSE) + + scale_size_continuous(range = c(1, 3)) + + theme_void() +``` + + + +And here we implement the truncated power approach rolled into a function called `truncated_power`. We use the values selected as optimal for the Menze and Ur (2012) paper. + +
+

Note that the block below takes several minutes to run.

+
+ + + +```r +d <- as.matrix(dist(mesop[, 1:2])) / 1000 + +truncated_power <- function (V, d, a, y, B) { + temp <- matrix(0, nrow(d), ncol(d)) + for (i in seq_len(nrow(d))) { + for (j in seq_len(ncol(d))) { + temp[i, j] <- V[i] ^ y * V[j] ^ y * d[i, j] ^ -a * exp(-d[i, j] / B) + if (temp[i, j] == Inf) { + temp[i, j] <- 0 + } + } + } + return(temp) +} + +res_mat <- truncated_power(V = mesop$volume, d = d, a = 1, y = 1, B = 4) +``` + +We can now plot the sites again, this time with points scaled based on the total volume of flow incident on each node. + + +```r +edge_flow <- rowSums(res_mat) + +ggplot(mesop) + + geom_point(aes( + x = x, + y = y, + size = edge_flow, + alpha = edge_flow + ), + show.legend = FALSE) + + scale_size_continuous(range = c(1, 3)) + + theme_void() +``` + + + +If we compare the plot above to figure 8 in Menze and Ur (2012) we see highly central sites in the same locations suggesting that we've reasonably approximated their results even though we are using a slightly different sample. Further, as the next plot shows, if we remove the few sites that are isolated in our sample (due to us removing the bottom 50% of sites) we also see the same strong linear correlation between the log of site volume and the log of our measure of interaction. + + +```r +rem_low <- which(edge_flow > quantile(edge_flow, 0.01)) + +library(ggplot2) +library(scales) +df <- data.frame(Volume = mesop$volume[rem_low], Interaction = edge_flow[rem_low]) + +ggplot(data = df) + + geom_point(aes(x = Volume, y = Interaction)) + + scale_x_log10( + breaks = trans_breaks("log10", function(x) + 10 ^ x), + labels = trans_format("log10", math_format(10 ^ .x)) + ) + + scale_y_log10( + breaks = trans_breaks("log10", function(x) + 10 ^ x), + labels = trans_format("log10", math_format(10 ^ .x)) + ) + + theme_bw() +``` + + + +We could go about parameterizing this truncated power function in much the same way that we saw with the models above (i.e., testing values and evaluating results against the archaeological pattern). Indeed that is what Menze and Ur do but with a slight twist on what we've seen so far. In their case, they are lucky enough to have remotely sensed data on actual trails among sites for some portion of their study area. How they selected model parameters is by testing a range of values for all parameters and selecting the set that produced the closest match between site to site network edges and the orientations of actual observed trails (there are some methodological details I'm glossing over here so refer to the article for more). As this illustrates, there are many potential ways to select model parameters based on empirical information. + +## Radiation Models{#RadiationModels} + +In 2012 Filippo Simini and colleagues ([Simini et al. 2012](https://dspace.mit.edu/handle/1721.1/77896)) presented a new model, designed specifically to model human geographic mobility called the radiation model. This model was created explicitly as an alternative to various gravity models and, in certain cases, was demonstrated to generate improved empirical predictions of human population movement between origins and destinations. This model shares some basic features with gravity models but importantly, the approach includes no parameters at all. Instead, this model uses simply measures of population at a set of sites and the distances between them. That is all that is required so this model is relatively simple and could likely be applied in many archaeological cases. Let's take a look at how it is formally defined: + +$$T_{ij} = T_i \frac{m_in_j}{(m_i + s_{ij})(m_i + n_j + s_{ij})}$$ + +where + +* $T_i$ is the total number of "commuters" or migrating individuals from node $i$. +* $m_i$ and $n_j$ are the population estimates of nodes $i$ and $j$ respectively. +* $s_{ij}$ is the total population in a circle centered at node $i$ and touching node $j$ excluding the populations of both $i$ and $j$. + +We have defined a function for calculating radiation among a set of sites using just two inputs: + +* **`pop`** is a vector of population values. +* **`d_mat`** is a distance matrix among all nodes. + +A script containing this function can also be downloaded [here](scripts/radiation.R) + + +```r +radiation <- function(pop, d_mat) { + ## create square matrix with rows and columns for every site + out <- + matrix(0, length(pop), length(pop)) + for (i in seq_len(length(pop))) { + # start loop on rows + for (j in seq_len(length(pop))) { + # start loop on columns + if (i == j) + next() + # skip diagonal of matrix + m <- pop[i] # set population value for site i + n <- pop[j] # set population value for site j + # find radius as distance between sites i and j + r_ij <- + d_mat[i, j] + # find all sites within the distance from i to j centered on i + sel_circle <- + which(d_mat[i, ] <= r_ij) + # remove the site i and j from list + sel_circle <- + sel_circle[-which(sel_circle %in% c(i, j))] + s <- sum(pop[sel_circle]) # sum population within radius + # calculate T_i and output to matrix + temp <- + pop[i] * ((m * n) / ((m + s) * (m + n + s))) + if (is.na(temp)) temp <- 0 + out[i, j] <- temp + } + } + return(out) +} +``` + +In order to test this model, we will use the same Wankarani settlement data we used above for the simple gravity model. We will use site area divided by 500 as our proxy for population here. Again we limit our sample to Late Intermediate period sites and habitations. [Download the data here to follow along](data/Wankarani_siteinfo.csv). + + + +```r +wankarani <- read.csv("data/Wankarani_siteinfo.csv") +wankarani <- wankarani[which(wankarani[["Period"]] == "Late Intermediate"), ] +wankarani <- wankarani[which(wankarani[["Type"]] == "habitation"), ] + +d <- as.matrix(dist(wankarani[, 5:6])) + +rad_test <- radiation(pop = wankarani[["Area"]] / 500, d_mat = d) +``` + +Now let's plot the resulting network with each node scaled by the total incident flow (row sums of the output of the function above). We plot network edges with weights indicated by color (blue indicates low weight and yellow indicates high weight). + + +```r +library(igraph) +library(ggraph) +library(sf) +library(ggmap) + +load("data/bolivia.Rdata") + +locations_sf <- + st_as_sf(wankarani, coords = c("Easting", "Northing"), crs = 32721) +loc_trans <- st_transform(locations_sf, crs = 4326) +coord1 <- do.call(rbind, st_geometry(loc_trans)) %>% + tibble::as_tibble() %>% + setNames(c("lon", "lat")) + +xy <- as.data.frame(coord1) +colnames(xy) <- c("x", "y") + + +net <- + graph_from_adjacency_matrix(rad_test, mode = "undirected", + weighted = TRUE) + +# Extract edge list from network object +edgelist <- igraph::as_edgelist(net) + +# Create data frame of beginning and ending points of edges +edges <- + data.frame(xy[edgelist[, 1], ], + xy[edgelist[, 2], ]) +colnames(edges) <- c("X1", "Y1", "X2", "Y2") + +# Calculate weighted degree +dg <- rowSums(rad_test) + +# Plot data on map +ggmap(base_bolivia, darken = 0.35) + + geom_segment( + data = edges, + aes( + x = X1, + y = Y1, + xend = X2, + yend = Y2, + col = log(E(net)$weight), + alpha = log(E(net)$weight) + ), + show.legend = FALSE + ) + + scale_alpha_continuous(range = c(0, 0.5)) + + scale_color_viridis() + + geom_point( + data = xy, + aes(x, y, size = dg), + alpha = 0.8, + color = "red", + show.legend = FALSE + ) + + theme_void() +``` + + + +This map shows clusters of higher and lower edge weights and again variation in total weighted degree (with higher values in the east). The results are similar, but not identical to the output of the simple gravity model. + + +```r +dg_grav <- rowSums(grav_mod( + attract = wankarani[["Area"]] / 1000, + B = 1, + d = d_mat +)) + +dg_rad <- rowSums(rad_test) + +library(ggplot2) +library(scales) +df <- + data.frame(Radiation = dg_rad, Gravity = dg_grav) + +ggplot(data = df) + + geom_point(aes(x = Radiation, y = Gravity)) + + scale_x_log10( + breaks = trans_breaks("log10", function(x) + 10 ^ x), + labels = trans_format("log10", math_format(10 ^ .x)) + ) + + scale_y_log10( + breaks = trans_breaks("log10", function(x) + 10 ^ x), + labels = trans_format("log10", math_format(10 ^ .x)) + ) + + theme_bw() +``` + + + +We are aware of few published examples of the use of radiation models for archaeological cases, but there is certainly potential (see Evans 2016). + +## Other Spatial Interaction Models{#OtherModels} + +There are many other spatial interaction models we haven't covered here. Most are fairly similar in that they take information on site size, perhaps other relevant archaeological information, and a few user selected parameters to model flows across edges and sometimes to iteratively predict sizes of nodes, the weights of flows, or both. Other common models we haven't covered here include the XTENT model (Renfrew and Level 1979; see Ducke and Suchowska 2021 for an example with code for GRASS GIS) and various derivations of MaxEnt (or maximum entropy) models. Another approach that merits mention here is the [ariadne model](https://figshare.com/articles/dataset/ariadne/97746) designed by Tim Evans and used in collaboration with Ray Rivers, Carl Knappett, and others. This model provides a means for predicting site features and estimating optimal networks based on location and very general size information (or other archaeological features). This model has features that make it particularly useful for generating directed spatial networks (see [Evans et al. 2011](https://plato.tp.ph.ic.ac.uk/~time/networks/arch/interactionsArxivSubmissionV2.pdf)). Although there is a basic R implementation for the ariadne model developed by the ISAAKiel team [available here](https://rdrr.io/github/CRC1266-A2/moin/src/R/hamiltonian.R) the computational constraints make this function unfeasible in R for all but very small networks. Instead, if you are interested in applying the ariadne model, we suggest you use the original Java program created by Tim Evans and [available here](https://figshare.com/articles/dataset/ariadne/97746). diff --git a/09-affiliation.Rmd b/09-affiliation.Rmd index fee443a..859cd37 100644 --- a/09-affiliation.Rmd +++ b/09-affiliation.Rmd @@ -348,12 +348,12 @@ where $N_p$ is the number of connections from mode 1 to mode 2 for node $p$ and The Newman method of weighting bipartite networks has been implemented in an R package called `tnet`. This package has a few other useful functions for the analysis of bipartite, weighted, and longitudinal networks so it is worth investigating (see `?tnet`). Unfortunately, it is no longer being actively maintained. ``` -Let's take a look at Newman's method using the `tnet` package. This package expects a simple two-column edge list with only integers for the node identifiers which we can generate using the `igraph` `get.edgelist` function and including the `names = FALSE` argument: +Let's take a look at Newman's method using the `tnet` package. This package expects a simple two-column edge list with only integers for the node identifiers which we can generate using the `igraph::as_edgelist` function and including the `names = FALSE` argument: ```{r, warning=F, message=F, fig.height=7, fig.width=7} library(tnet) -tm_el <- get.edgelist(cibola_inc, names = FALSE) +tm_el <- igraph::as_edgelist(cibola_inc, names = FALSE) head(tm_el) proj_newman <- as.matrix(projecting_tm(tm_el, method = "Newman")) @@ -418,7 +418,7 @@ col_net <- graph_from_adjacency_matrix(crossprod(as.matrix(cibola_clust)), diag = FALSE) # Combine both edge lists into a single frame -el_com <- rbind(get.edgelist(cibola_om_reduced), get.edgelist(col_net)) +el_com <- rbind(igraph::as_edgelist(cibola_om_reduced), igraph::as_edgelist(col_net)) # Define composite network object and add Edge and Vertex attributes net2 <- graph_from_edgelist(el_com, directed = FALSE) diff --git a/09-affiliation.md b/09-affiliation.md new file mode 100644 index 0000000..9249400 --- /dev/null +++ b/09-affiliation.md @@ -0,0 +1,753 @@ +# Affiliation Data and Co-Association{#Affiliation} + +![](images/image_break.png){width=100%} + +Many of the material cultural networks that archaeologists have generated in recent studies are based, at least in part, on affiliation data. An affiliation network is a particular form of network defined in terms of what are often called "actors" and "events." Typically, such data are used to generate a bipartite (two-mode) network where one set of nodes represents a set of social entities (individuals, groups, etc.) and the second set of nodes represent some set of features or events they can have in common or attend. For example, the classic affiliation data set is refereed to as the *Deep South* case study which represents data on a group of women in a southern town and the social events in which they participated (Davis et al. 1941). An affiliation network is defined connecting people to events in this case based on the notion that people who attended more events together had more opportunities to interact, or perhaps that their co-attendance was a reflection of other social relationships (see Breiger 1974). Similarly, events that had many of the same attendees could also be thought of as being more strongly connected than events with very different rosters of participants. The bipartite network created connecting these two classes of nodes are often further projected into distinct one-mode networks of person-to-person and event-to-event relationships for further analyses. + +Although this is not always explicitly discussed, the affiliation network framework mirrors many archaeological network constructions where sites/regions/contexts are connected via the materials present in those contexts. For example, sites may stand in for "persons" in such a network and artifacts categories as "events" with the underlying reasoning being that the inhabitants of sites that share more categories of artifacts were more likely to have interacted than the inhabitants of sites with very different materials. In most archaeological examples where such data have been used (e.g., Coward 2013; Golitko et al. 2012; Mizoguchi 2013; Mills et al. 2013, 2015, etc.) these affiliation data are projected into a single one-mode network focused on sites/contexts and that network is used for most formal analyses. This is not, of course, the only path forward. There are examples of archaeologists conducting direct analyses of two-mode data (e.g., Blair 2015, 2017; Ladefoged et al. 2019, etc.). The consideration of material networks as affiliation networks also opens up the possibility of many additional methods that have as of yet been rare in archaeological network research. In this section, we outline a few approaches that may be of use as archaeologists continue to experiment with such affiliation data. + +## Analyzing Two-Mode Networks{#AnalyzingTwoMode} + +In the examples here we will be using the [Cibola data set](#Cibola) used throughout this document. The specific data we will use consist of a set of sites as the first mode and a set of ceramic technological clusters as the second mode. Our underlying assumption is that sites that share more ceramic technological clusters are more strongly connected than sites that share fewer. Further, ceramic technological clusters that are frequently co-associated in site assemblages are more closely connected than those that do not frequently co-occur. [Download the data here to follow along](data/Cibola_clust.csv). + +Let's read in the data and create a simple two-mode network visualization to start by reading in the Cibola incidence matrix. We will be using the `igraph` package for most of the examples below so we initialize that as well as the `ggraph` package for plotting: + + +```r +library(igraph) +library(ggraph) + +# Read in two-way table of sites and ceramic technological clusters +cibola_clust <- read.csv(file = "data/Cibola_clust.csv", + header = TRUE, + row.names = 1) +# Create network from incidence matrix based on presence/absence of +# a cluster at a site +cibola_inc <- igraph::graph_from_incidence_matrix(cibola_clust, + directed = FALSE) +# Plot as two-mode network +set.seed(4643) +ggraph(cibola_inc) + + geom_edge_link(aes(size = 0.5), color = "gray", show.legend = FALSE) + + geom_node_point(aes(color = as.factor(V(cibola_inc)$type), + size = 4), + show.legend = FALSE) + + geom_node_text(aes(label = name), size = 3, repel = TRUE) + + theme_graph() +``` + + + + +### Using Traditional Network Metrics{#TraditionalMetrics} + +There are several possible approaches for analyzing two-mode network data. Perhaps the simplest approach is to analyze two-mode data using typical one-mode metrics like we've already seen throughout this guide. Essentially, this is akin to treating both modes as equivalent and evaluating relative positions and structures between node classes. If you send a bipartite network object to the typical network measures outlined in the [Exploratory Analysis](#Exploratory) section, you will get results returned as if it were a one-mode network. For example, here we apply two measures of centrality and plot the results. + + +```r +dg_bi <- igraph::degree(cibola_inc) +bw_bi <- igraph::betweenness(cibola_inc) + +# Plot as two-mode network with size by centrality +set.seed(4643) +ggraph(cibola_inc) + + geom_edge_link(aes(size = 0.5), color = "gray", show.legend = FALSE) + + geom_node_point(aes(color = as.factor(V(cibola_inc)$type), + size = dg_bi), + show.legend = FALSE) + + geom_node_text(aes(label = name), size = 3, repel = TRUE) + + ggtitle("Node Size by Degree") + + theme_graph() +``` + + + +```r +set.seed(4643) +ggraph(cibola_inc) + + geom_edge_link(aes(size = 0.5), color = "gray", show.legend = FALSE) + + geom_node_point(aes(color = as.factor(V(cibola_inc)$type), + size = bw_bi), + show.legend = FALSE) + + geom_node_text(aes(label = name), size = 3, repel = TRUE) + + ggtitle("Node Size by Betweenness") + + theme_graph() +``` + + + +This method could be useful if you are interested in determining relative centrality between classes of nodes, in particular where the numbers of nodes in each mode are similar. In the example here, for both degree and betweenness centrality the mode made of of ceramic technological clusters (in blue) seems to include the most central nodes, but there are a few exceptions. Importantly, however, there is an imbalance in the size of each mode so it is important to consider the potential impact of such differences. + +### Using Two-Mode Specific Network Metrics{#TwoModeMetrics} + +In addition to the traditional approach to calculating network metrics for bipartite networks using the same one-mode metrics we've previously used, there are also methods designed specifically to work with two-mode network data. Unfortunately, most of these metrics have not been incorporated into robust and currently maintained packages for R. Many of these approaches represent simply normalizations of existing network metrics, however, so it is possible to create our own custom versions without too much trouble. + +#### Density{-} + +For example, if we are interested in network density, it doesn't make sense to simply use regular density measures as that assumes any node can be connected to any other node. In a two-mode network, nodes can only be connected *between* classes. Thus, to obtain appropriate two-mode density, we need to divide density by a factor defined as: + +$$\frac{n_1n_2}{(n_1+n_2)(n_1+n_2-1)}$$ +where $n_1$ and $n_2$ represent the number of nodes in modes 1 and 2 respectively. + +Let's give this a try by first calculating density the traditional (`den_init`) way and then correcting it (`den_corr`). Note that we divide our initial density by 2 because we only want density counted based on connections in one direction. + + +```r +# edge density divided by 2 because we only want edges counted in one direction +den_init <- edge_density(cibola_inc) / 2 +den_init +``` + +``` +## [1] 0.145122 +``` + +```r +# number of nodes in first mode +n1 <- length(which(V(cibola_inc)$type == FALSE)) +n1 +``` + +``` +## [1] 31 +``` + +```r +# number of nodes in second mode +n2 <- length(which(V(cibola_inc)$type == TRUE)) +n2 +``` + +``` +## [1] 10 +``` + +```r +den_corr <- den_init / ((n1 * n2) / ((n1 + n2) * (n1 + n2 - 1))) +den_corr +``` + +``` +## [1] 0.7677419 +``` + +As this example shows, our initial density estimate was quite low at about 0.145 but once we correct for node class, we get a quite dense network of about 0.768. This makes sense given how many active edges we see in the figures above. The high centrality suggests that most nodes in mode 1 have connections to most nodes in mode 2. + +#### Degree Centrality{-} + +Let's take a look at degree centrality next. As we saw in the previous section it is possible to calculate degree centrality using the traditional metric and simply plotting that. In the plot shown previously, most of the "Technological Cluster" nodes shown in blue had much higher degree than the sites. This isn't surprising given that there are 31 sites and 10 ceramic clusters and such a high two-mode density. In other words, the theoretically possible degree for the technological cluster nodes is more than 3 times higher than that for sites so it isn't surprising that our observed values are higher for mode two. One easy way to deal with degree in two-mode network is to normalize by the size of the opposite node class. For example, degree centrality in one-mode networks can be normalized as: + +$$d_i^* = \frac{d_i}{n-1}$$ + +where $d_i$ is the original degree for node $i$ and $n$ is the number of nodes in the network. + +For two-mode networks, the standardization would take the following form: + +$$\begin{aligned} +d^*_{i} =& \frac{d_{i}}{n_2} \text{, for } i \in V_1 \\ +d^*_{j} =& \frac {d_{j}}{n_1} \text{, for } j \in V_2 +\end{aligned}$$ + +where + +* $d_{i}$ is the degree of node $i$ in mode $V_1$ +* $d_{j}$ is the degree of node $j$ in mode $V_2$ +* $n_1$ is the number of nodes in mode $1$ +* $n_2$ is the number of nodes in mode $2$ + +Let's give this a try with our Cibola ceramic technological clusters data. We roll this into a function for convenience: + + +```r +degree_twomode <- function(net) { + n1 <- length(which(V(net)$type == FALSE)) + n2 <- length(which(V(net)$type == TRUE)) + temp_dg <- igraph::degree(net, mode = "in") + dg_n1 <- temp_dg[which(V(net)$type == FALSE)] / n2 + dg_n2 <- temp_dg[which(V(net)$type == TRUE)] / n1 + return(c(dg_n1, dg_n2)) +} + +dg_tm <- degree_twomode(cibola_inc) +dg_tm +``` + +``` +## Apache Creek Atsinna Baca Pueblo +## 0.8000000 0.6000000 0.8000000 +## Casa Malpais Cienega Coyote Creek +## 0.9000000 0.8000000 0.9000000 +## Foote Canyon Garcia Ranch Heshotauthla +## 1.0000000 0.7000000 0.7000000 +## Hinkson Hooper Ranch Horse Camp Mill +## 1.0000000 0.8000000 0.9000000 +## Hubble Corner Jarlosa Los Gigantes +## 0.8000000 0.6000000 0.5000000 +## Mineral Creek Pueblo Mirabal Ojo Bonito +## 0.9000000 0.7000000 0.6000000 +## Pescado Cluster Platt Ranch Pueblo de los Muertos +## 0.7000000 0.8000000 0.6000000 +## Rudd Creek Ruin Scribe S Spier 170 +## 0.9000000 0.6000000 0.6000000 +## Techado Springs Tinaja Tri-R Pueblo +## 1.0000000 0.7000000 0.9000000 +## UG481 UG494 WS Ranch +## 1.0000000 0.8000000 0.9000000 +## Yellowhouse Clust1 Clust2 +## 0.3000000 0.6129032 1.0000000 +## Clust3 Clust4 Clust5 +## 1.0000000 0.9032258 0.7741935 +## Clust6 Clust7 Clust8 +## 0.9354839 0.9032258 0.5161290 +## Clust9 Clust10 +## 0.6774194 0.3548387 +``` + +```r +# Plot as two-mode network with size by centrality +set.seed(4643) +ggraph(cibola_inc) + + geom_edge_link(aes(size = 0.5), color = "gray", show.legend = FALSE) + + geom_node_point(aes(color = as.factor(V(cibola_inc)$type), + size = dg_tm), + show.legend = FALSE) + + geom_node_text(aes(label = name), size = 3, repel = TRUE) + + ggtitle("Node Size by Two-Mode Normalized Degree") + + theme_graph() +``` + + + +As this shows, after normalization most of the nodes have similar degree values with a couple of low degree nodes. If we plot the normalized degree distributions for both the one-mode method and the two-mode normalization as density plots, the difference is even more obvious. + + +```r +dg_orig <- igraph::degree(cibola_inc, mode = "in", normalized = TRUE) + +dg_all <- c(dg_orig, dg_tm) +dg_lab <- c(rep("one-mode degree", 41), rep("two-mode degree", 41)) + +df <- data.frame(dg = dg_all, group = dg_lab) + +ggplot(df) + + geom_density(aes(x = dg, fill = group), alpha = 0.5) + + xlim(range = c(0, 1)) + + theme_bw() +``` + + + +Indeed, the one-mode metric suggests that most nodes have low degree whereas the two-mode metric suggests most nodes have high degree. This demonstrates how important it is to modify traditional network metrics for the two-mode use case to assess distributional features like this. + +#### Betweenness Centrality {-} + +Let's now try the same for betweenness centrality. The normalization of betweenness is a bit more complicated as it involves shortest paths rather than just counts of nodes. For one mode networks, betweenness is typically normalized by dividing results by $(n-1)(n-2)$. For the case of two-mode networks the following equations are used: + +$$\begin{aligned} +b_{v_1\text{max}} = & \frac{1}{2}[n_2^2 (s+1)^2 + n_2 (s+1) (2t-s-1)-t(2s-t+3))] \\ +b_{v_2\text{max}} = & \frac{1}{2}[n_1^2 (p+1)^2 + n_1 (p+1) (2r-p-1)-t(2p-r+3))] +\end{aligned} +$$ + +$$\begin{aligned} +s =& (n_1 - 1) \text { div } n_2 \\ +t =& (n_1 - 1) \text{ mod } n_2 \\ +p =& (n_2 - 1) \text { div } n_1 \\ +r =& (n_1 - 1) \text { mod } n_2 +\end{aligned}$$ + +where + +* $b_{v_1\text{max}}$ is the theoretical maximum betweenness for mode 1 and $b_{v_2\text{max}}$ is the same for mode 2 +* $n_1$ and $n_2$ are the number of nodes in modes 1 and 2 respectively +* $\text {div}$ refers to integer division where any numbers past the decimal point are dropped after the division operation +* $\text {mod}$ refers to modulo where only numbers beyond the decimal point are retained after the division operation + +Using these equations we can then calculate normalized betweenness for two-modes as: + +$$\begin{aligned} +b^*_i =& \frac{b_i}b_{v_1\text{max}} \text {, for } i \in V_1\\ +b^*_j =& \frac{b_j}b_{v_2\text{max}} \text {, for } j \in V_2 +\end{aligned}$$ + +In the following chunk of code, we roll these equations into a function and then calculate two-mode betweenness and plot it. For the sake of convenience, we have placed both of the functions for two-mode centrality created here into a [script which you can download here](scripts/twomode.R). + + +```r +betweenness_twomode <- function(net) { + n1 <- length(which(V(net)$type == FALSE)) + n2 <- length(which(V(net)$type == TRUE)) + temp_bw <- igraph::betweenness(net, directed = FALSE) + s_v <- round((n1 - 1) / n2, 0) + t_v <- (n1 - 1) %% n2 + p_v <- round((n2 - 1) / n1, 0) + r_v <- (n1 - 1) %% n2 + bw_v1 <- + 0.5 * (n2^2 * (s_v + 1)^2 + n2 * (s_v + 1) * + (2 * t_v - s_v - 1) - t_v * (2 * s_v - t_v + 3)) + bw_v2 <- + 0.5 * (n1^2 * (p_v +1)^2 + n1 * (p_v +1) * (2 * r_v - p_v - 1) * + r_v * (2 * p_v - r_v + 3)) + bw_n1 <- temp_bw[which(V(net)$type == FALSE)] / bw_v2 + bw_n2 <- temp_bw[which(V(net)$type == TRUE)] / bw_v1 + return(c(bw_n1, bw_n2)) +} + +bw_tm <- betweenness_twomode(cibola_inc) +bw_tm +``` + +``` +## Apache Creek Atsinna Baca Pueblo +## 0.0070822286 0.0025896615 0.0093451935 +## Casa Malpais Cienega Coyote Creek +## 0.0099600613 0.0100144209 0.0125792905 +## Foote Canyon Garcia Ranch Heshotauthla +## 0.0161688627 0.0042695215 0.0040337432 +## Hinkson Hooper Ranch Horse Camp Mill +## 0.0161688627 0.0070822286 0.0099600613 +## Hubble Corner Jarlosa Los Gigantes +## 0.0102638260 0.0025896615 0.0017552743 +## Mineral Creek Pueblo Mirabal Ojo Bonito +## 0.0125792905 0.0040337432 0.0025896615 +## Pescado Cluster Platt Ranch Pueblo de los Muertos +## 0.0051844595 0.0070822286 0.0025896615 +## Rudd Creek Ruin Scribe S Spier 170 +## 0.0133076983 0.0022732239 0.0030177883 +## Techado Springs Tinaja Tri-R Pueblo +## 0.0161688627 0.0040337432 0.0099600613 +## UG481 UG494 WS Ranch +## 0.0161688627 0.0070822286 0.0133076983 +## Yellowhouse Clust1 Clust2 +## 0.0002542476 0.0348523775 0.1297623337 +## Clust3 Clust4 Clust5 +## 0.1297623337 0.0927886415 0.0634360762 +## Clust6 Clust7 Clust8 +## 0.1134973990 0.0960766642 0.0224202375 +## Clust9 Clust10 +## 0.0529783153 0.0102589547 +``` + +```r +# Plot as two-mode network with size by centrality +set.seed(4643) +ggraph(cibola_inc) + + geom_edge_link(aes(size = 0.5), color = "gray", show.legend = FALSE) + + geom_node_point(aes(color = as.factor(V(cibola_inc)$type), + size = bw_tm), + show.legend = FALSE) + + geom_node_text(aes(label = name), size = 3, repel = TRUE) + + ggtitle("Node Size by Two-Mode Normalized Betweenness") + + theme_graph() +``` + + + +This plot suggests that the normalized betweenness is quite similar the original one-mode measure in this particular network, but that will not necessarily always be the case. + +There are similar normalization factors for other centrality measures in the published network literature ([see this document by Borgatti for examples](http://www.analytictech.com/borgatti/papers/2modeconcepts.pdf)) but few of these have been implemented in R as of yet. This would be a useful project in the future (and perhaps we will add that here eventually. [Want to help?](#Contributing)). + +### Projecting Two-Mode Networks Before Analysis{#ProjectingTwoMode} + +Another common approach to the analysis of two-mode networks is to project them into two separate one-mode networks and then to analyze one or both modes using traditional one-mode metrics. We have already seen this approach in several examples throughout this guide. Indeed, this is the most common approach that archaeological network studies have taken. As we describe in the [Network Data Formats](#NetworkData) section, there are several ways of projecting an incidence matrix into one-mode networks. This includes the matrix multiplication method which counts the numbers of co-occurrences that we already described [in our coverage of two-mode networks](#TwoMode) as well as various similarity metrics for producing weighted networks based on measures such as Brainerd-Robinson similarity, $\chi$-square distance, Jaccard similarity, and many others discussed in the [similarity networks](#SimilarityNetworks) section. Although such networks are not often explicitly described in terms of affiliation networks, they very much fit the definition. In this section, we offer a slightly expanded discussion of some of these methods to highlight important issues for affiliation data specifically. + +#### Matrix Multiplication{-} + +As [prevsiously described](#TwoMode) one of the most common ways for generating one mode projections from two-mode data is through matrix multiplication. Specifically, if you multiply a matrix $A$ by the transpose of that same matrix $A^T$, you will get a square matrix with the number of rows and columns equal to the rows in the original matrix with each cell representing the number of intersections between the two modes (and with the diagonal of the matrix representing the number of opposite mode categories present in the mode under consideration). Let's take a look at this process formally: + +$$\begin{equation} +A \cdot A^T = +\begin{pmatrix} +a & b \\ +c & d +\end{pmatrix} + \cdot +\begin{pmatrix} +a & c \\ +b & d +\end{pmatrix} = +\begin{pmatrix} +aa + bb & ac+bd \\ +ca +db & cc + dd +\end{pmatrix} +\end{equation}$$ + +If we assume the original matrix contains only 0s and 1s then we will end up with the intersection of the two network modes in the diagonal as describe above. + +There are a couple of ways to conduct this procedure in R. Previously we used the `%*%` operator for matrix multiplication and the `t()` transpose function to calculate a matrix multiplied by its transpose, but it is also possible to use an R built-in function called `crossprod` to do the same thing. Indeed, if you are working on large matrices, which can be computationally expensive, the `crossprod` function is considerably faster. In the chunk below we calculate a square matrix for the `cibola_clust` data set using both methods and then subtract the results from each other to ensure that they are identical. + + +```r +mat1 <- as.matrix(cibola_clust) +mat1 <- ifelse(mat1 > 0, 1, 0) + +res1 <- mat1 %*% t(mat1) + +# this does the same as the above +res2 <- crossprod(t(mat1)) + +# Check to see if they are identical +max(res1-res2) +``` + +``` +## [1] 0 +``` + +Another important feature of a one-mode network generated in this way is that it can be treated as a weighted network by simply including the `weighted = TRUE` argument in the call. For example: + + +```r +diag(res1) <- 0 +cibola_onemode <- graph_from_adjacency_matrix(res1, weighted = TRUE) + +set.seed(4643) +ggraph(cibola_onemode) + + geom_edge_link(aes(alpha = weight, color = weight), + width = 1, show.legend = FALSE) + + scale_edge_alpha_continuous(range = c(0, 0.5)) + + scale_edge_color_continuous() + + geom_node_point(aes(size = igraph::degree(cibola_onemode)), + show.legend = FALSE) + + geom_node_text(aes(label = name), size = 3, repel = TRUE) + + theme_graph() +``` + + + +That produces a hairball that is fairly hard to visually interpret. To try to ameliorate that we can borrow a function we previously created in the [two-mode networks](#TwoMode) discussion in the Network Data section and only consider a ceramic cluster present at a site if it makes up at least 20% of the assemblage. Let's try this: + + + +```r +two_mode <- function(x, thresh = 0.2) { + # Create matrix of proportions from x input into function + temp <- prop.table(as.matrix(x), 1) + # Define anything with greater than or equal to threshold as + # present (1) + temp[temp >= thresh] <- 1 + # Define all other cells as absent (0) + temp[temp < 1] <- 0 + # Return the new binarized table as output of the function + return(temp) +} + +mat_new <- two_mode(cibola_clust, thresh = 0.2) + +res3 <- crossprod(t(mat_new)) + +cibola_om_reduced <- graph_from_adjacency_matrix(res3, weighted = TRUE) + +set.seed(4643) +ggraph(cibola_om_reduced) + + geom_edge_link(aes(alpha = weight, color = weight), + width = 1, show.legend = FALSE) + + scale_edge_alpha_continuous(range = c(0.1, 1)) + + scale_edge_color_gradient() + + geom_node_point(aes(size = igraph::degree(cibola_om_reduced)), + show.legend = FALSE) + + geom_node_text(aes(label = name), size = 3, repel = TRUE) + + theme_graph() +``` + + + +That is a lot easier to interpret. We've got one cluster of quite strong ties and then a second cluster characterized by weaker ties with relatively week ties between the clusters. Notably, this pattern is very similar to the pattern seen in the Brainerd-Robinson similarity matrices generated using these data which isn't too surprising. + +#### Weighted Matrix Projection{-} + +We have already described several similarity metrics in detail in [the similarity networks section of this document](#SimilarityNetworks). In this section, we want to offer one additional approach developed by Newman (2001) for defining connections in scientific collaboration networks. Newman wanted to extend the procedure for assessing co-occurrence to take into account the number of collaborators involved in each collaboration. Specifically, he posited that when there were fewer collaborators, the connection between a pair of co-authors was stronger than when there were many co-authors. We could equivalently think of this in terms of material culture to suggest that sites or contexts that shared rare categories were more strongly connected than sites/contexts that shared only common categories. Using this logic, Newman created a new weighting scheme for co-occurrence networks where the weight of a connection between nodes $i$ and $j$ is the defined in terms of the number of cross-mode connections for that node. In other words: + +$$w_{ij} = \Sigma_p \frac{1}{N_p-1}$$ + +where $N_p$ is the number of connections from mode 1 to mode 2 for node $p$ and $w_{ij}$ is the weight of the connection from $i$ to $j$. + +
+

The Newman method of weighting bipartite networks has been +implemented in an R package called tnet. This package has a +few other useful functions for the analysis of bipartite, weighted, and +longitudinal networks so it is worth investigating (see +?tnet). Unfortunately, it is no longer being actively +maintained.

+
+ +Let's take a look at Newman's method using the `tnet` package. This package expects a simple two-column edge list with only integers for the node identifiers which we can generate using the `igraph::as_edgelist` function and including the `names = FALSE` argument: + + +```r +library(tnet) + +tm_el <- igraph::as_edgelist(cibola_inc, names = FALSE) +head(tm_el) +``` + +``` +## [,1] [,2] +## [1,] 1 32 +## [2,] 1 33 +## [3,] 1 34 +## [4,] 1 35 +## [5,] 1 36 +## [6,] 1 37 +``` + +```r +proj_newman <- as.matrix(projecting_tm(tm_el, method = "Newman")) + +proj_net <- graph_from_edgelist(proj_newman[, 1:2]) +E(proj_net)$weight <- proj_newman[, 3] +V(proj_net)$name <- row.names(cibola_clust) + +set.seed(4643) +ggraph(proj_net) + + geom_edge_link(aes(color = weight), + width = 1, show.legend = FALSE) + + scale_edge_alpha_continuous(range = c(0, 0.5)) + + scale_edge_color_continuous() + + geom_node_point(aes(size = igraph::degree(proj_net)), + show.legend = FALSE) + + geom_node_text(aes(label = name), size = 3, repel = TRUE) + + theme_graph() +``` + + + +As expected, we get another hairball here, but let's take a look at the edge weights of this projection versus the original matrix multiplication projection: + + +```r +cor(E(proj_net)$weight, E(cibola_onemode)$weight)^2 +``` + +``` +## [1] 0.9454047 +``` + +```r +plot(E(proj_net)$weight, E(cibola_onemode)$weight, pch = 16, col = "blue", + xlab = "Newman Weights", ylab = "Matrix Multiplication Weights") +``` + + + +As this shows, edge weights are highly correlated with an $r^2 = 0.95$ but there are also some subtle differences that distinguish these two projection methods. Via experimentation we have found the Newman model to be particularly useful in situations where there are a mix of common and rare artifact (mode 2) categories as this produces networks that account for relative frequency via context-to-context co-associations. + +## Correspondence Analysis{#CorrespondenceAnalysis} + +Another method that has proven useful in exploring affiliation data in archaeological and many other contexts is correspondence analysis. Correspondence analysis is a method of dimension reduction based on the decomposition of the $chi$-square statistic that allows for the projection of the rows and columns of an incidence matrix into the same low dimensional space (see Peeples and Schachner 2012). Correspondence analysis works on either count or presence/absence data. The technical details of this approach are beyond the scope of this document (see Peeples and Schachner 2012), but generally correspondence analysis can be used to plot row and column cases from an incidence matrix in a single bi-plot where the spatial configuration of those points is related to the degree of co-association among them (though not a perfect representation of co-association). Correspondence analysis is frequently used for frequency seriation in archaeology but can also be useful in any analysis focused on co-association including spatial analyses (Alberti 2017). Correspondence analysis has also previously been used by [Giomi and Peeples (2019)](https://www.sciencedirect.com/science/article/abs/pii/S0278416518301132?via%3Dihub) for investigating assemblages of materials recovered in discrete excavation contexts within the Pueblo Bonito Chacoan Great House complex. They explicitly compare correspondence analysis to related co-association methods we will describe further below to construct networks of co-association among artifact categories. + +
+

Conducting correspondence analysis and visualizing it in R is +typically done using the ca package. This package has a +plotting function within it that creates bi-plots where each mode (rows +and columns of the incidence matrix) are displayed with different shapes +and colors. See the ?ca() function help for more +information. Although we do not use it here, the anacor +package provides additional extensions of correspondence analysis +methods (such as alternative axis scaling methods) that may also be +useful.

+
+ +Let's start by applying the `ca` function to our `cibola_clust` incidence matrix data set: + + +```r +library(ca) + +ca_cibola <- ca(cibola_clust) +plot(ca_cibola) +``` + + + +This plot clearly shows associations between certain clusters and sets of sites. If you are familiar with the sites used here you may also notice that there are clear geographic clusters as well. In addition to this, the dimensions contain percentages in the labels. What this indicates is the amount of variation in the underlying incidence matrix that each dimension represents. Correspondence analysis is designed such that the first dimension accounts for the most variation and so on up to 1 dimension less than the number of categories present. + +### Network Visuals Using Correspondence Analysis{#CAViz} + +Correspondence analysis has frequently been used for plotting affiliation data like this in sociology, in particular using layouts generated through CA to plot networks with edges generated using some other mode of projection described above (see Borgatti and Halgin 2014; Faust 2005). In the chunk of code below we plot the reduced Cibola one-mode network edges as well as the one mode projection of the columns using the positions on the correspondence analysis axes as the point locations. + + +```r +# Create network object using crossprod function +col_net <- graph_from_adjacency_matrix(crossprod(as.matrix(cibola_clust)), + weighted = TRUE, + mode = "undirected", + diag = FALSE) + +# Combine both edge lists into a single frame +el_com <- rbind(igraph::as_edgelist(cibola_om_reduced), igraph::as_edgelist(col_net)) + +# Define composite network object and add Edge and Vertex attributes +net2 <- graph_from_edgelist(el_com, directed = FALSE) +E(net2)$weight <- c(E(cibola_om_reduced)$weight, E(col_net)$weight) +E(net2)$mode <- c(rep("blue", ecount(cibola_om_reduced)), + rep("red", ecount(col_net))) +V(net2)$mode2 <- c(rep("blue", vcount(cibola_om_reduced)), + rep("red", vcount(col_net))) +V(net2)$name <- c(row.names(cibola_clust), colnames(cibola_clust)) + +# Create object containing row and column coordinates from correspondence +# analysis results +xy <- rbind(ca_cibola$rowcoord[, 1:2], ca_cibola$colcoord[, 1:2]) + +# Plot the results color coding by mode +set.seed(4643) +ggraph(net2, layout = "manual", + x = xy[, 1], + y = xy[, 2]) + + geom_edge_link0(aes(alpha = weight, color = E(net2)$mode), + show.legend = FALSE) + + scale_edge_color_manual(values = c("blue", "red")) + + geom_node_point(aes(color = mode2, shape = mode2), + size = 3, + show.legend = FALSE) + + scale_color_manual(values = c("blue", "red")) + + scale_shape_manual(values = c(16, 17)) + + geom_node_text(aes(label = name), size = 3, repel = TRUE) + + theme_bw() +``` + + + +As this plot illustrates, there is considerable information here about the co-associations of the two modes in our incidence matrix in visuals like this. We suggest that visualizations like this are a potential avenue worth perusing in future archaeological network investigations of incidence matrix data sets. + +## Measuring Co-association{#MeasuringCoassociation} + +Giomi and Peeples (2019) presented a network analysis focused on intra-site variability in the Chacoan Great House complex of Pueblo Bonito. In this analysis, they used correspondence analysis as shown above in addition a method for assessing co-occurrence in two-way tables first published by Kintigh (2006). This method calculates the expected co-occurrence of every pair of objects in an assemblage based on the total number of contexts considered and the number of contexts that contain each type of object (you can think about types of objects and contexts as two modes in a two-mode network). This method relies on presence/absence only so it can be applied in many contexts even where only sketchy information on archaeological context inventories are available. Giomi and Peeples present this measure of co-association $C_{ab}$ between object types $a$ and $b$ defined as: + +$$C_{ab}=\frac{o_{ab}-Np_{ab}}{\sqrt{Np_{ab}(1-p_{ab})}}$$ + +where + +* $o_{ab}$ = the observed number of co-occurrences between object classes $a$ and $b$. +* $N$ = the total number of assemblages +* $p_{ab}$ = the expected proportion of co-occurrences between object classes $a$ and $b$ defined as the proportion of assemblages where $a$ occurs times the proportion of assemblages where $b$ occurs. + +This measure provides an index of the number of co-occurrences observed in relation to expected given the overall frequency of each object class in Z-standardized units. That means that a value of 3 means that two object classes co-occur approximately 3 standard deviations more than we would expect given the frequency of occurrence for those object classes and the number of contexts for which we have data. Similarly a value of -2 means that two object classes are 2 standard deviations less associated than would be expected given their frequency of occurrence. + +Here is the function used by Giomi and Peeples (2019) and also available [on GitHub here](https://github.com/mpeeples2008/Giomi-and-Peeples-2019). + + +```r +## Co-occurrence assessment script +## This function expects a binary data frame object that contains only 1s and 0s with the contexts under consideration as rows and the +## categories as columns and each cell represents the presence or absence of a particular category in a particular context + +cooccur <- function(x) { + + # calculate the proportional occurrence of each artifact class + nm.p <- colSums(x)/nrow(x) + + # calculated observed co-occurrences through matrix multiplication + obs <- t(as.matrix(x)) %*% (as.matrix(x)) + diag(obs) <- 0 + + # create matrix of expected values based on proportional occurrence + expect <- matrix(0,nrow(obs),ncol(obs)) + for (i in 1:nrow(obs)) { + for (j in 1:ncol(obs)) { + expect[i,j] <- (nm.p[i]*nm.p[j])*nrow(x)}} + + # convert expected count to expected proportion + p <- expect/nrow(x) + + # calculate final matrix of scores and output + out <- (obs-expect)/(sqrt(expect*(1-p))) + diag(out) <- 0 + return(out)} +``` + +To test this approach we're going to use a partial inventory of artifacts from a site in west-central New Mexico called Techado Springs (Smith et al. 2009). This was an extensively excavated settlement with over 500 rooms and we have artifact inventory data for 198 of those rooms. For the example here, we're using 9 categories of features/objects encountered in those rooms, all of which are related to the ceramic production process. You can [download the artifact data here](data/Techado_artifacts.csv) to follow along. + + +```r +tec <- read.csv("data/Techado_artifacts.csv", header = TRUE, row.names = 1) +tec <- tec[which(rowSums(tec) > 0), ] + +out <- cooccur(tec) + +# see first few columns +out[,1:4] +``` + +``` +## CeramicDryFeature CeramicVessel Puki PaintCup +## CeramicDryFeature 0.0000000 3.9785881 2.25632854 1.5877018 +## CeramicVessel 3.9785881 0.0000000 -1.38858632 -0.2800886 +## Puki 2.2563285 -1.3885863 0.00000000 -0.9061016 +## PaintCup 1.5877018 -0.2800886 -0.90610162 0.0000000 +## PolishingStone -1.0575847 NaN -5.15060380 1.3732138 +## RawClay -0.8651613 -1.5384481 -1.43135460 -1.3739322 +## Scoop 0.7167460 -1.4775196 -1.59036373 0.6854396 +## CeramicScrapper 0.2165159 0.2621044 -0.01569124 2.1188426 +## UnfiredVessel 3.6918908 8.3227556 1.04664169 1.9422423 +``` + +Looking at the first few columns of the output of our `cooccur` function we can see a few particularly high and low values. For example, polishing stones and pukis (tools used for supporting vessels while forming them) are much less associated than would be expected by chance (-5.15). On the other side, ceramic drying features and un-fired vessels are much more associated than we would expect by chance (3.69). There are also several `NaN` or `NA` indicating that those two categories never co-occur in this data set. + +Next, let's plot all of the returned values as a histogram: + + +```r +hist(out, breaks = 10, xlab = "C_p") +``` + + +If we examine this histogram of all values returned from the analysis we see a distribution that is peaked near 0 (or the degree of co-association we would expect by chance) but there are extreme values in both directions suggesting much less and greater co-association than would be expected by chance for certain features/object classes. + +### Alternative Methods for Visualizing Co-associations{#COViz} + +Another way we can visualize these data is using a network graph as described by Giomi and Peeples (2019). First, we dichotomize the output created above. We are going to use an absolute threshold here and define a edge (connection) between any pair of object classes that are associated 2 standard deviations or greater more than expected by chance. There is nothing special about this 2 SD threshold and in practice it is a good idea to try a range of values and compare your results. + + +```r +net_dat <- out +net_dat <- ifelse(net_dat < 2, 0, net_dat) +net_dat[is.na(net_dat)] <- 0 + +net_c <- graph_from_adjacency_matrix(net_dat, + mode = "undirected", + weighted = TRUE) +V(net_c)$name <- colnames(tec) + +set.seed(4643) +ggraph(net_c) + + geom_edge_link(aes(width = weight)) + + scale_edge_width_continuous(range = c(0.5, 1.5)) + + geom_node_point(size = 3, color = "red") + + geom_node_text(aes(label = name), size = 3, repel = TRUE) + + theme_graph() +``` + + + +This plot shows the strongest co-associations among categories and provides a nice distillation of important relationships in one mode of these data. It is also possible to do the reverse and explore the connections among rooms by virtue of the co-associations in their assemblages. Let's give this a try using the `t()` transpose function to reverse the matrix for our analysis. In the plot below we have further defined clusters among nodes using the Louvain clustering method and color coded them to evaluate the potential for community structure in our results. + + +```r +net_dat <- cooccur(t(tec)) +net_dat <- ifelse(net_dat > 2, 1, 0) +net_dat[is.na(net_dat)] <- 0 + +net_r <- graph_from_adjacency_matrix(net_dat, mode = "undirected") +V(net_r)$name <- row.names(tec) + +group <- cluster_louvain(net_r)$memberships[1, ] + +set.seed(4532) +ggraph(net_r, layout = "fr") + + geom_edge_link(alpha = 0.5) + + geom_node_point(aes(color = as.factor(group)), + size = 3, + show.legend = FALSE) + + scale_color_discrete("Set2") + + geom_node_text(aes(label = name), size = 3, repel = TRUE) + + theme_graph() +``` + + + +Hmm... There appear to be some clusters... interesting. There are a lot of things we would likely want to do here including going back to the original data and looking for other commonalities among rooms that group together in terms of strong similarities in ceramic production tools and features. For example, Giomi and Peeples (2019) explore the relationships between cluster/clique membership and room inventories and make behavioral interpretations of common assemblages drawing on ethnographic examples. + +As the brief example above illustrates, co-association methods like this are a natural fit for network methods and we argue that archaeologists should explore these and similar approaches more in the future. + diff --git a/10-diffusion.Rmd b/10-diffusion.Rmd index a64dcf0..af29e67 100644 --- a/10-diffusion.Rmd +++ b/10-diffusion.Rmd @@ -171,7 +171,7 @@ xy <- as.data.frame(coord1) colnames(xy) <- c("x", "y") # Extract edge list from network object for road_net -edgelist1 <- get.edgelist(road_net3) +edgelist1 <- igraph::as_edgelist(road_net3) # Create data frame of beginning and ending points of edges edges1 <- as.data.frame(matrix(NA, nrow(edgelist1), 4)) diff --git a/10-diffusion.md b/10-diffusion.md new file mode 100644 index 0000000..aacf033 --- /dev/null +++ b/10-diffusion.md @@ -0,0 +1,717 @@ +# Network Diffusion{#NetworkDiffusion} + +![](images/image_break.png){width=100%} + +In the social and behavioral sciences, network models and empirical networks have frequently been used to investigate diffusion processes. Diffusion in this contexts refers to the spread of social or biological contagions (diseases, technological innovations, memes, rumors, etc.) among individuals or larger groups in a given a social context. Work in this realm has shown that social networks with different topological properties can lead to very different kinds of diffusion trajectories in terms of the speed and completeness with which such contagions spread. In this section, we introduce a few simple network diffusion models and demonstrate how they can be used with common forms of archaeological network data or to address general archaeological questions. + +## Diffusion Processes{#DiffusionProcesses} + +Network diffusion processes are frequently investigated using simulation methods. Specifically, a researcher starts with a network either generated based on empirical data or modeled on some generative process (like a random or small world network) and then introduces a social contagion of some sort into one or more nodes in the network. This network is then walked through a series of time steps (which could represent hours, days, years or whatever length of time is appropriate for the given question) and the contagion spreads from node to node with some probability based on the configuration and strength of connections among the nodes and potentially some other features such as the susceptibility of a given node based on non-network attributes. Such simulations can be repeated many times with a given network configuration and then the nature of the diffusion process can be examined in the resulting data which might include estimates of the rate of infection/adoption, the proportion of nodes that take on the contagion across each time step, or the order in which nodes are infected/adopt among many other possibilities. Typically, researchers are interested in identifying specific features of the infection/adoption curve, specific directions of spread, or other aggregate features of the diffusion process to compare to other empirical information or theoretical expectations. For example, empirical research on the diffusion of technological innovations has shown that adoption rates are often characterized by an S-shaped curve where adoption rates are initially slow until an innovation reaches some threshold, which is followed by rapid adoption and then eventually a leveling off as the adoption rate reaches saturation within a given population. Using network simulation as described here, it is possible to compare how different network configurations relate to such expectations. + +![Examples of different S-shaped curves of adoption of innovations (adapted from Rogers 2003: Figure 1.2)](images/rogers.jpg){width=80%} + +## Simulating Network Diffusion in R{#SimNetworkR} + +In this section we introduce basic methods for simulating network diffusion process on empirical and model based network configurations in R. + +
+

For the analyses here, we largely rely on a package called +netdiffuseR which includes built-in functions for +simulating many common topological forms of networks such as random or +small-world networks and also allows us to estimate diffusion rates and +directions across nodes across time steps. Importantly, this package +allows for consideration of both empirical and simulated networks as the +starting point.

+
+ +Let's get started by initializing our library and exploring the primary function within this package called `rdiffnet`. This function can take a number of arguments to specify the nature of the network to be created and the diffusion process to be simulated on that network. These arguments include: + +* **n** - The number of nodes to include in the network. If you supply a `seed.graph` this argument is not needed. +* **t** - the number of time steps to consider. +* **seed.graph** - An optional argument that lets you supply an empirical network in the form of an adjacency matrix to serve as the initial network configuration. +* **seed.nodes** - This argument can be set to either `marginal`, `central`, or `random` and this refers to the positions of the initial nodes to be "infected" or "adopters" in the network model. These options will select nodes with either the lowest degree, highest degree, or randomly respectively. Alternatively, you can supply a vector of node numbers representing the nodes which should be adopters in time step 1. +* **seed.p.adopt** - This is the proportion of nodes that will be initial adopters/infected. +* **rgraph.args** - This argument includes arguments that are further passed to the `rgraph` function to define the parameters of the random graph to be created (if this is relevant). +* **rewire** - This logical argument expects a `TRUE` or `FALSE`. If `TRUE` at each time step a number of edges will be reassigned at random based on additional options passed to the `rewire.args` argument. Note that this argument is `TRUE` by default. +* **rewire.args** - This argument is used to send options to the `rewire_graph` function which rewires a certain number of edges at each step. In general the most relevant option is `p` which is the proportion of edges that should be rewired. +* **threshold.dist** - This argument expects either a function or a vector of length `n` that defines the adoption threshold (susceptibility) of each node. +* **exposure.args** - This argument contains options passed to the `exposure` function which defines adoption rates for various kinds of network edge weighting schema. + +As we will see below, we do not need to use all of these arguments in every network simulation. Reading the documentation of the `rdiffnet` package provides additional details on options described briefly here. + +One important concept that needs to be formally defined before we move on is the network threshold (defined in relationship to $\tau$ or `threshold.dist`). This can be formally written as: + + $$ + a_i = \left\{\begin{array}{ll} + 1 &\mbox{if } \tau_i\leq E_i \\ + 0 & \mbox{Otherwise} + \end{array}\right. \qquad + E_i \equiv \frac{\sum_{j\neq i}\mathbf{X}_{ij}a_j}{\sum_{j\neq i}\mathbf{X}_{ij}} + $$ + +where + +* $\tau$ is the proportion of neighbors who need to be adopters for the target to adopt. +* $E_i$ is exposure where $\mathbf{X}$ is the adjacency matrix of the network. + +In other words, node $i$ will adopt at a given time step if exposure is greater than or equal to $\tau$. + +### Simulated Networks{#DiffuseSimulatedNetworks} + +We start with a simple random small-world network simulation to show how this function works. Let's run the code and then we'll explain what is happening: + + +```r +library(netdiffuseR) + +set.seed(4436) +net_test1 <- rdiffnet( + n = 1000, + t = 20, + seed.nodes = "random", + seed.p.adopt = 0.001, + seed.graph = "small-world", + rgraph.args = list(p = 0.1), + threshold.dist = function (x) runif(1, 0.1, 0.5) +) + +summary(net_test1) +``` + +``` +## Diffusion network summary statistics +## Name : A diffusion network +## Behavior : Random contagion +## ----------------------------------------------------------------------------- +## Period Adopters Cum Adopt. (%) Hazard Rate Density Moran's I (sd) +## -------- ---------- ---------------- ------------- --------- ---------------- +## 1 1 1 (0.00) - 0.00 -0.00 (0.00) +## 2 4 5 (0.00) 0.00 0.00 0.10 (0.01) *** +## 3 6 11 (0.01) 0.01 0.00 0.16 (0.01) *** +## 4 9 20 (0.02) 0.01 0.00 0.18 (0.01) *** +## 5 13 33 (0.03) 0.01 0.00 0.18 (0.01) *** +## 6 25 58 (0.06) 0.03 0.00 0.20 (0.01) *** +## 7 39 97 (0.10) 0.04 0.00 0.24 (0.01) *** +## 8 71 168 (0.17) 0.08 0.00 0.19 (0.01) *** +## 9 124 292 (0.29) 0.15 0.00 0.20 (0.01) *** +## 10 167 459 (0.46) 0.24 0.00 0.21 (0.01) *** +## 11 197 656 (0.66) 0.36 0.00 0.18 (0.01) *** +## 12 186 842 (0.84) 0.54 0.00 0.16 (0.01) *** +## 13 111 953 (0.95) 0.70 0.00 0.10 (0.01) *** +## 14 41 994 (0.99) 0.87 0.00 0.03 (0.01) *** +## 15 6 1000 (1.00) 1.00 0.00 - +## 16 0 1000 (1.00) 0.00 0.00 - +## 17 0 1000 (1.00) 0.00 0.00 - +## 18 0 1000 (1.00) 0.00 0.00 - +## 19 0 1000 (1.00) 0.00 0.00 - +## 20 0 1000 (1.00) 0.00 0.00 - +## ----------------------------------------------------------------------------- +## Left censoring : 0.00 (1) +## Right centoring : 0.00 (0) +## # of nodes : 1000 +## +## Moran's I was computed on contemporaneous autocorrelation using 1/geodesic +## values. Significane levels *** <= .01, ** <= .05, * <= .1. +``` + +In this example, we have created a random network with 1000 nodes and [small world](https://en.wikipedia.org/wiki/Small-world_network#:~:text=A%20small%2Dworld%20network%20is,number%20of%20hops%20or%20steps.) structure. We examine the network across 20 time steps. We send a value of `0.1` to the `rgraph.args` argument meaning that proportion of ties will be rewired in the random graph to generate "small-world" structure (see `rgaph_ws` for more info) in the initial network configuration. We set the initial adopters in the network to `0.001` or a single node in this 1000 node network. Finally, we set the `threshold.dist` to be a random uniform number (using the `runif` function) between `0.1` and `0.5` meaning that a node will adopt the contagion at a given time step if between 10% and 50% of it's neighbors have adopted. Note that we have not set a value for `rewire` so by default this is `TRUE` and a small proportion of edges will be rewired at each time step. + +The summary output provides information on the number of adopters and the cumulative adoption percent at each time step. We also have information on the hazard rate, which is the probability that a given node will be infected/adopt at each step. The Moran's I is a measure of autocorrelation which here is sued to indicate whether infected nodes/adopters are concentrated among neighbors in the network (nodes that share an edge). Not surprisingly, we see they are across all but the first time step. + +The `netdiffuseR` package also has built in functions for plotting. First, let's plot our simulated network at a few different time steps to see the distributions of adopters and non-adopters. Here we plot the 1st, 10th, and 15th time steps: + + +```r +plot_diffnet(net_test1, slices = c(1, 10, 15)) +``` + + + +We can also plot the adoption curve across all time steps using the `plot_adopters` function: + + +```r +plot_adopters(net_test1) +``` + + + +These results show the classic S-shaped curve for cumulative adoption with the parameters we've provided where adoption is at first slow, followed by a period of rapid adoption, and then a gradual slowdown as adoptions reaches saturation. + +We can also plot a network that shows the time step at which each node adopted the contagion: + + +```r +plot_diffnet2(net_test1) +``` + + + +Using the `rdiffnet` function and altering the arguments, we can experiment with how different configurations of parameters change the rate or completeness of adoption. For example, in the chunk of code below we replicate the model above exactly except that we allow for some nodes to have a higher threshold required for adoption (70% of nodes as the max instead of 50%). Let's see how that changes our results: + + +```r +set.seed(4436) +net_test2 <- rdiffnet( + n = 1000, + t = 20, + seed.nodes = "random", + seed.p.adopt = 0.001, + seed.graph = "small-world", + rgraph.args = list(p = 0.1), + threshold.dist = function (x) runif(1, 0.1, 0.7) +) + +summary(net_test2) +``` + +``` +## Diffusion network summary statistics +## Name : A diffusion network +## Behavior : Random contagion +## ----------------------------------------------------------------------------- +## Period Adopters Cum Adopt. (%) Hazard Rate Density Moran's I (sd) +## -------- ---------- ---------------- ------------- --------- ---------------- +## 1 1 1 (0.00) - 0.00 -0.00 (0.00) +## 2 4 5 (0.00) 0.00 0.00 0.10 (0.01) *** +## 3 3 8 (0.01) 0.00 0.00 0.14 (0.01) *** +## 4 5 13 (0.01) 0.01 0.00 0.17 (0.01) *** +## 5 2 15 (0.01) 0.00 0.00 0.12 (0.01) *** +## 6 2 17 (0.02) 0.00 0.00 0.15 (0.01) *** +## 7 8 25 (0.02) 0.01 0.00 0.15 (0.01) *** +## 8 11 36 (0.04) 0.01 0.00 0.18 (0.01) *** +## 9 23 59 (0.06) 0.02 0.00 0.15 (0.01) *** +## 10 32 91 (0.09) 0.03 0.00 0.19 (0.01) *** +## 11 44 135 (0.14) 0.05 0.00 0.18 (0.01) *** +## 12 56 191 (0.19) 0.06 0.00 0.15 (0.01) *** +## 13 74 265 (0.26) 0.09 0.00 0.15 (0.01) *** +## 14 95 360 (0.36) 0.13 0.00 0.15 (0.01) *** +## 15 101 461 (0.46) 0.16 0.00 0.15 (0.01) *** +## 16 82 543 (0.54) 0.15 0.00 0.13 (0.01) *** +## 17 92 635 (0.64) 0.20 0.00 0.12 (0.01) *** +## 18 86 721 (0.72) 0.24 0.00 0.13 (0.01) *** +## 19 69 790 (0.79) 0.25 0.00 0.14 (0.01) *** +## 20 45 835 (0.83) 0.21 0.00 0.14 (0.01) *** +## ----------------------------------------------------------------------------- +## Left censoring : 0.00 (1) +## Right centoring : 0.16 (165) +## # of nodes : 1000 +## +## Moran's I was computed on contemporaneous autocorrelation using 1/geodesic +## values. Significane levels *** <= .01, ** <= .05, * <= .1. +``` + +```r +plot_adopters(net_test2) +``` + + + +```r +plot_diffnet2(net_test2) +``` + + + +As this shows, by changing that simple parameter to allow for a higher adoption threshold for some nodes, we no longer get saturation within the same 20 time steps and we see a generally slower rate of adoption. + +We could also explore alternate graph generation models using this approach. In the example below, we generate a [scale-free](https://en.wikipedia.org/wiki/Scale-free_network) network using the `rgraph_ba` function (ba for Barabasi and Albert who defined this model). We set the parameter `m = 4` which means that 4 edges will be created for each node in the initial network. We leave all other parameters as they were in our initial example. + + +```r +set.seed(4436) +net_test2 <- rdiffnet( + n = 1000, + t = 20, + seed.nodes = "random", + seed.p.adopt = 0.001, + seed.graph = "scale-free", + rgraph.args = list(m = 4), + threshold.dist = function (x) runif(1, 0.1, 0.5) +) + +summary(net_test2) +``` + +``` +## Diffusion network summary statistics +## Name : A diffusion network +## Behavior : Random contagion +## ----------------------------------------------------------------------------- +## Period Adopters Cum Adopt. (%) Hazard Rate Density Moran's I (sd) +## -------- ---------- ---------------- ------------- --------- ---------------- +## 1 1 1 (0.00) - 0.00 -0.00 (0.00) +## 2 1 2 (0.00) 0.00 0.00 0.00 (0.00) *** +## 3 2 4 (0.00) 0.00 0.00 0.01 (0.00) *** +## 4 3 7 (0.01) 0.00 0.00 0.02 (0.00) *** +## 5 3 10 (0.01) 0.00 0.00 0.01 (0.00) *** +## 6 5 15 (0.01) 0.01 0.00 0.02 (0.00) *** +## 7 5 20 (0.02) 0.01 0.00 0.01 (0.00) *** +## 8 9 29 (0.03) 0.01 0.00 0.01 (0.00) *** +## 9 13 42 (0.04) 0.01 0.00 0.02 (0.00) *** +## 10 17 59 (0.06) 0.02 0.00 0.01 (0.00) *** +## 11 71 130 (0.13) 0.08 0.00 0.01 (0.00) *** +## 12 189 319 (0.32) 0.22 0.00 0.01 (0.00) *** +## 13 385 704 (0.70) 0.57 0.00 0.01 (0.00) *** +## 14 273 977 (0.98) 0.92 0.00 0.00 (0.00) +## 15 23 1000 (1.00) 1.00 0.00 - +## 16 0 1000 (1.00) 0.00 0.00 - +## 17 0 1000 (1.00) 0.00 0.00 - +## 18 0 1000 (1.00) 0.00 0.00 - +## 19 0 1000 (1.00) 0.00 0.00 - +## 20 0 1000 (1.00) 0.00 0.00 - +## ----------------------------------------------------------------------------- +## Left censoring : 0.00 (1) +## Right centoring : 0.00 (0) +## # of nodes : 1000 +## +## Moran's I was computed on contemporaneous autocorrelation using 1/geodesic +## values. Significane levels *** <= .01, ** <= .05, * <= .1. +``` + +```r +plot_adopters(net_test2) +``` + + + +```r +plot_diffnet2(net_test2) +``` + + + +As the figures above show, the scale-free network model generates an adoption curve that is slow to grow and then shows a rapid cascade across the network and quick saturation in just a few time steps. As this suggests, these different forms of network topology likely lead to different kinds of adoption/infection processes. We could potentially use these curves and assessments of rates of uptakes from other empirical analyses to determine which sorts of network generative process are more or less plausible given our data. + +### Empirical Networks{#DiffuseEmpiricalNetworks} + +The `rdiffnet` function described above can also be applied to empirical networks. By way of example, let's take a look at the Iberian [Roman Roads](#RomanRoads) data set we've used in several places in this document. Here we define sites as nodes and connect them with edges when there is a documented road between them. We draw additional edges to nearest neighbors for all remaining unconnected nodes to create a single fully connected network. [Download the network data here to follow along](data/road_networks.Rdata) and [download the basemap here]("data/road_base.Rdata"). + +First, let's map the network labeling the nodes by number: + + + +```r +library(igraph) +library(ggmap) +library(sf) +library(dplyr) +library(ggrepel) + +# Read in required data +load("data/road_networks.RData") +load("data/road_base.Rdata") + +nodes <- nodes[match(V(road_net3)$name, nodes$Id), ] + +# Convert name, lat, and long data into sf coordinates +locations_sf <- + st_as_sf(nodes, coords = c("long", "lat"), crs = 4326) +coord1 <- do.call(rbind, st_geometry(locations_sf)) %>% + tibble::as_tibble() %>% + setNames(c("long", "lat")) + +# Create data.frame of long and lat as xy coordinates +xy <- as.data.frame(coord1) +colnames(xy) <- c("x", "y") + +# Extract edge list from network object for road_net +edgelist1 <- igraph::as_edgelist(road_net3) + +# Create data frame of beginning and ending points of edges +edges1 <- as.data.frame(matrix(NA, nrow(edgelist1), 4)) +colnames(edges1) <- c("X1", "Y1", "X2", "Y2") +for (i in seq_len(nrow(edgelist1))) { + edges1[i,] <- c(nodes[which(nodes$Id == edgelist1[i, 1]), 3], + nodes[which(nodes$Id == edgelist1[i, 1]), 2], + nodes[which(nodes$Id == edgelist1[i, 2]), 3], + nodes[which(nodes$Id == edgelist1[i, 2]), 2]) +} +# Plot ggmap object with network on top +ggmap(my_map) + + geom_segment(data = edges1, + aes( + x = X1, + y = Y1, + xend = X2, + yend = Y2 + ), + size = 1) + + geom_point( + data = xy, + aes(x, y), + alpha = 0.8, + col = "black", + fill = "white", + shape = 21, + size = 3, + ) + + geom_text_repel(aes(x = x, y = y, label = row.names(xy)), + data = xy, + size = 3) + + theme_void() +``` + + + +In order to use this network to model diffusion process, we simply have to supply it as the `seed.graph` within the `rdiffnet` function. In the following chunk of code, we supply the network shown above as the seed and model 20 time steps with the initial "adopters" in this case defined as nodes 72 and 77 in the far eastern portion of the study area. We define the `threshold.dist` as a vector where all values are `0.25` indicating that a node will adopt if a quarter of its connections have already adopted. Further, we set `rewire = FALSE` so that our edges will remain the same across all time steps. Let's take a look at the network and the adopter plot: + + +```r +set.seed(4435436) +diffnet_road <- rdiffnet( + seed.graph = as.matrix(road_net3), + t = 20, + seed.nodes = c(72, 77), + threshold.dist = rep(0.25, 122), + rewire = FALSE +) + +summary(diffnet_road) +``` + +``` +## Diffusion network summary statistics +## Name : A diffusion network +## Behavior : Random contagion +## ----------------------------------------------------------------------------- +## Period Adopters Cum Adopt. (%) Hazard Rate Density Moran's I (sd) +## -------- ---------- ---------------- ------------- --------- ---------------- +## 1 2 2 (0.02) - 0.02 0.04 (0.01) *** +## 2 6 8 (0.07) 0.05 0.02 0.49 (0.02) *** +## 3 1 9 (0.07) 0.01 0.02 0.49 (0.02) *** +## 4 3 12 (0.10) 0.03 0.02 0.51 (0.02) *** +## 5 3 15 (0.12) 0.03 0.02 0.52 (0.02) *** +## 6 3 18 (0.15) 0.03 0.02 0.53 (0.02) *** +## 7 3 21 (0.17) 0.03 0.02 0.52 (0.02) *** +## 8 7 28 (0.23) 0.07 0.02 0.53 (0.02) *** +## 9 8 36 (0.30) 0.09 0.02 0.56 (0.02) *** +## 10 8 44 (0.36) 0.09 0.02 0.60 (0.02) *** +## 11 7 51 (0.42) 0.09 0.02 0.58 (0.02) *** +## 12 9 60 (0.49) 0.13 0.02 0.59 (0.02) *** +## 13 9 69 (0.57) 0.15 0.02 0.63 (0.02) *** +## 14 7 76 (0.62) 0.13 0.02 0.68 (0.02) *** +## 15 6 82 (0.67) 0.13 0.02 0.69 (0.02) *** +## 16 4 86 (0.70) 0.10 0.02 0.66 (0.02) *** +## 17 1 87 (0.71) 0.03 0.02 0.66 (0.02) *** +## 18 2 89 (0.73) 0.06 0.02 0.63 (0.02) *** +## 19 3 92 (0.75) 0.09 0.02 0.60 (0.02) *** +## 20 5 97 (0.80) 0.17 0.02 0.54 (0.02) *** +## ----------------------------------------------------------------------------- +## Left censoring : 0.02 (2) +## Right centoring : 0.20 (25) +## # of nodes : 122 +## +## Moran's I was computed on contemporaneous autocorrelation using 1/geodesic +## values. Significane levels *** <= .01, ** <= .05, * <= .1. +``` + +```r +plot_adopters(diffnet_road) +``` + + + +As the plot above shows, this network doesn't reach saturation of adoption in the 20 time steps we give it but it is on an upward trajectory that is about the same slope from beginning to end. Let's now consider the time of adoption for individual nodes. To do this, we can look at an object appended to the output of the `rdiffnet` function called `toa` or "time of adoption" which indicates which time step the node adopted. + + +```r +diffnet_road$toa +``` + +``` +## n0 n73 n27 n77 n59 n1 n20 n70 n2 n64 n57 n46 n88 n3 n28 n4 +## 14 15 13 14 15 15 15 14 8 8 13 7 7 8 9 18 +## n16 n33 n52 n29 n5 n85 n15 n6 n86 n7 n83 n30 n8 n81 n60 n9 +## 19 18 17 19 NA NA NA 6 5 NA NA NA 12 11 13 11 +## n74 n75 n76 n78 n10 n42 n35 n38 n31 n11 n24 n12 n79 n13 n48 n23 +## 11 12 12 10 11 12 12 10 12 6 5 4 3 NA 20 15 +## n36 n47 n14 n17 n55 n69 n39 n19 n53 n54 n18 n72 n21 n40 n67 n22 +## 20 NA 9 10 NA 20 19 20 15 13 NA NA NA 20 NA 13 +## n84 n25 n49 n26 n80 n66 n32 n34 n44 n71 n37 n56 n41 n50 n43 n45 +## 14 13 14 NA 12 NA NA 1 2 2 16 9 1 2 4 9 +## n82 n61 n87 n51 n62 n63 n58 n65 n68 n89 n90 n91 n92 n93 n94 n95 +## 8 10 10 8 9 NA 13 11 9 11 NA NA 2 4 10 9 +## n96 n97 n98 n99 n100 n101 n102 n103 n104 n105 n106 n107 n108 n109 n110 n111 +## 14 NA NA 11 NA 6 NA NA 8 16 NA 16 5 9 2 7 +## n112 n113 n114 n115 n116 n117 n118 n119 n120 n121 +## 10 10 13 12 12 16 13 8 2 14 +``` + +Let's now plot a map of the network color coding nodes by this variable: + + +```r +library(ggmap) +library(ggrepel) + +ggmap(my_map) + + # geom_segment plots lines by the beginning and ending + # coordinates like the edges object we created above + geom_segment( + data = edges1, + aes( + x = X1, + y = Y1, + xend = X2, + yend = Y2 + ), + col = "black", + size = 1 + ) + + # plot site node locations + geom_point( + data = xy, + aes(x, y, color = diffnet_road$toa), + alpha = 0.8, + shape = 16, + size = 3, + ) + + scale_color_viridis_c(option = "plasma") + + geom_text_repel(aes(x = x, y = y, label = row.names(xy)), + data = xy, + size = 3) + + theme_void() +``` + + + +As this map illustrates, nodes closest to the initial adopters are the earliest adopters. Further the area in the southern portion of the study area shows a dense collection of nodes colored gray indicating they did not adopt in the 20 time steps we assessed. + +Let's now create another adopter plot and map color coded by time of adoption. In the next example, we leave everything alone but this time set the initial adopters as nodes 6 and 7 in the northwestern portion of the study area: + + + +```r +set.seed(44336) +diffnet_road <- rdiffnet( + seed.graph = as.matrix(road_net3), + t = 20, + seed.nodes = c(6, 7), + threshold.dist = rep(0.25, 122), + rewire = FALSE +) + +plot_adopters(diffnet_road) +``` + + + +```r +ggmap(my_map) + + # geom_segment plots lines by the beginning and ending + # coordinates like the edges object we created above + geom_segment( + data = edges1, + aes( + x = X1, + y = Y1, + xend = X2, + yend = Y2 + ), + col = "black", + size = 1 + ) + + # plot site node locations + geom_point( + data = xy, + aes(x, y, color = diffnet_road$toa), + alpha = 0.8, + shape = 16, + size = 3, + ) + + scale_color_viridis_c(option = "plasma") + + geom_text_repel(aes(x = x, y = y, label = row.names(xy)), + data = xy, + size = 3) + + theme_void() +``` + + + +Using this starting point, we get a fairly typical S-shaped curve and full saturation of adoption within 20 time steps. As this shows, the specific location within a network where the innovation/meme/disease/contagion originates can have a big impact on the rate and completeness of spread, even when considering the same network. + +## Evaluating Diffusion Models{#EvaluatingDiffusion} + +Frequently, when evaluating diffusion processes in empirical networks, the goal is to compare a formal model or simulation of a diffusion process to other empirical information we have regarding nodes, edges, or network level metrics. To provide an example of what this can look like, we use the [Chaco World](#ChacoWorld) data and specifically the minimum distance network created in the [Spatial Networks section](#SpaceSW) which defined edges among Chacoan architectural complexes (ca. A.D. 1050-1150) within 36 kilometers of each other (which represents a one day walk on foot). The period in question is the peak distribution of Chacoan complexes across the region. We have added one additional attribute to this data set which is the beginning and ending date of each Chacoan complex. We will use this information to evaluate our diffusion models below. [Download the data here to follow along](data/Chaco_net.Rdata). + +Let's start by loading in the data and mapping it: + + +```r +library(igraph) +library(ggmap) +library(sf) +library(dplyr) + +load(file = "data/Chaco_net.Rdata") + +chaco_map <- ggmap(base, darken = 0.15) + + geom_segment( + data = edges, + aes( + x = X1, + y = Y1, + xend = X2, + yend = Y2 + ), + col = "white", + size = 0.10, + show.legend = FALSE + ) + + geom_point( + data = xy, + aes(x, y), + alpha = 0.65, + size = 1, + col = "red", + show.legend = FALSE + ) + + theme_void() +chaco_map +``` + + + +Next, we will create a diffusion network object using the `rdiffnet` function. In this plot we use the maximum distance network matrix (36 kilometers) called `d36` as our `seed.graph` and we set our initial nodes to include all of the architectural complexes within Chaco Canyon, which is the core of the Chaco World and the location of the earliest formal Great Houses. We set the threshold distance such that nodes will adopt when they have at least one neighbor that has adopted and set `rewire = FALSE`. We run this model for 20 time steps. + +Let's run this function and take a look at the adopter plot: + + + +```r +chaco <- which(attr$CSN_macro_group == "Chaco Canyon") + +set.seed(443) +diffnet_chaco <- rdiffnet( + seed.graph = as.matrix(d36), + t = 20, + seed.nodes = chaco, + threshold.dist = function(i) 1L, + rewire = FALSE, + exposure.args = list(normalized = FALSE) +) + +summary(diffnet_chaco) +``` + +``` +## Diffusion network summary statistics +## Name : A diffusion network +## Behavior : Random contagion +## ----------------------------------------------------------------------------- +## Period Adopters Cum Adopt. (%) Hazard Rate Density Moran's I (sd) +## -------- ---------- ---------------- ------------- --------- ---------------- +## 1 10 10 (0.04) - 0.08 0.08 (0.01) *** +## 2 40 50 (0.22) 0.19 0.08 0.42 (0.01) *** +## 3 23 73 (0.33) 0.13 0.08 0.46 (0.01) *** +## 4 29 102 (0.46) 0.19 0.08 0.56 (0.01) *** +## 5 13 115 (0.52) 0.11 0.08 0.59 (0.01) *** +## 6 13 128 (0.57) 0.12 0.08 0.62 (0.01) *** +## 7 20 148 (0.66) 0.21 0.08 0.62 (0.01) *** +## 8 25 173 (0.78) 0.33 0.08 0.64 (0.01) *** +## 9 12 185 (0.83) 0.24 0.08 0.64 (0.01) *** +## 10 5 190 (0.85) 0.13 0.08 0.64 (0.01) *** +## 11 6 196 (0.88) 0.18 0.08 0.66 (0.01) *** +## 12 3 199 (0.89) 0.11 0.08 0.73 (0.01) *** +## 13 0 199 (0.89) 0.00 0.08 0.73 (0.01) *** +## 14 0 199 (0.89) 0.00 0.08 0.73 (0.01) *** +## 15 0 199 (0.89) 0.00 0.08 0.73 (0.01) *** +## 16 0 199 (0.89) 0.00 0.08 0.73 (0.01) *** +## 17 0 199 (0.89) 0.00 0.08 0.73 (0.01) *** +## 18 0 199 (0.89) 0.00 0.08 0.73 (0.01) *** +## 19 0 199 (0.89) 0.00 0.08 0.73 (0.01) *** +## 20 0 199 (0.89) 0.00 0.08 0.73 (0.01) *** +## ----------------------------------------------------------------------------- +## Left censoring : 0.04 (10) +## Right centoring : 0.11 (24) +## # of nodes : 223 +## +## Moran's I was computed on contemporaneous autocorrelation using 1/geodesic +## values. Significane levels *** <= .01, ** <= .05, * <= .1. +``` + +```r +plot_adopters(diffnet_chaco) +``` + + + +As this shows, the parameters provided lead to relatively quick adoptions followed by a leveling off. Notably, not all nodes adopt as there are some disconnected components within this network. + +Now let's take a look at a map color coded by time of adoption: + + +```r +chaco_map2 <- ggmap(base, darken = 0.15) + + geom_segment( + data = edges, + aes( + x = X1, + y = Y1, + xend = X2, + yend = Y2 + ), + col = "white", + size = 0.10, + show.legend = FALSE + ) + + geom_point( + data = xy, + aes(x, y, color = diffnet_chaco$toa), + alpha = 0.65, + size = 2, + ) + + scale_color_viridis_c(option = "plasma") + + theme_void() + +chaco_map2 +``` + + + +This map clearly shows a spatial pattern where sites to the south of Chaco Canyon are early adopters and then sites further to the north are relatively late adopters. + +In order to evaluate this further we next want to assess the time of adoption for nodes in relation to other attribute data. one approach we can take is to use the `classify` function built into `netdiffuseR` to place nodes into a set of categories based on their time of adoption. These categories are: `Early Adopters`, `Early Majority`, `Late Majority`, `Laggards` and `Non-Adopters`. + + +```r +toa_class <- + factor( + classify(diffnet_chaco)$toa, + levels = c( + "Early Adopters", + "Early Majority", + "Late Majority", + "Laggards", + "Non-Adopters" + ) + ) + +table(toa_class) +``` + +``` +## toa_class +## Early Adopters Early Majority Late Majority Laggards Non-Adopters +## 50 65 33 51 24 +``` + +Next, in order to investigate how these different adopters related to other node attributes we will create a data frame containing these `toa_class` values as well as the beginning dates of each site and then create box plot of beginning date by `toa_class`. + + +```r +df <- data.frame(BeginDate = attr$Begin, Category = toa_class) + +library(ggplot2) + +ggplot(data = df) + + geom_boxplot(aes(y = BeginDate, fill = Category)) + + theme_bw() +``` + + + +As this box plot illustrates, sites that were in the "Early Adopter" or "Early Majority" category include the vast majority of sites that have earlier starting dates though the median is the same across groups. This may suggest that network distance from Chaco Canyon (where we originated our "contagion" and where the earliest Great Houses are found) may have been a factor in the establishment of Chacoan complexes outside of Chaco. Of Course, if wanted to take this further we would need to assess the variable roles of spatial distance, network distance, and perhaps could even consider material cultural similarity data. At this point, however, this brief example at least points out that there is an interesting pattern worth investigation. Further, this example demonstrates one simple approach that could be used to compare diffusion models to other archaeological data. + +We have only scratched the surface on the network methods that can be used to study diffusion here. There are many other advanced models that may be relevant for archaeological analysis including many interesting [Epidemiological Models](http://www.epimodel.org/tut.html) that would likely work well in archaeological context for considerations of all sorts of contagions (social or biological). We hope these brief examples will promote further exploration of such approaches. diff --git a/11-comparing-networks.Rmd b/11-comparing-networks.Rmd index d70a6e3..da24cc4 100644 --- a/11-comparing-networks.Rmd +++ b/11-comparing-networks.Rmd @@ -649,7 +649,7 @@ Importantly, the quantification of graphlets for graphlet-based methods discusse ``` -Let's start by using learning how to use the `orca` package in R to calculate all orbits for a given $k$ for network objects. By way of example, we will once again use the [Roman Roads network data](data/road_networks.RData) as the first network and the [Cibola ceramic similarity network](data/Cibola_adj.csv) as the second (click on the links for each to download and follow along). To use the `orca` package, we need to convert our igraph network objects into edge list with all each column defined as integer data. We do this using the `apply` function with a `get.edgelist` function nested within it. After making this conversion we use the `orca::count4` and `orca::count5` functions to define counts of orbits for the $k=4$ and $k=5$ node configurations respectively. We then view the first few rows of each. These output data frames have a row for each node and the columns represent the counts of orbits of each configuration using the same numbering scheme shown in the figure above. +Let's start by using learning how to use the `orca` package in R to calculate all orbits for a given $k$ for network objects. By way of example, we will once again use the [Roman Roads network data](data/road_networks.RData) as the first network and the [Cibola ceramic similarity network](data/Cibola_adj.csv) as the second (click on the links for each to download and follow along). To use the `orca` package, we need to convert our igraph network objects into edge list with all each column defined as integer data. We do this using the `apply` function with an `igraph::as_edgelist` call nested within it. After making this conversion we use the `orca::count4` and `orca::count5` functions to define counts of orbits for the $k=4$ and $k=5$ node configurations respectively. We then view the first few rows of each. These output data frames have a row for each node and the columns represent the counts of orbits of each configuration using the same numbering scheme shown in the figure above. ```{r} library(orca) @@ -665,7 +665,7 @@ cibola_net <- igraph::graph_from_adjacency_matrix(as.matrix(cibola), # Calculate orbits for k = 4 and 5 for the Roman road network road_net_int <- - t(apply(igraph::get.edgelist(road_net, names = F), 1, as.integer)) + t(apply(igraph::as_edgelist(road_net, names = FALSE), 1, as.integer)) road_orb4 <- orca::count4(road_net_int) road_orb5 <- orca::count5(road_net_int) @@ -676,7 +676,7 @@ head(road_orb5) # Calculate orbits for k = 4 and 5 for the Cibola network cibola_net_int <- - t(apply(igraph::get.edgelist(cibola_net, names = F), 1, as.integer)) + t(apply(igraph::as_edgelist(cibola_net, names = FALSE), 1, as.integer)) cibola_orb4 <- orca::count4(cibola_net_int) cibola_orb5 <- orca::count5(cibola_net_int) @@ -827,8 +827,8 @@ require(orca) netGCD <- function(net1, net2) { # Convert igraph objects into integer edge lists - net1a <- t(apply(igraph::get.edgelist(net1, names = F), 1, as.integer)) - net2a <- t(apply(igraph::get.edgelist(net2, names = F), 1, as.integer)) + net1a <- t(apply(igraph::as_edgelist(net1, names = FALSE), 1, as.integer)) + net2a <- t(apply(igraph::as_edgelist(net2, names = FALSE), 1, as.integer)) # Calculate orbit counts for 4 nodes and exclude redundant indexes orb1 <- orca::count4(net1a)[, -c(4, 13, 14, 15)] diff --git a/11-comparing-networks.knit.md b/11-comparing-networks.knit.md new file mode 100644 index 0000000..8d057de --- /dev/null +++ b/11-comparing-networks.knit.md @@ -0,0 +1,1270 @@ +# Comparing Networks{#ComparingNetworks} + +![](images/image_break.png){width=100%} + +How can we systematically and quantitatively compare network configurations and node/edge structural positions? Can we compare networks even if they represent different kinds of phenomena and have vastly different numbers of nodes and edges? What are the best metrics to use in our comparisons (i.e., graph level vs. node/edge level metrics)? Can we capture both global network properties and local structures in our comparisons? These have all been common questions in the world of network research in recent years. This is not surprising as one of the most exciting aspects of networks in general is the realization that many networked systems share common properties and dynamics despite representing wholly different kinds of relations and social or biological systems. Identifying and specifying the relative similarity or distance among different networks, thus, would likely get us quite far. + +There has been increased interest in developing models for directly comparing networks in recent years (see Tantardini et al. 2019). This work is quite variable but can generally be grouped into two sets of approaches: + +* **Known Node Correspondence** - Methods focused on comparing network configurations which share a common set of nodes (or at least have a sub-set of nodes in common). +* **Unknown Node Correspondence** - Methods focused on comparing networks represented by different sets of nodes, which can differ dramatically in size, content, and the nature of ties. + +There are lots of methods out there for both approaches and importantly, there has been considerable methodological work focused on these methods to identify which work best for characterizing and comparing important dimensions of network variability in different contexts (e.g., Milenković and Pržulj 2008; Milenković et al. 2010; Pržulj 2007; Tantardini et al. 2019; Trpevski et al. 2016; Yaveroğlu et al. 2014). In this section, we briefly explore both "known node correspondence (KNC)" and "unknown node correspondence (UNC)" methods (borrowing the useful terminology of Tantardini et al. 2019) and the potential utility of such approaches for archaeological network research. + +
+

Many of the methods outlined in this section are fairly +computationally intensive and involve iterative calculations that scale +rapidly in complexity with the size of the networks involved. Although +there are implementations of many of these methods in R, other analyses +are more efficient or more readily available in Python or other +languages. Do not fear! R-Studio has has the built-in ability to run +Python scripts and to use Python libraries and we show how this works in +the examples below. Using a package called reticulate it is +relatively easy to move back and forth between R and Python.

+
+![R-eticulated Python](images/reticulated_python.png){width=80%} + +## Known Node Correspondence Methods{#KnownNode} + +In this section, we outline potential analytical approaches we can take when our networks share the same nodes or when they have a substantial sub-set of nodes in common. Approaches in this vein are frequently used for comparing different layers of multi-layer networks or different temporal slices of dynamic networks. As both of these kinds of data are common in archaeology these methods likely have much to offer. + +### Direct Comparison of Adjacency Matrices{#CompareAdjacencyMatrices} + +The simplest approach to comparing networks with a common set of nodes is to simply directly evaluate differences in the underlying adjacency matrices. For example, you can simply take the absolute difference of one matrix from the other and then normalize it based on some distance metric. Importantly this simple approach works on both simple binary networks, directed networks, as well as weighted networks. + +For the example here we will use three time slices of a ceramic similarity network from the [Southwest Social Networks Project](#SWSN) San Pedro region dating to AD1250-1300, AD1300-1350, and AD1350-1400 respectively. Each network object is named based on the first year in the 50 year interval it represents. These network slices have nodes in common but the nodes present in each interval are not identical. Thus, we first create a function below called `network_subset_common` which requires two network objects and the name attribute and outputs a list with two networks that include only those nodes where the two overlap. Let's start by importing the data and finding the common networks for every combination of the three time slices. Our output objects are named `net` followed by the year indicating the first 50 year interval included, then `_`, and then the year indicating the second 50 year interval. Here we make lists for both the consecutive intervals and the non-consecutive intervals. Download the [data here](data/SanPedro_nets.Rdata) to follow along: + + +```r +library(statnet) +``` + +``` +## Installed ReposVer Built +## ergm "4.6.0" "4.12.0" "4.2.3" +## ergm.count "4.1.1" "4.1.3" "4.2.3" +## ndtv "0.13.3" "0.13.4" "4.2.3" +## network "1.18.2" "1.20.0" "4.2.3" +## networkDynamic "0.11.4" "0.12.0" "4.2.3" +## sna "2.7-2" "2.8" "4.2.3" +## statnet.common "4.9.0" "4.13.0" "4.2.3" +## tergm "4.2.0" "4.2.2" "4.2.3" +## tsna "0.3.5" "0.3.6" "4.2.3" +``` + +```r +load("data/SanPedro_nets.Rdata") + +network_subset_common <- + function(net1, net2, namevar = "vertex.names") { + net1_names <- + network::get.vertex.attribute(net1, attrname = namevar) + net2_names <- + network::get.vertex.attribute(net2, attrname = namevar) + common_names <- net1_names[which(net1_names %in% net2_names)] + out1 <- net1 %s% which(net1 %v% namevar %in% common_names) + out2 <- net2 %s% which(net2 %v% namevar %in% common_names) + return(list(out1, out2)) + } + +net1250_1300 <- + network_subset_common(AD1250net, AD1300net, namevar = "vertex.names") +net1300_1350 <- + network_subset_common(AD1300net, AD1350net, namevar = "vertex.names") +net1250_1350 <- + network_subset_common(AD1250net, AD1350net, namevar = "vertex.names") +``` + +In order to make comparisons, we next extract the adjacency matrices from the network list objects of interest. Let's start by comparing AD1250-1300 and AD1300-1350. First we extract the matrices: + + +```r +net1 <- as.matrix(net1250_1300[[1]]) +net2 <- as.matrix(net1250_1300[[2]]) +``` + +Next, we find the absolute value of the difference between these two adjacency matrices: + + +```r +net_diff <- abs(net1-net2) + +# Let's look at the first 4 rows and columns +net_diff[1:4, 1:4] +``` + +``` +## Artifact Hill Ash Terrace Bayless Ruin Big Bell +## Artifact Hill 0 1 0 0 +## Ash Terrace 1 0 0 0 +## Bayless Ruin 0 0 0 0 +## Big Bell 0 0 0 0 +``` + +Now we can simply identify the number and proportion of edges that differ between the two adjacency matrices. This metric can be formally defined as: + +$$S = 1- \frac{\Sigma | A^1_{ij} - A^2_{ij}|}{n(n-1)}$$ + +where + +* $A^1_{ij}$ and $A^2_{ij}$ are the adjacency matrices with identical sets of nodes. +* $n$ is the number of nodes + +Let's take a look using the matrices we created above: + + +```r +1 - (sum(net_diff) / (nrow(net_diff) * (nrow(net_diff) - 1))) +``` + +``` +## [1] 0.8205128 +``` + +So this number let's us know that the proportion of ties in common between our two networks is `0.82`. + +We can roll this whole process into a function for ease of use. The function we use here expects two adjacency matrices with the same node set. + + +```r +adjacency_compare <- function(net1, net2) { + net_diff <- abs(net1 - net2) + out <- + 1 - (sum(net_diff) / (nrow(net_diff) * (nrow(net_diff) - 1))) + return(out) +} + +# Run function for comparing AD1300-1350 to AD1350-1400 +adjacency_compare(as.matrix(net1300_1350[[1]]), + as.matrix(net1300_1350[[2]])) +``` + +``` +## [1] 0.625731 +``` + +```r +# Run function for comparing AD1250-1300 and AD1350-1400 +adjacency_compare(as.matrix(net1250_1350[[1]]), + as.matrix(net1250_1350[[2]])) +``` + +``` +## [1] 0.474359 +``` + +As these results show, our comparison between AD1250-1300 and AD1300-1350 showed the most overlap, followed by the AD1300-1350 and AD1350-1400 interval with an overlap of `0.63` and then finally our non-sequential comparison between AD1250-1300 and AD1350-1400 with an overlap of `0.47`. + +This procedure also works on weighted networks, directed networks, or any other form of one-mode network. In the next chunk of code we create two random weighted networks, convert them to adjacency matrices and then compare them using our function: + + +```r +library(igraph) +set.seed(3464) +rnet1 <- erdos.renyi.game(n = 40, p.or.m = runif(1)) +E(rnet1)$weight <- sample(1:5, ecount(rnet1), replace = TRUE) + +rnet2 <- erdos.renyi.game(n = 40, p.or.m = runif(1)) +E(rnet2)$weight <- sample(1:5, ecount(rnet2), replace = TRUE) + +par(mfrow=c(1,2)) +plot(rnet1) +plot(rnet2) +``` + + + +```r +par(mfrow=c(1,1)) + +ra1 <- as_adjacency_matrix(rnet1, sparse = FALSE) +ra2 <- as_adjacency_matrix(rnet2, sparse = FALSE) + +adjacency_compare(ra1, ra2) +``` + +``` +## [1] 0.5051282 +``` + +This basic adjacency matrix comparison approach can help us get a sense of how similar or different our networks are but it does not account for the direction of difference nor does it provide a means for interpreting that difference in relation to network structure. As we will see, the examples below extend this basic approach in several ways that allow us to consider such features of our network data. + +### Quadratic Assignment Procedure{#QAP} + +The quadratic assignment procedure (QAP) is a method for evaluating network correlations or other network metrics among two networks with the same node-set using Monte Carlo simulation methods. In R, the QAP function and test are available in the `statnet` package through a function called `qaptest`. + +Using the lists of network objects with same node-set we defined in the last example, we can make further assessments of the similarity between them using the `gcor` or graph correlation function in the `sna` package. This function gives a number between -1 and 1 which indicates the degree and direction of similarity in edges between the two networks. If the two networks have all present and absent edges in common, they would get a value of 1 and if absence and presence are reversed (all active edges in net1 are inactive in net2 and all inactive edges in net1 are active in net2) they would get a value of -1. + + + +```r +gcor(net1250_1300)[[2]] +``` + +``` +## [1] 0.6266283 +``` + +```r +gcor(net1300_1350)[[2]] +``` + +``` +## [1] 0.3873585 +``` + +```r +gcor(net1250_1350)[[2]] +``` + +``` +## [1] 0.2070197 +``` + +As the results here show, AD1250-1300 and AD1300-1350 are reasonably similar with a graph correlation of `0.63` and AD1300-1350 and AD1350-1400 is less similar with a correlation of `0.39`. When we compare the non-sequential intervals AD1250-1300 and AD1350-1400 we see even lower similarity at `0.21`. Notably all values are positive. The question that remains, however, is how we might interpret these values. For example, if we had two nearly complete or nearly empty networks we could potentially get a high graph correlation by chance. The QAP test is designed to deal specifically with this issue. + +The QAP test creates a large number of random versions of the two imported network objects (by randomly switching node label assignments while keeping the same network structure) and then calculates the graph correlation (or some other graph level function) between the random versions of the networks for every simulation (the number of simulations is set to 1000 by default). The logic behind this is that if randomly shuffling of the network labels generated new networks that frequently produced graph correlation values as high or higher than the observed, that would suggest that the correlation between our observed networks could be easily generated by chance given network size and density. If, on the other hand, few or no random network label shufflings produced correlations as high and higher than the observed correlation, that could be used to reject the null hypothesis that the relationship between our graphs is random. + +Let's give it a shot for all three comparisons and then discuss the results: + + + +```r +set.seed(43673) + +qap1250_1300 <- qaptest(net1250_1300, gcor, g1 = 1, g2 = 2) +qap1250_1300 +``` + +``` +## +## QAP Test Results +## +## Estimated p-values: +## p(f(perm) >= f(d)): 0 +## p(f(perm) <= f(d)): 1 +``` + +```r +qap1300_1350 <- qaptest(net1300_1350, gcor, g1 = 1, g2 = 2) +qap1300_1350 +``` + +``` +## +## QAP Test Results +## +## Estimated p-values: +## p(f(perm) >= f(d)): 0.001 +## p(f(perm) <= f(d)): 1 +``` + +```r +qap1250_1350 <- qaptest(net1250_1350, gcor, g1 = 1, g2 = 2) +qap1250_1350 +``` + +``` +## +## QAP Test Results +## +## Estimated p-values: +## p(f(perm) >= f(d)): 0.093 +## p(f(perm) <= f(d)): 0.967 +``` + +The value we're most interested in here is the top p-value which is the simulated probability of obtaining a correlation as high or higher than the observed from randomly shuffled versions of our network. The first two comparisons for temporally sequenced periods both show simulated p-values at or near `0` suggesting that there is a low probability of obtaining correlations as high as the observed in shuffled networks. For the comparison between the non-consecutive AD1250-1300 and AD1350-1400 intervals, however, we get a considerably higher probability with `p = 0.093`. Our results indicate that in 93 of the 1000 random network label shuffles a correlation value as high or higher than the observed was recorded. In light of this we would probably not put much weight behind the correlation between those periods. As the function documentation suggests, QAP results should not be interpreted as indicative of underlying structural differences between networks, but instead simply differences in a particular node labeling schema controlling for structural differences (as only labels are shuffled and not actual network structures). + +Finally, we can plot a visualization of the QAP test which provides a density plot of the correlation values produced in each random replication along with a dotted line representing the observed value: + + +```r +plot(qap1250_1300, xlim = c(-1, 1)) +``` + + + +```r +plot(qap1300_1350, xlim = c(-1, 1)) +``` + + + +```r +plot(qap1250_1350, xlim = c(-1, 1)) +``` + + + +QAP is potentially useful for identifying whether or not a given similarity or other graph level metric between two graphs is statistically significant, but there are also problems with this method in that results can also vary in relation to the size and topological nature of the networks in question (see Anderson et al. 1999). + +### DeltaCon{#DeltaCon} + +Another method for comparing networks with a common node-set that has more recently gained popularity is the DeltaCon approach (Koutra et al. 2016). This method is similar to the direct comparison of adjacency matrices except that it is based on the commonality in paths between networks rather than just specific edges. The underlying assumption here is that simply matching first degree connections doesn't capture the true similarities and differences in networks and capturing paths of various lengths will likely provide a better assessment of structural similarities and differences among networks. Much like the adjacency matrix methods above DeltaCon ranges from 0 to 1 and provides and indication of the strength of association between the two networks. + +The analytical details and justification of DeltaCon are beyond the scope of this guide and we direct you [to the original paper](https://dl.acm.org/doi/abs/10.1145/2824443) where this method was defined for more details. Luckily, GitHub user [Baoxu "Dash" Shi](https://github.com/bxshi) has already created an R function to calculate DeltaCon that is [available here](https://github.com/bxshi/rdsg). We have ported that function into a script and simplified it somewhat in this repository and use that simplified version here. Note that this function relies on three packages which must be installed to use this function: `Matrix`, `SparseM`, and `pracma`. + +This function expects two numeric edge lists representing networks with the same node set and you also must indicate the number of nodes in each (so that unconnected nodes/isolates can also be considered). Let's give this a try with the same San Pedro network data we imported above. [Click here](scripts/delta_con.R) to download our modified version of the script used here. The input expected is two matrix objects in the form of an adjaency matrix with the same set of nodes: + + + +```r +source("scripts/delta_con.R") + +el1 <- as.matrix(net1250_1300[[1]]) +el2 <- as.matrix(net1250_1300[[2]]) + +delta_con(el1, el2) +``` + +``` +## [1] 0.8639706 +``` + +We get a value of `0.86` which is just a little higher than our adjacency matrix comparison. Now let's try it for the other interval comparisons: + + +```r +el1 <- as.matrix(net1300_1350[[1]]) +el2 <- as.matrix(net1300_1350[[2]]) + +delta_con(el1, el2) +``` + +``` +## [1] 0.6645235 +``` + +```r +el1 <- as.matrix(net1250_1350[[1]]) +el2 <- as.matrix(net1250_1350[[2]]) + +delta_con(el1, el2) +``` + +``` +## [1] 0.6992425 +``` + +Interestingly, when we compare these results to the simple adjacency matrix comparison above we see some pretty big differences. In the adjacency matrix comparison the non-consecutive intervals were the least similar of the three comparisons with an overlap proportion of about `0.47`. Here the DeltaCon for that comparison is actually higher than that for the AD1300-1350 and AD1350-1400 comparison. This suggests that, although there may be first order differences in nodes between the non-sequential networks, there are perhaps slightly greater similarities in longer paths between the two. This example illustrates how making multiple comparisons with different approaches can sometimes reveal unexpected insights. + + +## Unknown Node Correspondence{#UnknownNode} + +Although methods for comparing networks with a common node-set are useful, perhaps the more common use case for network comparison considers networks that do not have the same nodes or even the same size. Indeed, network analysts are frequently interested in characterizing the similarity and differences among networks that have entirely different sources (for example, comparing road networks to the internet). There are a variety of methods designed for this use case and here we highlight a few of the most useful drawing on the methodological comparison of methods presented by [Tantardini et al. 2019](https://www.nature.com/articles/s41598-019-53708-y#Sec2). Many of the methods outlined by Tantardini and colleagues are available in R but even more are available in Python and anyone considering seriously investigating these approaches for archaeological data would probably want to use Python (we show how you can use Python within R-Studio below). + +### Comparing Network Global and Local Statistics{#ComparingStatistics} + +Perhaps the simplest and most common means for comparing networks is to simply compare different network metrics and properties of those networks (standardized by network size or type). For example, we could calculate the density of two networks and compare the values and get some sense of how they relate (at least in terms of that one feature). Alternatively, we could calculate node or edge based metrics like centrality and then compare the resulting distributions. We have already shown several examples of this type of comparison in the Exploratory Network Analysis section of this document with our [Roman Roads Case Study](#ExploratoryRomanRoads). In that example, we happened to be using three networks with the same node-set but this is certainly not required. + +In the chunk of code below we reproduce our `net_stats` function for calculating a range of network metrics from the [Exploratory +Network Analysis](#Exploratory) section and then calculate those stats for our San Pedro time-slice networks. + + +```r +library(igraph) + +net_stats <- function(net) { + out <- matrix(NA, 10, 2) + out[, 1] <- c("Nodes", "Edges", "Isolates", "Density", "Average Degree", + "Average Shortest Path", "Diamater", + "Clustering Coefficient", "Closed Triad Count", + "Open Triad Count") + # number of nodes + out[1, 2] <- vcount(net) + # number of edges + out[2, 2] <- ecount(net) + # number of isolates + out[3, 2] <- sum(igraph::degree(net) == 0) + # network density rounding to the third digit + out[4, 2] <- round(edge_density(net), 3) + # mean degree rounding to the third digit + out[5, 2] <- round(mean(igraph::degree(net)), 3) + # mean shortest path length rounding to the third digit + out[6, 2] <- round(igraph::mean_distance(net), 3) + # network diameter + out[7, 2] <- igraph::diameter(net) + # average global transitivity rounding to the third digit + out[8, 2] <- round(igraph::transitivity(net, type = "average"), 3) + # closed triads in triad_census + out[9, 2] <- igraph::triad_census(net)[16] + # open triads in triad_census + out[10, 2] <- igraph::triad_census(net)[11] +return(out) +} +``` + + +Since our San Pedro networks are `network` format objects we use the `intergraph` package to convert them to the `igraph` objects this function expects. + + +```r +library(intergraph) + +ns1 <- net_stats(asIgraph(AD1250net)) +ns2 <- net_stats(asIgraph(AD1300net)) +ns3 <- net_stats(asIgraph(AD1350net)) + +ns_res <- cbind(ns1, ns2[, 2], ns3[, 2]) +colnames(ns_res) <- c("Measure", "AD1250-1300", "AD1300-1350", + "AD1350-1400") + +knitr::kable(ns_res, format = "html") +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Measure AD1250-1300 AD1300-1350 AD1350-1400
Nodes 13 21 20
Edges 26 89 165
Isolates 0 0 0
Density 0.333 0.424 0.868
Average Degree 4 8.476 16.5
Average Shortest Path 1.554 1.971 1.132
Diamater 3 5 2
Clustering Coefficient 0.832 0.848 0.96
Closed Triad Count 23 247 855
Open Triad Count 39 119 128
+ +Data like this can be useful for making general comparisons of networks. For example, we can see major differences in density among our time periods. Further the ratio of open to closed triads differs dramatically through time. + +Another simple approach that is frequently used for comparing networks is to compare distributions of node or edge level statistics. For example, we can compare degree distributions among our networks here using simple histogram visuals: + + +```r +hist( + sna::degree(AD1250net, rescale = TRUE), + main = "AD1250-1300", + xlab = "Degree" +) +``` + + + +```r +hist( + sna::degree(AD1300net, rescale = TRUE), + main = "AD1300-1350", + xlab = "Degree" +) +``` + + + +```r +hist( + sna::degree(AD1350net, rescale = TRUE), + main = "AD1350-1400", + xlab = "Degree" +) +``` + + + +A quick glance at these three plots shows that the degree distributions differ in magnitude and the direction of skew among our three intervals. We could take this further by examining features of these distributions in detail and this may provide additional information about differences in the network structures and potential generative processes. + +Simple comparisons like those shown here are often a good first step, but it can be difficult to know what to make of differences in such metrics or distributions. In the next several examples, we present approaches that are designed to provide information to help put such single metric/feature comparisons in context. + +### Graph Kernel Methods{#NetworkKernel} + +A network (or graph) kernel is a function that measures the similarity of a pair of networks based on comparisons of vectors representing specific features of those networks. In practice this represents an inner product of two vectors representing some relevant feature (in a single dimension) of those networks. To explain how this works let's consider a hypothetical example where we want to compare a set of books on various topics to determine how similar each book is to all the other books. Say we have these books as digital text files and we have not previously read them. How might we begin? One relatively simple approach (simple for a computer at least) would be to tabulate all of the words in each book and then compare the frequency distributions of each book to all the others. This is the so-called "bag of words" approach where similarities in books are defined not by the structure of the content but simply by the overlap in word frequency. Two books on fishing are more likely to have more words in common than a book on fishing and another on nuclear physics. We can extend this same general line of thinking to networks to create a "bag of graphs" representation of the underlying network which provide vectors for comparison (see Silva et al. 2018). + +Let's start with a relatively simple example. Let's say we are interested in defining a kernel for comparing two networks based on their "graphlet" representation. A graphlet is simply a set of possible configurations that a set of nodes can take for a given $k$ number of nodes. For example, for 3 nodes the following graphlets are possible configurations: + + + +So for two graphs, we could then compare the number of times each of these configurations appears. Let's make a couple of small random graphs and try it out: + + +```r +set.seed(4354) +g1 <- erdos.renyi.game(6, 0.6) +g2 <- erdos.renyi.game(5, 0.4) + +plot(g1) +``` + + + +```r +plot(g2) +``` + + + +Now we can tabulate the number of graphets of each configuration from 0 to 3 as denoted above for each graph as a vector: + +$$\begin{aligned} +f_{G1} =& (0, 4, 8, 8) \\ +f_{G2} =& (0, 3, 6, 1) +\end{aligned}$$ + +Now we further need to account for the size of each network and the number of graphlets so we can divide the graphlet counts but total number of graphlets in each network: + +$$\begin{aligned} +f_{G1} =& (0, 4, 8, 8) / 20 = (0, 0.2, 0.4, 0.4) \\ +f_{G2} =& (0, 3, 6, 1) / 10 = (0, 0.3, 0.6, 0.1) +\end{aligned}$$ + +Now to create we can create a kernel describing the relationship between these two graphs by calculating the inner product of the transpose of the vector for the first graph by the vector of the second graph: + +$$K(G1,G2) = F_{G1}^T \cdot F_{G2} = 0.34$$ + + +```r +fg1 <- c(0, 4, 8, 8) +fg1 <- fg1 / sum(fg1) + +fg2 <- c(0, 3, 6, 1) +fg2 <- fg2 / sum(fg2) + +t(fg1) %*% fg2 +``` + +``` +## [,1] +## [1,] 0.34 +``` + +So in this case our returned value is `0.34` for graphlet kernel $k=3$. + +There are many other kinds of kernels that can be calculated on such networks. For example, we can calculate a shortest path kernel which determines how many shortest paths of length 1 through the diameter of the network (standardized for the total number of shortest paths possible) for both networks. Further, we could create a random walk kernel by generating a number of random walks on both networks simultaneously and then quantifying the number of matching walks. There are many different types of kernels that can be calculated in networks and the details of these are described elsewhere (see Ghosh et al. 2018). + +
+

The package graphkernels provides a set of 14 graph +kernel calculation methods that can be calculated for both R and Python. +This package works using igraph network objects and expects +a list of networks grouped in a single object. We use the R +implementation in our example here.

+
+ +In the chunk of code below, we convert our San Pedro `network` objects into the `igraph` network format and then calculate graphlet kernel for $k=3$, a random walk kernel, and finally a shortest path kernel. The results are provided as a symmetric matrix and we can evaluate comparisons between networks by looking at the row and column corresponding to the item number in the list provided to the function. The values are in terms of whatever measure is provide (e.g., number or length of random walks, proportional overlap of graphlets, etc.) but in general, larger numbers indicate greater similarity between networks for a given feature. + +Let's give it a try: + + + +```r +library(graphkernels) +library(intergraph) +g <- list(asIgraph(AD1250net), asIgraph(AD1300net), asIgraph(AD1350net)) + +k_graphlet <- CalculateGraphletKernel(g, par = 3) + +k_rw <- CalculateGeometricRandomWalkKernel(g, par = 0.00001) + +k_sp <- CalculateShortestPathKernel(g) + +out <- matrix(NA, nrow = 3, ncol = 3) + +out[1, 1] <- k_graphlet[1, 2] +out[2, 1] <- k_graphlet[2, 3] +out[3, 1] <- k_graphlet[1, 3] + +out[1, 2] <- k_rw[1, 2] +out[2, 2] <- k_rw[2, 3] +out[3, 2] <- k_rw[1, 3] + +out[1, 3] <- k_sp[1, 2] +out[2, 3] <- k_sp[2, 3] +out[3, 3] <- k_sp[1, 3] + +row.names(out) <- + c("AD1200-1250, 1300-1350", + "AD1300-1350, 1350-1400", + "AD1250-1300, 1350-1400") +colnames(out) <- c("graphlets", "random walks", "shortest paths") + +knitr::kable(out, "html") +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
graphlets random walks shortest paths
AD1200-1250, 1300-1350 0.3436301 273.0926 9256
AD1300-1350, 1350-1400 0.2206312 420.5885 58740
AD1250-1300, 1350-1400 0.1412342 260.1718 17160
+ +As these results above show, the rank-order of kernel values for different metrics are not the same, despite using the same networks. As this illustrates, a single graph kernel doesn't typically paint the whole picture and graphs can be similar in some features and not others. There are many more graph kernel methods available and it would likely be profitable for archaeological network analysts to explore these methods in greater detail. + +### Spectral Methods{#SpectralMethods} + +A network *spectrum* is a complex set of functions based on *eigenvalues* of a network that describe the structural properties of that network in $n$ or fewer dimensions when $n$ is the number of nodes in that network. Many approaches to network comparison rely on network *spectra*. In short you can think of a *spectrum* as a set of values that describe the relationships in a network based on linear transformations of the underlying adjacency matrix and those can be compared between different networks. A full explanation of eigenvectors and eigenvalues is beyond the scope of this document ([but check here for a good video explanation](https://www.youtube.com/watch?v=PFDu9oVAE-g)) but in short, eigenvectors ($\overrightarrow v$) and eigenvalues ($\lambda$) for an adjacency matrix ($A$) satisfy the following equation: + +$$\begin{aligned} +A \overrightarrow v = & \lambda \overrightarrow v \\ +A \overrightarrow v - \lambda \overrightarrow v = & 0 +\end{aligned}$$ + +That is, the adjacency matrix multiplied by the eigenvector is equal to the eigenvalue multiplied by the same eigenvector. In two dimensions, we can visualize how this might work by imagining that we stretch or shear an image in a particular direction and evaluate the outcome for specific vectors along that image. In the Mona Lisa example below when we apply a shearing transformation to the famous painting, points along the red axis are no longer along the same directional vector after the transformation. Points along the blue axis, however, are still exactly where they were before the transformation. Thus, the blue vector is an *eigenvector*. In this case we have not re-scaled the image so points along the blue vector are in the same place in both images. Thus, our *eigenvalue* would be 1. If we had sheared the pictures as we see here but also scaled the picture by a factor of 2 our *eigenvalue* for the blue vector would be 2 meaning that if a particular pixel was on the point (0,1) in the first image it would be on the point (0,2) in the sheared and scaled image. + +![Eigenvector image stretching example](images/Mona_Lisa_eigenvector_grid.png){width=100%} + +If we want to extend this to three dimensional space we could imagine rotating a cube in some direction around some random axis. If we think of every point within that cube as having coordinates in 3D space, the only points that will remain along the same vector same after the rotation are those that fall along the axis of rotation. That would be the eigenvector of that transformation. If we were to expand or shrink the shape while making this rotation, the scaling factor would represent the eigenvalue. + +When we apply this same approach to a larger n-dimensional space like an adjacency matrix representing $n$ nodes it becomes a bit harder to visualize but the underlying principle is the same. The set of all eigenvalues (which is of length $n$ for an $n$ x $n$ adjacency matrix) ordered from largest to smallest is the *spectrum* of that matrix. Once we have these in hand for two graphs, we can simply find the Euclidean distance between them as: + +$$d(G_1,G_2) = \sqrt{\Sigma_i(s_i^{(1)}-s_i^{(2)})^2} $$ + +where + +* $s_i^{(1)}$ is the spectrum for graph 1 ($G_1$) ordered from largest to smallest +* $s_i^{(2)}$ is the spectrum for graph 2 ($G_2$) ordered from longest to smallest. + +If one spectrum vector is longer than the other the vector can be padded with zeros while maintaining the ordering. + +Spectral comparisons are useful for comparing graphs of different sizes but there are some issues including the fact that different graphs can have the same spectra and small changes in graph structure can sometimes result in large changes in spectra. Despite these potential problems these methods are still common, especially for comparisons of large graphs as they can be calculated efficiently. + +To take this one step further, many graph-based spectral comparison methods are not based on the adjacency matrix directly, but instead further derivations of that matrix that highlight special features. For example, many methods are based on what is called the Laplacian matrix. A Laplacian matrix is the degree matrix of a graph minus the adjacency matrix of the same graph. The degree matrix is defined as a $n$ x $n$ matrix where all entries outside of the diagonal are 0 and the values along the diagonal represent the degree of the network for each node. The same methods described above can be used to find eigenvector and eigenvalues of a Laplacian matrix by simply replacing that matrix for the adjacency matrix ($A$) in the equation above. + +There are a few existing packages out there that allow for assessments of spectral-based graph comparisons. We have found many of the most useful and efficient to be limited to Python rather than R. Thus, we take this an an opportunity to explore how Python scripts can be used within R-Studio and using R objects using the package called `reticulate`. + +
+

The R package reticulate is designed to help you move +objects and functions back and forth between R and Python environments +on your computer. In order to use reticulate you need to +install the R package and also install an instance of Python >= 3.8 +on your computer. The easiest way to install Python on your machine if +you don’t already have it is to use the +reticulate::install_miniconda() right in your R console. Check here for more +details.

+
+ +Let's first initialize the `reticulate` package: + + +```r +library(reticulate) +``` + +``` +## Warning: package 'reticulate' was built under R version 4.2.3 +``` + +At this point install Python if you have not already done that using the `reticulate::install_miniconda()` command. Once Python in installed you will then need to install a Python package called [NetLSD (Network Laplacian Spectral Descriptors)](https://github.com/xgfs/NetLSD). The easiest way to do this is to click on the "Terminal" panel in your R-Studio instance and then type the following command directly at the terminal (you only need to do this once in an instance of R-Studio): + + +```python +pip install netlsd +``` + +If all goes well, that will install the NetLSD Python package in the instance of Python associated with your R-Studio installation and then you call Python commands either directly from the terminal or using special functions built into the `reticulate` package. This is a package that calculates spectral distance between networks using a version of the Laplacian matrix described above to define eigenvalues for each graph. + +First, let's create a few network objects in R that we can use in a Python environment. In this case we need adjacency matrices for the NetLSD package: + + + +```r +g1 <- as.matrix(AD1250net) +g2 <- as.matrix(AD1300net) +g3 <- as.matrix(AD1350net) +``` + +
+

In any R Markdown document in R-Studio you can create a block and +specify it as Python code by simply changing the initial r +inside the {r} to {python}.

+
+ +Now that we've created our objects in R, we can run our functions in Python. The following chunk of code is actually in Python rather than R although it looks relatively similar. The first few lines consist of import commands which tell Python to initialize the required packages we will use (this is much like the `library()` function in R). + +Next, we create Python objects from our R objects. Any object that is in the R global environment can be called within Python in R-Studio by using the prefix `r.` before the name of the object. In the code below, we define a Python object called `graph1` that is based on the R object called `g1` using the `r.g1` command. + +The next line of codes represent functions that are within the Python package we installed. First we call a function called `heat` within the `netlsd` package by adding `netlst.heat()` and then providing the object to which this function should be calculated in the parentheses. Next we then calculate the Laplacian spectral distance between two graphs using a function in the `numpy` package (which we imported with the shortened name `np`) by typing `np.linalg.norm`. Just like in R, we can assign the result to an object and then type the name of that object to report the output on the screen. + + +``` +## Using virtual environment "C:/Users/mattp/Documents/.virtualenvs/r-reticulate" ... +``` + +``` +## Using virtual environment "C:/Users/mattp/Documents/.virtualenvs/r-reticulate" ... +``` + +``` +## [1] TRUE +``` + +``` +## [1] TRUE +``` + + + + +```python +import netlsd +import numpy as np + +graph1 = r.g1 +graph2 = r.g2 +graph3 = r.g3 + +desc1 = netlsd.heat(graph1) +desc2 = netlsd.heat(graph2) +desc3 = netlsd.heat(graph3) + +distance1_2 = np.linalg.norm(desc1 - desc2) +distance1_2 +``` + +``` +## 0.9955321810540423 +``` + +```python +distance2_3 = np.linalg.norm(desc2 - desc3) +distance2_3 +``` + +``` +## 0.25685544386678943 +``` + +```python +distance1_3 = np.linalg.norm(desc1 - desc3) +distance1_3 +``` + +``` +## 1.1791128279165706 +``` + +The results above show the Laplacian spectral distance between our three temporal networks. Smaller numbers indicate less distance and these numbers are not bounded on the upper end. Our results here show that the distance between AD1300-1350 and AD1350-1400 is the smallest (meaning those graphs are most similar by this measure) and the comparison between non-consecutive intervals is the greatest. + +The advantage of this spectral method is that is a comparison of a summary of the overall structural properties of a network rather than a particular feature as we saw with the graph kernel methods. + +Importantly, this metric could also be used to compare graphs that differ dramatically in size and scale as well. In the next chunk of code we import the `networkx` package as `nx` and then create a random graph with 1000 nodes using the Barabasi-Albert algorithm. We then compare that to our original 13 node network from AD1250-1300 to show how we can compare networks of dramatically different sizes. Note that all eigenvalues after the first 13 for the San Pedro network will be defined as 0. + + + + + + +```python +import networkx +import netlsd + +# create a random graph with 1000 nodes +g4 = networkx.barabasi_albert_graph(1000, m = 20, seed = 13) +desc4 = netlsd.heat(g4) + +distance_new = np.linalg.norm(desc1 - desc4) +distance_new +``` + +``` +## 1.6539873180732307 +``` + +We have certainly not exhausted the possibilities for spectral graph comparison here. In particular, it is currently unclear how such network summaries work for networks with features like common archaeological networks (for example, similarity networks with very high degrees of closure). As spectral methods perform differently for networks with different structural tendencies, such evaluation of archaeological networks with this in mind would be useful. + + +### Portrait Divergence{#PortraitDivergence} + +Another recently developed method which attempts to provide an overall metric of network relationships is referred to as the *portrait divergence* approach (Bagrow et al. 2018). This approach defines divergence among two networks of non-overlapping nodes based on the distribution of shortest path lengths in the network. Specifically, this method creates a network "portrait" which is a matrix $B_{lk}$ where $l$ is an integer vector from $0$ to the network diameter $d$ and $k$ is a vector from $0$ to $N-1$ where $N$ is the number of nodes with $k$ shortest path at each distance $l$. This metric can also be used for weighted and directed networks. Using this matrix $B_{lk}$ portrait divergence is defined as: + +$$P(k,l) = P(k|l)P(l) = \frac {1}{N}B_{lk} \frac {1}{\Sigma_c n^2_c}\Sigma_k k'B_{lk'}$$ + +where + +* $P(k,l)$ is the probability of choosing two nodes at random at distance $l$ and for one of the two nodes selected to have $k$ nodes at distance $l$. $Q(k,l)$ represents the same probability for network two. +* $N$ is the number of nodes in the network. +* $n_c$ is the number of nodes in connected component $c$. + +Portrait convergence is then calculated using the Jensen-Shannon divergence as: + +$$D(G_1,G_2) = \frac {1}{2}KL(P||M) + \frac{1}{2}KL(Q||M)$$ + +where + +* $M = (P+Q)/2$ and is the mixture distribution of $P$ and $Q$ +* $KL(\cdot||\cdot)$ is the Kullback-Liebler divergence. + +That's a lot of math, we know but essentially what this metric does is it creates a matrix that characterizes shortest path lengths in relation to the total diameter of each graph and the compares them based on the resulting probability distributions of paths of various length using assessments of mutual information. The Kullback-Liebler divergence is a measure of information lost when using one probability distribution to estimate another. This measure is not symmetric in that $KL(P||M)$ is not the same as $KL(M||P)$. + +To run this in R, we're going to actually use a Python package/script and initialize it using the `reticulate` command `source_python()` which works essentially like `source()` for R scripts. Our Python script is a modified version of the script [placed on GitHub](https://github.com/bagrow/network-portrait-divergence) by Bagrow ([you can download our modified version here](scripts/portrait_divergence.py)). + +In the next chunk of code we show another way that you can run Python code in R-Studio. In this example we first initialize the `portrait_divergence` function and then create 3 edge list objects. We then run the `portrait_divergence` function using the `py$` prefix. Much like we used the `r.` prefix to call global R objects in Python we can use `py$` to call Python objects in R. Using the `py$portrait_divergence()` function and supplying two edge lists as R objects we can run the function. + + + +```r +library(reticulate) +source_python("scripts/portrait_divergence.py") + +g1 <- as_edgelist(asIgraph(AD1250net)) +g2 <- as_edgelist(asIgraph(AD1300net)) +g3 <- as_edgelist(asIgraph(AD1350net)) + +py$portrait_divergence(g1, g2) +``` + +``` +## [1] 0.5958473 +``` + +```r +py$portrait_divergence(g2, g3) +``` + +``` +## [1] 0.3720442 +``` + +```r +py$portrait_divergence(g1, g3) +``` + +``` +## [1] 0.7556229 +``` + +Portrait divergence ranges from 0 to 1 where lower values indicate a closer relationship in terms of shortest path portrait structure and 1 represents no overlap in the probability distributions for both networks. + +Once again, this can also be used on networks of very different sizes and origins. In the next example we use the [Roman Road networks](data/road_networks.Rdata) we used in Section 3 to compare our San Pedro ceramic network to the road network. This Roman Road network has very different properties from the San Pedro network and is much larger as well. + + +```r +load("data/road_networks.RData") + +py$portrait_divergence(g1, as_edgelist(road_net, names = FALSE)) +``` + +``` +## This graph was created by an old(er) igraph version. +## Call upgrade_graph() on it to use with the current igraph version +## For now we convert it on the fly... +``` + +``` +## [1] 0.8242969 +``` + +When we compare our two networks we see a relatively high portrait divergence value which we might expect given the drastically different nature of these two networks. + +Portrait divergence can also be calculated for weighted networks (based on a three column weighted network edge list) using the `portrait_divergence` function. In this example we add random weights to the same two networks we used in our last example to see how this effects the results. Note that the weighting schema can also have very different scales and this procedure will still work. In the example below weights in the AD1250-1300 ceramic similarity network range from 1-3 whereas values range from 1-100 in the road network. + + +```r +set.seed(46232) +g1w <- cbind(g1, sample(1:3, nrow(g1), replace = TRUE)) +rnet <- as_edgelist(road_net, names = FALSE) +rnetw <- cbind(rnet, sample(1:100, nrow(rnet), replace = TRUE)) + +py$portrait_divergence(g1w, rnetw) +``` + +``` +## [1] 0.7798428 +``` + +As this shows, we get a somewhat lower value when considering random weightings. It would likely be informative to evaluate sets of networks defined using different weighting schemes to track how different approaches to assigning and defining weight lead to different network portraits. + +### Graphlet-based Methods{#GraphletMethods} + +Another set of methods that have proven quite useful are referred to as graphlet-based measures. There are a few different specific models but in general these methods are grouped in that they define similarity in terms of the distribution of non-isomorphic sub-graphs of larger networks (typically ranging from 3 to 5 nodes). Experimental research has shown that these graphlet approaches do a good job of recovering information about the structure of networks, the individual positions of nodes and edges, and overall network topology as well as generative processes. Indeed, in their comparison of many network comparison methods, Tantardini et al. (2019) found graphlet-based methods generally performed the best under most common circumstances (although these methods have yet to be extended to weighted/valued networks). + +The basic premise is that for a set number of nodes there are limited numbers of unique graph motifs that are possible and by tallying the distribution of these and comparing between networks, we can get an idea of how similar networks are in terms of both local and global structural properties. The image below shows all of the possible undirected graphlet configurations for 2, 3, 4, and 5 nodes. The colored shading in this figure shows nodes that have structurally equivalent positions within the graphlet (so all nodes that are the same color in a given graphlet have the same structural position in that graphlet). The graphlets are further numbered (starting with 0) as they typically are in recent publications ($G_0$ to $G_{29}$) and nodes in unique "orbits" are further labeled $O_0$ to $O_{72}$. An orbit in this case refers to a unique structural position within a unique graphlet. + +![Graphlet automorphisms for 2, 3, 4, and 5 nodes (from Melckenbeeck et al. 2019)](images/orbits.png){width=100%} + +The graphlet automorphisms figure above comes from [Melckenbeeck et al. 2019](https://bmcbioinformatics.biomedcentral.com/articles/10.1186/s12859-018-2483-9) in *BMC Bioinformatics* and was originally published with a [CC-by-4.0 licence](https://creativecommons.org/licenses/by/4.0/) and reused here with altered colors. + +
+

Importantly, the quantification of graphlets for graphlet-based +methods discussed here and the graphlet kernel described above are not +exactly the same as the methods outlined in this section do not count +graphlets of size \(k\) with any +unconnected nodes. For example, in graphlet-based methods for \(k=3\) only graphlets are considered where +all three nodes have at least one connection (unlike graphlet kernel +methods which also considered an empty triad and a triad with one +unconnected node).

+
+ + +Let's start by using learning how to use the `orca` package in R to calculate all orbits for a given $k$ for network objects. By way of example, we will once again use the [Roman Roads network data](data/road_networks.RData) as the first network and the [Cibola ceramic similarity network](data/Cibola_adj.csv) as the second (click on the links for each to download and follow along). To use the `orca` package, we need to convert our igraph network objects into edge list with all each column defined as integer data. We do this using the `apply` function with an `igraph::as_edgelist` call nested within it. After making this conversion we use the `orca::count4` and `orca::count5` functions to define counts of orbits for the $k=4$ and $k=5$ node configurations respectively. We then view the first few rows of each. These output data frames have a row for each node and the columns represent the counts of orbits of each configuration using the same numbering scheme shown in the figure above. + + +```r +library(orca) +``` + +``` +## Warning: package 'orca' was built under R version 4.2.3 +``` + +```r +# Read in data +load("data/road_networks.RData") +cibola <- + read.csv(file = "data/Cibola_adj.csv", + header = TRUE, + row.names = 1) +cibola_net <- igraph::graph_from_adjacency_matrix(as.matrix(cibola), + mode = "undirected") +``` + +``` +## Warning: The `adjmatrix` argument of `graph_from_adjacency_matrix()` must be symmetric +## with mode = "undirected" as of igraph 1.6.0. +## ℹ Use mode = "max" to achieve the original behavior. +## This warning is displayed once every 8 hours. +## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was +## generated. +``` + +```r +# Calculate orbits for k = 4 and 5 for the Roman road network +road_net_int <- + t(apply(igraph::as_edgelist(road_net, names = FALSE), 1, as.integer)) +``` + +``` +## This graph was created by an old(er) igraph version. +## Call upgrade_graph() on it to use with the current igraph version +## For now we convert it on the fly... +``` + +```r +road_orb4 <- orca::count4(road_net_int) +road_orb5 <- orca::count5(road_net_int) + +head(road_orb4) +``` + +``` +## O0 O1 O2 O3 O4 O5 O6 O7 O8 O9 O10 O11 O12 O13 O14 +## [1,] 4 6 6 0 10 18 1 4 0 1 0 0 0 0 0 +## [2,] 3 5 2 1 6 8 3 0 0 0 0 1 1 0 0 +## [3,] 3 9 3 0 11 18 8 1 0 2 0 0 0 0 0 +## [4,] 2 4 1 0 9 4 3 0 0 0 0 0 0 0 0 +## [5,] 2 4 1 0 7 4 3 0 0 0 0 0 0 0 0 +## [6,] 3 2 1 2 5 2 0 0 0 0 2 0 0 1 0 +``` + +```r +head(road_orb5) +``` + +``` +## O0 O1 O2 O3 O4 O5 O6 O7 O8 O9 O10 O11 O12 O13 O14 O15 O16 O17 O18 O19 O20 +## [1,] 4 6 6 0 10 18 1 4 0 1 0 0 0 0 0 9 24 10 5 6 3 +## [2,] 3 5 2 1 6 8 3 0 0 0 0 1 1 0 0 8 6 4 1 8 6 +## [3,] 3 9 3 0 11 18 8 1 0 2 0 0 0 0 0 6 16 23 0 22 16 +## [4,] 2 4 1 0 9 4 3 0 0 0 0 0 0 0 0 13 7 2 6 10 3 +## [5,] 2 4 1 0 7 4 3 0 0 0 0 0 0 0 0 13 7 3 1 10 3 +## [6,] 3 2 1 2 5 2 0 0 0 0 2 0 0 1 0 9 3 0 4 0 0 +## O21 O22 O23 O24 O25 O26 O27 O28 O29 O30 O31 O32 O33 O34 O35 O36 O37 O38 +## [1,] 18 0 1 0 0 0 2 3 0 0 0 0 0 3 0 0 0 0 +## [2,] 0 1 0 0 0 0 0 0 0 3 0 0 0 2 0 0 0 0 +## [3,] 9 3 0 1 0 0 4 4 0 0 2 0 0 3 0 0 0 0 +## [4,] 0 1 0 0 0 0 2 0 0 0 0 0 0 1 0 0 0 0 +## [5,] 0 1 0 0 0 0 2 0 0 0 0 0 0 0 0 0 0 0 +## [6,] 0 0 0 0 0 0 0 0 5 0 0 0 0 1 0 0 0 0 +## O39 O40 O41 O42 O43 O44 O45 O46 O47 O48 O49 O50 O51 O52 O53 O54 O55 O56 +## [1,] 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 +## [2,] 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 +## [3,] 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 +## [4,] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +## [5,] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +## [6,] 0 0 0 0 0 0 0 0 0 2 0 0 0 0 0 0 0 0 +## O57 O58 O59 O60 O61 O62 O63 O64 O65 O66 O67 O68 O69 O70 O71 O72 +## [1,] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +## [2,] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +## [3,] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +## [4,] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +## [5,] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +## [6,] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +``` + +```r +# Calculate orbits for k = 4 and 5 for the Cibola network +cibola_net_int <- + t(apply(igraph::as_edgelist(cibola_net, names = FALSE), 1, as.integer)) + +cibola_orb4 <- orca::count4(cibola_net_int) +cibola_orb5 <- orca::count5(cibola_net_int) + +head(cibola_orb4) +``` + +``` +## O0 O1 O2 O3 O4 O5 O6 O7 O8 O9 O10 O11 O12 O13 O14 +## [1,] 11 17 7 48 105 11 0 0 2 11 75 13 40 37 115 +## [2,] 8 38 0 28 120 0 23 0 0 54 58 0 104 0 56 +## [3,] 1 13 0 0 28 0 17 0 0 61 0 0 0 0 0 +## [4,] 11 34 9 46 84 76 41 0 9 46 124 19 61 43 103 +## [5,] 13 18 13 65 65 60 15 3 1 32 124 24 15 86 173 +## [6,] 11 24 7 48 106 40 16 0 5 17 94 12 48 39 114 +``` + +```r +head(cibola_orb5) +``` + +``` +## O0 O1 O2 O3 O4 O5 O6 O7 O8 O9 O10 O11 O12 O13 O14 O15 O16 O17 O18 O19 O20 +## [1,] 11 17 7 48 105 11 0 0 2 11 75 13 40 37 115 496 69 0 122 0 0 +## [2,] 8 38 0 28 120 0 23 0 0 54 58 0 104 0 56 365 0 0 125 179 0 +## [3,] 1 13 0 0 28 0 17 0 0 61 0 0 0 0 0 73 0 0 24 115 0 +## [4,] 11 34 9 46 84 76 41 0 9 46 124 19 61 43 103 36 334 42 45 214 108 +## [5,] 13 18 13 65 65 60 15 3 1 32 124 24 15 86 173 158 185 1 48 61 63 +## [6,] 11 24 7 48 106 40 16 0 5 17 94 12 48 39 114 311 201 24 97 96 33 +## O21 O22 O23 O24 O25 O26 O27 O28 O29 O30 O31 O32 O33 O34 O35 O36 O37 O38 +## [1,] 0 0 0 66 0 14 261 6 372 1 0 0 0 0 0 14 0 0 +## [2,] 0 3 0 164 15 0 233 0 262 0 22 18 0 0 11 0 0 0 +## [3,] 0 4 0 125 0 0 42 0 0 0 45 0 0 0 2 0 0 0 +## [4,] 0 20 0 58 32 20 156 153 360 115 105 92 0 0 12 1 59 0 +## [5,] 28 4 0 60 11 47 111 155 449 74 31 81 2 2 5 0 8 2 +## [6,] 0 3 0 82 16 10 317 54 369 58 14 47 0 0 4 17 15 0 +## O39 O40 O41 O42 O43 O44 O45 O46 O47 O48 O49 O50 O51 O52 O53 O54 O55 O56 +## [1,] 0 0 26 0 31 1 30 261 9 28 0 0 2 10 4 0 0 2 +## [2,] 29 83 0 0 16 0 65 235 0 0 0 0 0 2 0 30 0 30 +## [3,] 85 0 0 0 0 0 46 0 0 0 0 0 0 0 0 0 0 152 +## [4,] 23 119 15 0 187 3 159 39 26 319 0 0 11 0 13 16 0 55 +## [5,] 38 28 30 12 193 2 86 50 9 344 0 0 1 4 3 0 14 47 +## [6,] 11 41 6 0 46 0 63 203 8 156 0 0 11 0 13 12 0 11 +## O57 O58 O59 O60 O61 O62 O63 O64 O65 O66 O67 O68 O69 O70 O71 O72 +## [1,] 117 10 37 33 6 1 0 5 16 150 58 9 0 50 84 171 +## [2,] 69 0 104 0 0 0 0 0 129 105 0 0 0 173 0 70 +## [3,] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +## [4,] 221 14 55 50 9 2 12 6 25 115 80 38 2 90 79 143 +## [5,] 355 10 33 40 21 0 0 4 1 76 132 2 4 12 242 276 +## [6,] 154 10 39 49 5 2 2 8 8 141 56 20 4 64 87 168 +``` + +#### Relative Graphlet Frequency Distribution{-} + +Once these orbits have been enumerated for a pair of networks, there are many options for comparing those networks. The simplest method is to simply calculate the absolute difference in the frequency of graphlets normalized for the total number of graphlets in each network. This is called the Relative Graphlet Frequency Distribution (RGFD). Before we can do this, however, we have to take one intermediate step. The `orca::count4` and `orca::count5` functions calculate orbit counts, but not graphlet counts. We have made a simple function here that assigns graphlet counts using the same schema shown in the figure above. + + +```r +orbit_to_graphlet <- function(x) { + out <- matrix(NA, nrow(x), 9) + out[, 1] <- x[, 1] + out[, 2] <- rowSums(x[, 2:3]) + out[, 3] <- x[, 4] + out[, 4] <- rowSums(x[, 5:6]) + out[, 5] <- rowSums(x[, 7:8]) + out[, 6] <- x[, 9] + out[, 7] <- rowSums(x[, 10:12]) + out[, 8] <- rowSums(x[, 13:14]) + out[, 9] <- x[, 15] + colnames(out) <- c("G0", "G1", "G2", "G3", "G4", "G5", "G6", "G7", "G8") + return(out) +} + +road_graphlet <- orbit_to_graphlet(road_orb4) +head(road_graphlet) +``` + +``` +## G0 G1 G2 G3 G4 G5 G6 G7 G8 +## [1,] 4 12 0 28 5 0 1 0 0 +## [2,] 3 7 1 14 3 0 1 1 0 +## [3,] 3 12 0 29 9 0 2 0 0 +## [4,] 2 5 0 13 3 0 0 0 0 +## [5,] 2 5 0 11 3 0 0 0 0 +## [6,] 3 3 2 7 0 0 2 1 0 +``` + +```r +cibola_graphlet <- orbit_to_graphlet(cibola_orb4) +head(cibola_graphlet) +``` + +``` +## G0 G1 G2 G3 G4 G5 G6 G7 G8 +## [1,] 11 24 48 116 0 2 99 77 115 +## [2,] 8 38 28 120 23 0 112 104 56 +## [3,] 1 13 0 28 17 0 61 0 0 +## [4,] 11 43 46 160 41 9 189 104 103 +## [5,] 13 31 65 125 18 1 180 101 173 +## [6,] 11 31 48 146 16 5 123 87 114 +``` + +With these in place we can then compare the two networks using a rescaled version of the relative graph frequency distribution defined here as: + +$$d(G_1,G_2) = \Sigma_{i=1} | F_i(G_1) - F_i(G_2)|$$ + +where + +* $F_i(G_1)$ is the count of graphlet $i$ in network 1 divided by the total number of graphlets in network 1 +* $F_i(G_2)$ is the count of graphlet $i$ in network 2 divided by the total number of graphlets in network 2 + +We can easily calculate this measure by dividing the column sums of graphlet counts for each network by the total number of graphlets in that network. Next we then find the sum of the absolute differences between the results. Note that in the version here we are using the 15 $k=4$ graphlets but we could also use the 73 $k=5$ graphlets or some other number (though larger numbers are computationally difficult to calculate and don't typically provide better results). Because this measure is a comparisons of two vectors of proportions that sum to 1, possible values range from 0 where two graphlet distributions are identical to 2 when two graphlet distributions are the inverse of one another. We have rolled this into a function here for ease of use. This function expects matrices of graphlet counts: + + +```r +rgfd <- function(graphlet1, graphlet2) { + graphlet1_sum <- colSums(graphlet1) / sum(graphlet1) + graphlet2_sum <- colSums(graphlet2) / sum(graphlet2) + return(sum(abs(graphlet1_sum - graphlet2_sum))) +} + + +rgfd(road_graphlet, cibola_graphlet) +``` + +``` +## [1] 1.09591 +``` + +Our example here produced a distance of `1.0959` suggesting these networks share about half of their graphlet distributions in common. + +Importantly, it is also possible to run the function defined above for orbit counts directly instead of just graphlet counts for either $k=4$ or $k=5$. Note that when we run the function for $k=4$ below for this example the results are identical to the graphlet counts above but this is by no means guaranteed. + + +```r +rgfd(road_orb4, cibola_orb4) +``` + +``` +## [1] 1.09591 +``` + +```r +rgfd(road_orb5, cibola_orb5) +``` + +``` +## [1] 1.273612 +``` + +Our results here suggest that there is a a somewhat greater distance in the distribution of orbits for $k=5$ than for $k=4$. + + +#### Graphlet (Orbit) Distribution Agreement {-} + +Another graphlet-based method for comparing networks is based on something called graphlet distribution agreement. This measure actually works on the orbits underlying graphlets so you may also sometimes see it referred to as the orbit distribution agreement measure. This measure is based on the distribution of degree for each of the 73 possible orbits for the graphlet $k=5$ case. It is calculated as follows for each possible orbit $j$ (indexed 0 through 72 as in the figure above): + +$$NG^j(k) = \frac{d G^j(k)/k}{T_{G^i}} $$ +where + +* $d G^j(k) / k$ is the number of nodes in network $G$ touching $k$ times for orbit $j$ divided by $k$ +* $T_{G^i}$ is the normalizing factor which is the sum of all values of the numerator for all values $j$ + +With this set of values in place for each orbit 0 through 72 for 2 networks we can then calculate agreement for each value of $j$ for two networks using the following equation: + +$$A^j(G_1,G_2) = 1 - \sqrt{\frac{1}{2} \Sigma (N^j_{G_1}(k) - N^j_{G_2}(k))^2}$$ + +The final value for agreement is the geometric or arithmetic mean of all 73 values for $A^j(G_1,G_2)$. The value will range between 0 and 1 with larger numbers indicating greater agreement in the distribution of orbits. + +
+

Luckily for us, someone has already gone to the trouble of coding +this method in R. GitHub User QiliShi has created a +package called NetworkSim which calculates the metric +described above. This package is not in the CRAN archive, however, so it +needs to be installed directly from GitHub using the +devtools::install_github function. If you have not already +installed this package, run the next line of code.

+
+ + +```r +devtools::install_github("QiliShi/NetworkSim") +``` + +The specific function within the `NetworkSim` package is called `netODA` for orbit distribution agreement. It expects either networks in igraph format or integer edge list objects to run. Let's give it a try using the Roman Road and Cibola ceramic similarity data we imported above using an arithmetic means: + + + + + + + + + + + + + + + diff --git a/DESCRIPTION b/DESCRIPTION index f3a7029..a9aa3c0 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -55,8 +55,6 @@ Suggests: networkD3, visNetwork, GISTools, - rgeos, - maptools, networkDynamic, scatterplot3d, patchwork, diff --git a/_book/reference-keys.txt b/_book/reference-keys.txt deleted file mode 100644 index 80bcc26..0000000 --- a/_book/reference-keys.txt +++ /dev/null @@ -1,188 +0,0 @@ -GettingStarted -InstallR -Windows -MacOS -Linux -InstallRStudio -RunRStudio -RBasics -Org -Math -Variables -Logical -Vectors -Functions -Tabular -DataTypes -ObjectTypes -Vec -Mat -DF -List -WorkspaceTab -Directory -FirstScript -InstallPackages -WorkingWithFiles -Plotting -Warnings -AdvancedR -Conditionals -Loops -CustomFunctions -TestYourSkills -DataAndWorkspace -DataSets -Everything -RomanRoad -SWSN -Cibola -Himalaya -ArchPubs -Guadalquivir -Importing -PrimaryPackages -ShouldIInstall -Environment -WorkspaceSetup -NetworkData -NetworkDataFormats -NetworkDataFunctions -Edgelist -AdjacencyList -AdjacencyMatrix -IncidenceMatrix -NodeAttributes -TypesOfNetworks -SimpleNetworks -DirectedNetworks -WeightedNetworks -TwoMode -SimilarityNetworks -Morisita -EgoNetworks -Multinet -ConvertingNetworkFormats -Exploratory -ExampleNetworkObjects -CalcMetric -Centrality -Degree -Betweenness -Eigenvector -PageRank -Closeness -HubsAndAuthorities -TriadsAndClustering -Triads -Transitivity -WalksPathsDistance -Distance -ShortestPaths -Diameter -ComponentsAndBridges -IdentifyingComponents -Cutpoints -Bridges -CliquesAndCommunities -Cliques -KCores -ClusterDetection -GirvanNewman -Walktrap -Louvain -Modularity -FindingEdgesBetween -ExploratoryRomanRoads -Uncertainty -UncertaintyScripts -UncertaintyGeneral -NodesAtRandom -EdgesAtRandom -IndNodesAtRandom -MissingBiased -SimIncidence -EdgeProbability -EdgeProbSim -SampleSize -Visualization -VizDatasets -ViZInR -networkpackage -igraphpackage -ggraphpackage -NetVizOptions -GraphLayouts -ManualLayouts -GeographicLayouts -AlgorithmicLayouts -NodeEdgeOptions -NodeOptions -EdgeOptions -LabelOptions -Colorblind -VizCommunities -ReplicatingBookFigures -SpatialNetworks -GeoData -ExampleData -PlanarTrees -EvaluatingPlanarity -DefiningTrees -SpatialNetworkModels -RelativeNeighborhoods -GabrialGraphs -BetaSkeletons -MinSpanningTrees -DelaunayTri -KNN -MaxDist -SpaceCaseStudies -IronAgeSpain -SpaceSW -ERGM -ERGMsInR -CranborneChase -NetProperties -fitting-models-with-ergm -ModelTheory -GOF -Diagnostics -SimERGMs -ERGMterms -Degeneracy -SpatialInteraction -GravityModel -ParameterizeGravity -RihllWilson -ParameterizingRetail -TruncatedPower -RadiationModels -OtherModels -Affiliation -AnalyzingTwoMode -TraditionalMetrics -TwoModeMetrics -ProjectingTwoMode -CorrespondenceAnalysis -CAViz -MeasuringCoassociation -COViz -NetworkDiffusion -DiffusionProcesses -SimNetworkR -DiffuseSimulatedNetworks -DiffuseEmpiricalNetworks -EvaluatingDiffusion -ComparingNetworks -KnownNode -CompareAdjacencyMatrices -QAP -DeltaCon -UnknownNode -ComparingStatistics -NetworkKernel -SpectralMethods -PortraitDivergence -GraphletMethods -Alignment diff --git a/_output.yml b/_output.yml index 4522d2c..bbeb4be 100644 --- a/_output.yml +++ b/_output.yml @@ -4,16 +4,6 @@ bookdown::bs4_book: theme: primary: "#1e90ff" info: "#0dcaf0" - base_font: - google: - family: Lato - heading_font: - google: - family: News Cycle - wght: 700 - code_font: - google: - family: Source Code Pro repo: base: https://github.com/mpeeples2008/ArchNetSci branch: main diff --git a/index.Rmd b/index.Rmd index 336ed5b..ae3018f 100644 --- a/index.Rmd +++ b/index.Rmd @@ -657,6 +657,7 @@ colnames(mat4) <- c("A", "B", "C") #assign col names mat4 # view matrix # Export the matrix as a csv file +dir.create("data", showWarnings = FALSE) write.csv(mat4, file = "data/output_mat.csv") ``` diff --git a/index.md b/index.md new file mode 100644 index 0000000..ce870c3 --- /dev/null +++ b/index.md @@ -0,0 +1,1641 @@ +--- +title: "Online Companion to *Network Science in Archaeology*" +author: "Matthew A. Peeples and Tom Brughmans" +date: "2026-04-21" +site: bookdown::bookdown_site +output: bookdown::bs4_book +documentclass: book +bibliography: + - assets/references.bib + - assets/packages.bib +csl: assets/society-for-american-archaeology.csl +url: https://book.archnetworks.net +description: | + This online bookdown document accompanies the Cambridge Manuals in Archaeology book + *Network Science in Archaeology* by Tom Brughmans and Matthew A. Peeples. +link-citations: yes +colorlinks: yes +github-repo: "mpeeples2008/ArchNetSci" +cover-image: images/cover.png +--- + +# Welcome{- #Welcome} + +![](images/image_break.png){width=100%} + + + + +This project serves as a companion to the Cambridge Manuals in Archaeology book *Network Science in Archaeology* by Tom Brughmans and Matthew A. Peeples (2023). + +Brughmans and Peeples Book Cover + +This document contains a series of tutorials that outline methods for managing, analyzing, and visualizing network data, primarily using the R programming language. We provide code and examples to replicate the analyses presented in the book as well as many other useful tools. The bulk of this Online Companion is designed to be used with the book in hand to expand on topics covered in the published version. Part I of this document (see the Table of Contents) is focused on helping you get started in R and R-Studio. Part II which includes sections 2 through 7 corresponds to the topics and information covered in Chapters 2 through 7 of the Brughmans and Peeples book. Part III (Going Beyond the Book) includes tutorials beyond the scope of the published book including "advanced" topics that require additional detailed description and knowledge of the methods presented in Parts I and II. We plan to continue to add to and expand Part III in the future. You can use the table of contents on the left-hand side of your screen to jump directly to a particular section and the table and contents on the right to navigate within each section. We have also created a [quick TOC reference](#TableOfContents) if you are seeking something in particular. + +For more information on the book and the authors check out the project website here: [archnetworks.net](https://archnetworks.net). + +**Cite this document as:** + +> Peeples, Matthew A. and Tom Brughmans (2023). *Online Companion to Network Science in Archaeology*. , Accessed 2026-04-21. + +**The associated book can be cited as** + +> Brughmans, Tom and Matthew A. Peeples (2023). *Network Science in Archaeology.* Cambridge Manuals in Archaeology. Cambridge University Press, Cambridge, UK. + + +## How Should I Use This Online Companion?{- #HowTo} + +The tutorials here are designed to complement the text of the associated book (Brughmans and Peeples 2023) but can also stand alone as a guide to implementation of network analyses in R if you have a basic background in network methods and terminology. Although each section of this guide builds upon the previous sections in terms of network concepts and R methods, the sections are each independent in terms of data, examples, and code and can be run out of order if you choose. + +A few suggestions on where to start: + +* If you are new to network analysis and R, we would suggest going through each section of this document, starting with Part I: Getting Started with R and then going through Part II in order as you following along with the corresponding chapters in the book. +* If you are already familiar with R but new to network analysis, you can start with Section 2 in Part II to set up your data and work space, and then follow along with the remaining numbered sections and associated book chapters as you read. +* If you are already a confident network analyst and R user and are just looking for code chunks to implement something in particular, feel free to skip around. We have tried to make each section as independent as possible so that you can pick and choose what you want to work on. Use the [Table of Contents](#TableOfContents) to find topics quickly. +* If you are a real pro and are designing your own network analyses or visualizations, we would love it if you contributed to the project to help this document grow. + +Throughout this document we use a few icons to call-out special information or concerns. Keep an eye out for the symbols below: + +
+

We use this icon to highlight our discussions of R packages, Python +packages, and other software used in this project. Check here for brief +overviews and instructions on how to use and configure these +packages.

+
+ +
+

We use this icon to highlight particular areas of concern in our +discussion of network methods and R code. In particular, we use this +icon to warn you of common errors or pitfalls for particular functions +or network methods.

+
+ +
+

We use this icon to highlight helpful tips for the use of particular +network methods or tools.

+
+ + +## Reproducibility{- #Repro} + +The most recent version of this document was built with R version 4.2.2 (2022-10-31 ucrt). We suggest you use a recent version of R when attempting to use the code in this document (version 4.2 recommended). + +![Github last-commit](https://img.shields.io/github/last-commit/mpeeples2008/ArchNetSci)    +[![Docker Image CI](https://github.com/mpeeples2008/ArchNetSci/actions/workflows/docker-image.yml/badge.svg)](https://github.com/mpeeples2008/ArchNetSci/actions/workflows/docker-image.yml) + +The content of this document is designed to be as accessible and reproducible as possible. The source code used to produce this document along with all of the data used in analyses are available on [GitHub](https://github.com/mpeeples2008/ArchNetSci). This GitHub repository allows users to open issues, contribute to the document, or help fix typos or other errors (see information about [contributing](#Contributing) below). We have also opened a GitHub discussion board with this repository where users can ask questions about any data or code in the repository without making edits or issue requests directly. + +The easiest way to reproduce this document is to launch the project directly in your browser using [Binder](https://mybinder.org/). When you click on the link below it will open a browser based instance of R studio with all of the required packages and files. From there you can test and evaluate the code directly. + +[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/mpeeples2008/ArchNetSci/main) + +When you open Binder you will see a window with the Binder logo and a spinning progress wheel. It will typically only take a minute or so to get up and running and then you will see the screen below. Click on the "R-Studio" link under "Notebook" and it will open a new window with an R-Studio instance that you can use just like you would on your own computer. If you click on the Binder link and it is taking a long time, click to "show" the build logs. If you are "lucky" enough to be the first to initialize Binder after a new build of the GitHub project it will take quite a bit longer to get started. Grab a coffee, tea, Dr. Pepper, or other beverage of choice as it may be approximately 30+ minutes before R-Studio loads. + +![Binder Notebook Page](images/binder.jpg){width=100%} + +You can also install this repository as an R package directly from GitHub using the following code: + + +```r +if (!require("devtools")) install.packages("devtools") +devtools::install_github("mpeeples2008/ArchNetSci") +``` + +This package installation includes all of the dependencies required to run the code in this document and will create folders called "data" and "scripts" in the package installation directory with all of the required files to replicate the analyses in this document. + +Finally, you can run the code and generate documents locally using R and R Studio by downloading the entire R repository here: [main.zip](https://github.com/mpeeples2008/ArchNetSci/archive/refs/heads/main.zip). Unzip the files and then: + +* Open the "ArchNetSci.Rproj" file in R studio. +* Use the `renv::restore()` command at the console to install the required packages and dependencies. Note that this is a large document that uses many packages so this may take some time. +* You will then be able to browse the files and execute all of the code in the repository on your own computer. + +This online bookdown document has been deployed using the Netlify platform and the badge below shows the current status of the build hosted at [https://book.archnetworks.net](book.archnetworks.net). + +[![Netlify Status](https://api.netlify.com/api/v1/badges/266d5736-f13a-4de4-b812-141c023f3a09/deploy-status)](https://app.netlify.com/sites/archnetworks/deploys) + +## Computational Archaeology Discord Community{- #Discord} + +We have created an [Archaeological Network Science Channel on the Computational Archaeology Discord Server](https://discord.gg/Z9UXwjASM5), which we hope will provide an additional venue for archaeological network practitioners to collaborate, interact, and ask for help with this document or with archaeological networks (and other computational methods) in general. We invite you to use this as a place to ask questions of the authors, to the community at large, or just to chat to other like-minded researchers. Note that this Discord is subject to the same [code of conduct](https://github.com/mpeeples2008/ArchNetSci/blob/main/CODE_OF_CONDUCT.md) we use for the GitHub repository and you must abide by that agreement to participate. We require that you have a Discord account with a verified email address. + +Discord Logo + +[Join the Computational Archaeology Discord](https://discord.gg/Z9UXwjASM5)       ![](https://discordapp.com/api/guilds/975267909012189184/widget.png?style=shield) + +## New to R and R Studio?{- #NewToR} + +The network tutorials in this document are built for users with a basic familiarity with R and R-studio but if you're just getting started, don't worry. We have created a detailed guide to [Getting started with R](#GettingStarted). This document covers the installation of the required software and provides a basic introduction to the R programming environment that we hope will be enough to get you started. + +If you already have a basic familiarity with R and want to go further, there are numerous additional resources (most are completely free) to help you learn. Some resources we would recommend include *R for Data Science* [(Wickham and Grolemund 2017)](https://r4ds.had.co.nz/), *Advanced R* [(Wickham 2019)](https://adv-r.hadley.nz/), *the R Cookbook, 2nd edition* [(Long and Teetor 2019)](https://rc2e.com/somebasics), and *R in Action* and the associated *Quick-R* website [(Kabacoff 2015)](https://www.statmethods.net/). In addition to this [Ben Marwick](https://anthropology.washington.edu/people/ben-marwick) has created an excellent repository of [resources for using R in archaeology](https://github.com/benmarwick/ctv-archaeology) as well as an ever-growing list of archaeological publications that include R code. The website associated with this book [(archnetworks.net)](https://www.archnetworks.net) includes a list of archaeological articles focused on network research that include data and code. Reproducing published results is, in our experience, one of the best ways to learn advanced techniques and data management in R so we suggest you give it a try. + +## Contribute To the Project{- #Contributing} + +We welcome contributions to this project from the community and the GitHub platform helps us facilitate that. You will first need to [sign up for a GitHub account](https://github.com/) and log in. If you find something that needs updating or changing (typos or errors) you can simply click the "Edit source" link at the right sidebar on the relevant page and then click the edit icon found near the top of the code block and make your proposed changes. These changes will be saved in a new "fork" of the document and we will review these and implement them where relevant and happily add your name to our list of contributors. Note that we generally use the [tidyverse style guide](https://style.tidyverse.org/) for formatting code and comments. + +If you detect a larger error such as code not running or if you would like to request a new feature or update, you can create an issue using the [issue tracker](https://github.com/mpeeples2008/ArchNetSci/issues) page associated with the project repository. + +All contributors must agree to adhere to our [code of conduct](https://github.com/mpeeples2008/ArchNetSci/blob/main/CODE_OF_CONDUCT.md). + +## Help Build the Community{- #Community} + +We are devoted to seeing the community of archaeological network practitioners grow and we hope our book and these online resources will help to make this happen. You can support the growth of our community too! + +* Spread the word to your friends and colleagues +* Share links to these online resources on social media using the [#archnetworks](https://twitter.com/search?q=%23archnetworks&src=typed_query) hashtag +* Please cite the book *and* the Online Companion if you use methods or code from this document. [Citation Info](https://github.com/mpeeples2008/ArchNetSci/blob/main/CITATION.bib) +* Star the [GitHub project repository](https://github.com/mpeeples2008/ArchNetSci) and contribute to the project +* [Join the Computational Archaeology Discord](https://discord.gg/Z9UXwjASM5) and invite other interested people +* Share articles, teaching resources, data, or other archaeological network materials for posting on our associated website [(archnetworks.net)](https://archnetworks.net) + +## Project License{-} + +[![](https://i.creativecommons.org/l/by-nc-nd/4.0/88x31.png)](http://creativecommons.org/licenses/by-nc-nd/4.0/) + +This Online Companion to Archaeological Network Science is licensed under a [Creative Commons Attribution-NonCommercial-NoDerivitives 4.0 International License](http://creativecommons.org/licenses/by-nc-nd/4.0/). + +## Acknowledgements{- #Acknowledgements} + +This online bookdown project and the associated book were made possible thanks to the support of several generous funding sources including: The Carlsberg Foundation, in the context of the Past Social Networks Project (CF21-0382); the National Science Foundation through both the Archaeology and the Measurement, Methodology, and Statistics programs (grant #1758690 and #1758606); and the School of Human Evolution and Social Change at Arizona State University. Thank you to Jens Emil Bødstrup Christoffersen for providing detailed comments on and for testing the initial version of this online bookdown document. Any errors that remain are our own. + +![](images/Carlsbergfondet.png){width=300px}     ![](images/NSF_Logo.png){width=150px}     ![](images/ASU_SHESC_Logo.png){width=300px} + +# (PART) **PART I: Getting Started** {-} + +# Getting Started with R{#GettingStarted} + +![](images/image_break.png){width=100%} + +In order to follow along with the code and examples in this document, you will need recent installations of both R and R-Studio on your computer. R and R-studio are available for Windows, MacOS, and Linux. This section provides a very brief overview of how to get up and running. Following this, we introduce the basics of R and R-Studio to get you ready for the tutorials in the remainder of this document. If you follow this tutorial here we are confident you will be able to engage with the examples and code in this Online Companion. + +## Download and Install R{#InstallR} + +The first step is to install a recent version of R (we recommend 4.2 or later as this document was originally created in version 4.2). Follow the instructions below for the appropriate operating system. + +* The first step is to go to the R project website [www.r-project.org](https://www.r-project.org) and click on the [CRAN](https://cran.r-project.org/mirrors.html) link under "Downloads" on the left hand side. +* Choose a mirror for your download selecting one in your country or the "Cloud" option. +* Next, click on Windows, MacOS, or your Linux distribution and follow the instructions below. + +### Windows{ #Windows} + +* Click the "base" sub-directory on the left hand side of the screen and then click "Download R-4.2.0 for Windows" (the version number should be 4.2 or later) to download the most recent version as an executable. +* Once your download is complete, run the *.exe file and answer questions as prompted to complete the installation. + +### MacOS{ #MacOS} + +To install R on MacOS, you first need to know which chip manufacturer your Mac has. In order to determine which chip you have go to the Apple menu and select "About This Mac" and look for information under "Processor" or "Chip" in the window that pops up. It will either be Intel or M1. + +* Next, click the on the link under "Latest release" for the *.pkg file for the appropriate Mac processor in your computer. There is a separate notarized and signed .pkg file Macs with Intel processors and Macs with Apple M1 processors (mostly produced 2020 and later). Note, these .pkg files are not interchangeable so confirm which one you need before attempting to install. +* Once you have downloaded the appropriate .pkg, run it and answer the questions during the install as required. + +### Linux{ #Linux} + +Linux installations of R are primarily done through the console but the instructions are slightly different depending on which distribution you are using. + +* Click on the link for the appropriate Linux distribution and then follow the detailed instructions provided. +* The "R-core" or "R-base" builds are the ones you want to choose. +* Follow the instructions for your build and install any recommended dependencies. + +## Download and Install R-Studio{ #InstallRStudio} + +R-studio is an integrated development environment (IDE) for R, Python, and related programming tools that provides additional features for running and debugging code and data management. We see this IDE as essential to working with large and complex R projects. + +In order to install R-Studio: + +* Go to the R-Studio website [www.rstudio.com](https://www.rstudio.com/) and click "Download" at the top of the screen. +* Select the "RStudio Desktop" option. +* Download and run the latest "installer" file for the appropriate operating system. +* Run the downloaded file and answer questions at prompts as appropriate. R-Studio should automatically detect your installation of R. + +## Run R-Studio{ #RunRStudio} + +Once you've installed both R and R-Studio, open R-studio and look for the Console window (it will typically be the left hand side of the screen). That will tell you the version of R that is associated with the installation of R-Studio. If all goes well, it should be the recent version of R you just installed. + +![R Version Installed](images/r-version.jpg){width=100%} + +## R and R-Studio Basics{ #RBasics} + +R is a powerful statistical analysis platform that can be used to conduct some quite complex analyses. The learning curve is a bit steep when first getting started but the payoff is HUGE because the ecosystem of existing R scripts and packages is so large and diverse. We cannot hope to cover everything R and R-Studio can do in this very short intro here. Our tutorial here is a version of the "Introduction to R programming" that Peeples has used in the first week of his Quantitative and Formal Methods in Archaeology class for a number of years. Hopefully this will get you started. + +Although R seems complicated at first, many quite complex statistical analyses are run with just a few lines of code. Once you learn the basics, more complex features of R are really just combinations of these basic procedures. You won't become an R expert overnight, but we've seen many students pick up the basics quite quickly and begin to take on their first independent analyses in R in a matter of hours. + +### Organization of R-Studio{ #Org} + +First off, let's take a look at the R-Studio setup. When you first open R-Studio for the first time, you will see a screen divided into 3 panes. Before getting started click on "File" at the top of the screen and go to "New File > R Script" to open a 4th pane. You should see something like the screen below. + +
+

Note that the color of your screen may be different as I am using a +particular “dark mode” color setting that I find easier on my eyes. To +change your color scheme, go to the top of the R-Studio window and click +on “Tools > Global Options > Appearance” and then select a color +mode that works for you.

+
+ +![R-Studio](images/r-studio.jpg){width=100%} + +Organization of R-Studio Windows: + +* **Workspace** - The pane in the top left contains the Workspace tabs which is where you can write code and other documents prior to executing the code. +* **Console** - The pane at the bottom left is the console where you can type and run commands directly. When you execute code from the workspace, it will also appear here. +* **Environment/History** - The pane at the upper right includes tabs for Environment (a list of objects and functions currently initialized) and History (a list of previous commands run at the console). +* **Files/Plots/Packages** - The lower right pane has tabs for Files (which shows files in the current directory), Plots (where plots created in the console will be displayed), Packages (a list of additional packages installed and initialized in R), and Help (where you can get information about particular functions and packages). + +Note that the locations and visibility of these panes can be changed by going to "View > Panes" and selecting different options. In the set of tutorials that follow we are going to focus on the Console first and will introduce the other panels and what they provide along the way. + +### Mathematical Operations{ #Math} + +Getting started with R is as simple as typing directly into the Console. You can use the R console like a calculator to conduct mathematical operations. Simply type the numbers and operators at the console and hit enter to calculate. The answer will output directly on the console by default. Try typing the following at the console: + + +```r +3 + 3 +``` + +``` +## [1] 6 +``` + +```r +4 * 10 +``` + +``` +## [1] 40 +``` + +```r +50 / 5 +``` + +``` +## [1] 10 +``` + +R uses `( )` for bracketing groups of operations. These can be nested to do more complex mathematical operations or to determine the order of operations. For example compare the two equations below: + + +```r +((4 * 5 + 3) / 2) * 12 +``` + +``` +## [1] 138 +``` + +```r +(((4 * 5)) + 3 / 2) * 12 +``` + +``` +## [1] 258 +``` + +R uses typical mathematical operators including `+ - * /` for addition, subtraction, multiplication, and division and `^` to raise a number to an exponent. + + +```r +5^2 +``` + +``` +## [1] 25 +``` + +```r +5^(2 + 1) +``` + +``` +## [1] 125 +``` + +Anything placed after a `#` in a block of code will be treated as a comment and not evaluated: + + +```r +4 * 20 # comment here +``` + +``` +## [1] 80 +``` + +```r +3 * 4 # 4 + 4 will not be evaluated as it is after the # +``` + +``` +## [1] 12 +``` + +### Creating Variables/Objects{ #Variables} + +R can also assign numbers, characters, or more complex operations to variables (also known as objects in this context) which can then be used in mathematical operations. Typically, we assign values to a object using the `<-` assign command but `=` also works. For example: + + +```r +test_var <- 50 +test_var +``` + +``` +## [1] 50 +``` + +```r +test2 = 10 + test_var +test2 +``` + +``` +## [1] 60 +``` + +```r +char1 <- "hello world" +char1 +``` + +``` +## [1] "hello world" +``` + +Object names in R are case sensitive and cannot include spaces. Object names can include numbers and letters but must start with a letter. It is a good idea to use descriptive object names where the object will be used repeatably. + +When formatting object names there are a few common styles such as: + +* **`snake_case_style`** - see the little snakes (underscores) in the place of spaces +* **`CamalCaseStyle`** - see the capitalized humps denoting each word +* **`kebab-case-style`** - skewered right down the middle + +In general any of these styles is fine, but we suggest you try to remain consistent. Also, avoid using `.` to separate words as that is used by particular R functions and calls in other ways and can cause confusion. + +![Illustration by Allison Horst `@allison_horst`](images/case.jpg){width=100%} + +Many mathematical constants are built right into R so be sure not to overwrite any of these (or any other function) by giving an object the same name. + + +```r +pi +``` + +``` +## [1] 3.141593 +``` + +```r +LETTERS +``` + +``` +## [1] "A" "B" "C" "D" "E" "F" "G" "H" "I" "J" "K" "L" "M" "N" "O" "P" "Q" "R" "S" +## [20] "T" "U" "V" "W" "X" "Y" "Z" +``` + +```r +letters +``` + +``` +## [1] "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l" "m" "n" "o" "p" "q" "r" "s" +## [20] "t" "u" "v" "w" "x" "y" "z" +``` + +```r +month.name +``` + +``` +## [1] "January" "February" "March" "April" "May" "June" +## [7] "July" "August" "September" "October" "November" "December" +``` + + +### Logical Operators{ #Logical} + +R can also use logical operators (see list below). These operators can be used in conjunction with other operations and return a value indicating `TRUE` or `FALSE`. These can be used in more complex functions and conditional statements as we will see below. + + +|Operator |Meaning | +|:--------|:------------------------| +|< |less than | +|> |greater than | +|<= |less than or equal to | +|>= |greater than or equal to | +|== |exactly equal to | +|!= |not equal to | +|& |and | +|| |or | + + + +```r +v <- 50 +v > 20 +``` + +``` +## [1] TRUE +``` + +```r +v < 20 +``` + +``` +## [1] FALSE +``` + +```r +v * 2 == 100 +``` + +``` +## [1] TRUE +``` + +Logical operators can also include *and* statements with the `&` symbol and *or* statements with `|`. For example: + + +```r +v <- 40 +v > 20 & v < 30 # and statement +``` + +``` +## [1] FALSE +``` + +```r +v > 20 | v < 30 # or statement +``` + +``` +## [1] TRUE +``` + +### Vectors{ #Vectors} + +R can also assign a vector of numbers or characters to a variable and preform operations using that vector. For example in the following we use the `c()` (c for combine) command to create a vector and subject it to a mathematical or other operation. + + +```r +z <- c(2, 4, 6, 8, 10, 12) +z / 2 +``` + +``` +## [1] 1 2 3 4 5 6 +``` + +If you want to call a particular value or selection of values in a vector you can use the `[]` square brackets and indicate which item(s) you are interested in. + + +```r +z[3] # item 3 in object z +``` + +``` +## [1] 6 +``` + +```r +z[4:6] # items 4 through 6 in object z +``` + +``` +## [1] 8 10 12 +``` + +```r +z[c(3, 2, 1)] # items 3, 2, 1, in that order from object z +``` + +``` +## [1] 6 4 2 +``` + +We can also search vectors or other objects for specific values: + + +```r +vec_obj <- + c("Ohtani", + "Wheeler", + "Correa", + "Semien", + "Soto", + "Guerrero Jr.", + "Correa") + +vec_obj[vec_obj == "Correa"] +``` + +``` +## [1] "Correa" "Correa" +``` + +To see if a particular value is in a given object we can use the `%in%` operator and get a logical value in return. + + +```r +"Ohtani" %in% vec_obj +``` + +``` +## [1] TRUE +``` + +```r +"Judge" %in% vec_obj +``` + +``` +## [1] FALSE +``` + + +### Using Basic R Functions{ #Functions} + +R has a number of built-in functions that perform many common operations and statistical analyses. We have already used one of these above `c()` and it was so fast and easy you might have missed it. Functions are typically used by typing the name of the function followed by a set of parenthesis that contain all of the arguments that the function expects. For example: + + +```r +v <- c(5, 10, 15, 20, 25, 30, 2000) +max(v) +``` + +``` +## [1] 2000 +``` + +```r +min(v) +``` + +``` +## [1] 5 +``` + +```r +mean(v) +``` + +``` +## [1] 300.7143 +``` + +```r +median(v) +``` + +``` +## [1] 20 +``` + +```r +log(v, base = exp(1)) # argument setting the base +``` + +``` +## [1] 1.609438 2.302585 2.708050 2.995732 3.218876 3.401197 7.600902 +``` + +```r +log10(v) +``` + +``` +## [1] 0.698970 1.000000 1.176091 1.301030 1.397940 1.477121 3.301030 +``` + +```r +round(pi, digits = 2) # argument setting the number of digits to retain +``` + +``` +## [1] 3.14 +``` + +For a list of some of the most frequently used built-in functions see [this Quick-R](https://www.statmethods.net/management/functions.html) page. + +### Tabular Data{ #Tabular} + +R can be used to work with tabular data as well. Typically it is most convenient to read such data for a file for very large tabular data [(see working with files below)](#WorkingWithFiles), but we can also generate simple numeric tabular data directly in R using the `matrix()` function. In the following example we create a two-row, two-column matrix by converting a vector of numbers into a matrix by specifying the number of rows `nrow` and number of columns `ncol`. The assignments we make inside the `matrix()` function are called arguments. + + +```r +dat <- c(3, 4, 2, 20) +mat1 <- matrix(data = dat, nrow = 2, ncol = 2) +mat1 +``` + +``` +## [,1] [,2] +## [1,] 3 2 +## [2,] 4 20 +``` + +Note that the `matrix()` function reads the numbers in first by column and then by row. If we want want to change that we can first investigate the options for this function using the `help()` function. In order to see the documentation for a given function simply type `help("NameOfFunction")` at the console or `?NameOfFunction`. + + +```r +?matrix +``` + +![help for matrix function](images/help.jpg){width=100%} + +And let's zoom in to one piece in particular: + +![byrow argument](images/byrow.jpg){width=100%} + +As we can see in the help materials for matrix, there is an additional argument we did not use called `byrow` which is set to `FALSE` by default. Let's change that to `TRUE` and check the results. Note that you can use capital `F` and `T` in the place of `FALSE` and `TRUE` in functions but it is generally good form to write it out in context where you are sharing your code publicaly. Note also that our function call can span multiple rows and will automatically end when we close the parentheses. This multi-line formatting will be essential for making longer function calls readable. + +
+

R-Studio has a nice built-in command for formatting code, especially +very long lines of code, into multiple lines that are easier to read. +This command also creates proper spacing between operators and objects. +To give it a try, simply highlight a chunk of code in the workspace area +and hit “Ctrl+Shift+A” (or “Cmd+Shift+A” for a Mac) to reformat the code +within the selection. There are many other nifty R-Studio keyboard +shortcuts. Check +here for more info.

+
+ + + +```r +mat2 <- matrix( + data = dat, + nrow = 2, + ncol = 2, + byrow = TRUE +) +mat2 +``` + +``` +## [,1] [,2] +## [1,] 3 4 +## [2,] 2 20 +``` + +Just like we did with vectors, we can also use matrices for many mathematical and statistical functions that are built directly into R. For example, let's run a Fisher's Exact Test using the `fisher.test` function to assess the independence of rows and columns in this table. + + +```r +fisher.test(mat2) +``` + +``` +## +## Fisher's Exact Test for Count Data +## +## data: mat2 +## p-value = 0.07474 +## alternative hypothesis: true odds ratio is not equal to 1 +## 95 percent confidence interval: +## 0.5875228 107.8450263 +## sample estimates: +## odds ratio +## 6.815654 +``` + +The output includes information about the data we used to run the test, a p-value, the alternative hypothesis, confidence intervals, and the odds ratio. The output we get from any given function will vary depending on the application. See the `help()` documents for your function of interest to get more info about output. + +### Data Types in R{ #DataTypes} + +There are many different types of data that R understands but we focus here on the most common categories. This includes numeric data, integer data, character data, logical data, and factors. + +* **numeric data** - This is the designation used for real numbers which can include a decimal point. +* **integer data** - This is the designation for whole numbers without a decimal. To designate a number as an integer type, you can add `L` after the number (see example below). Note that R automatically converts between numeric and integer data as necessary in mathematical operations. +* **character data** - This is the designation for any string of characters that does not exclusively consist of numbers. Character data can be a single character such as `"a"` or a long string `"this string is character data"`. In general R displays character data inside `" "`. +* **logical data** - This is the designation for evaluations of logical statements and takes the form of `TRUE` or `FALSE`. +* **factors** - Factors are nominal variables stored as vectors as R objects which have distinct "levels" which each value must be. Factors are useful in both many statistical procedures and visualizations in that unique values can be treated as "groups" rather than simply unique character data. To designate data as a factor, use the `as.factor()` function. Note that factors can be numbers but they will be treated as nominal characters when evaluated. + +It is possible to determine what type of data an R object contains using the `str()` function. Let's look at examples for each type below: + + +```r +num <- c(12.3, 32.4, 53, 4.2, 4, 22.3) +str(num) +``` + +``` +## num [1:6] 12.3 32.4 53 4.2 4 22.3 +``` + +```r +int <- c(1L, 2L, 5L, 6L) +str(int) +``` + +``` +## int [1:4] 1 2 5 6 +``` + +```r +char <- c("string1", "string2", "This too is a string") +str(char) +``` + +``` +## chr [1:3] "string1" "string2" "This too is a string" +``` + +```r +tf <- c(TRUE, FALSE, FALSE, TRUE) # note the lack of " " +str(tf) +``` + +``` +## logi [1:4] TRUE FALSE FALSE TRUE +``` + +```r +fac <- as.factor(c("type1", "type2", "type2", "type3")) +str(fac) +``` + +``` +## Factor w/ 3 levels "type1","type2",..: 1 2 2 3 +``` + +### Object Types in R{ #ObjectTypes} + +The four most common object types in R are vectors, matrices, lists, and data frames. We have already explored vectors and matrices but we can define these and the other classes in more detail here. + +* **vector** - a combined set of values all of the same type (character, numeric, etc.). Note that if you mix numbers and character data, R will assume every entry represents character data. +* **matrix** - a set of values in a rectangular two-way table all of the same type (character, numeric, etc.) +* **data frame** - a set of values in a rectangular two-way table where different columns can be different data types +* **list** - a list is a collection of other R objects that can be vectors, matrices, data frames or others in any format that are combined into a single object. + +#### Vectors{ #Vec} + +We have already introduced vectors above but we can point out one more feature that is often useful in assessing vectors. The `length()` function tells you how many elements are in a vector. + + +```r +v <- c(1, 6, 4, 8, 7, 5, 3, 8, 10, 44) +length(v) +``` + +``` +## [1] 10 +``` + +#### Matrices{ #Mat} + +Once again, we have already introduced matrices above but there are a few more details that are worth addressing here. Again, if you want to call a specific value in a matrix you can use the `[ , ]` square brackets with the row number listed followed by a comma and the column number. For example: + + +```r +mat1 +``` + +``` +## [,1] [,2] +## [1,] 3 2 +## [2,] 4 20 +``` + +```r +mat1[2, 1] # row 2 column 1 +``` + +``` +## [1] 4 +``` + +If you want to know the size of a matrix, you can use the `dim()` dimensions function: + + +```r +dim(mat1) +``` + +``` +## [1] 2 2 +``` + + +#### Data Frame{ #DF} + +As the brief definitions above suggest, data frames are very similar to matrices but can include mixed data types in the same rectangular table. Each row and column must, however, have the same number of entries. A data frame can be created by combining a set of vectors. For example: + + +```r +col1 <- c("mammoth", "mastodon", "bison") +col2 <- c(50L, 52L, 14L) +col3 <- c(11.14, 22.23, 656.34) +col4 <- as.factor(c("type1", "type1", "type2")) +col5 <- c(TRUE, FALSE, TRUE) + +dat <- data.frame(col1, col2, col3, col4, col5) +dat +``` + +``` +## col1 col2 col3 col4 col5 +## 1 mammoth 50 11.14 type1 TRUE +## 2 mastodon 52 22.23 type1 FALSE +## 3 bison 14 656.34 type2 TRUE +``` + +If we want to look at what kind of data R understands each column to be, we can use the `str()` or structure function. + + +```r +str(dat) +``` + +``` +## 'data.frame': 3 obs. of 5 variables: +## $ col1: chr "mammoth" "mastodon" "bison" +## $ col2: int 50 52 14 +## $ col3: num 11.1 22.2 656.3 +## $ col4: Factor w/ 2 levels "type1","type2": 1 1 2 +## $ col5: logi TRUE FALSE TRUE +``` + +Note that the `dim()` function also works on data frames as does the `[,]` call for specific items: + + +```r +dat +``` + +``` +## col1 col2 col3 col4 col5 +## 1 mammoth 50 11.14 type1 TRUE +## 2 mastodon 52 22.23 type1 FALSE +## 3 bison 14 656.34 type2 TRUE +``` + +```r +dim(dat) +``` + +``` +## [1] 3 5 +``` + +```r +dat[2, 1] +``` + +``` +## [1] "mastodon" +``` + +#### Lists{ #List} + +A list is simply a convenient way of combining multiple objects into a single object. It doesn't matter what type of objects they are. Lists can be defined using the `list()` function. For example: + + + +```r +out1 <- list(mat1, dat, c(1, 2, 4)) # create a list containing 3 objects +out1 +``` + +``` +## [[1]] +## [,1] [,2] +## [1,] 3 2 +## [2,] 4 20 +## +## [[2]] +## col1 col2 col3 col4 col5 +## 1 mammoth 50 11.14 type1 TRUE +## 2 mastodon 52 22.23 type1 FALSE +## 3 bison 14 656.34 type2 TRUE +## +## [[3]] +## [1] 1 2 4 +``` + +If you want to call a specific element of the list you use double square brackets `[[]]` along with the numeric index in the middle: + + +```r +out1[[3]] +``` + +``` +## [1] 1 2 4 +``` + +You can even stack sets of double and single brackets to call specific items within list elements: + + +```r +out1[[3]][2] # item 2 in list object 3 +``` + +``` +## [1] 2 +``` + +```r +out1[[2]][2, 1] # row 2 column 1 in list object 2 +``` + +``` +## [1] "mastodon" +``` + + +## The Workspace Tab{ #WorkspaceTab} + +Now that we are starting to get into more complex calls and functions, it will be useful to write and edit the code before executing it rather than typing it directly into the Console. To do this, we can work in the Workspace tab R script document we created at the beginning of this tutorial (Go to File > New File > R Script to open a new document). These .R documents can be edited and saved on your computer so that you can return to them later. Let's take a look at how this works. + +Think of the R script document as a draft of what you plan to type to the Console. + +### Setting the Working Directory{ #Directory} + +Before we get started, let's save the blank R file we just created. First, we want to define the "Working Directory" where files associated with this project will go. To do that go to the menu at the top of the screen and click "Session > Set Working Directory > Choose Working Directory" and then navigate to the location where you would like to save the file. Next, click on "File > Save As" and define a name for your R script. This should end in .R as this is the extension R and R studio recognize for R Scripts. + +### Working with your first R script{ #FirstScript} + +Now that you have saved this script, you can type mathematical operations, functions, and other code just as we did directly in the Console above. The main advantage is that if you make a mistake you can go back and fix it more easily. Go ahead and copy the code in the next code chunk below and paste it in your R script int he Workspace window and then save the document. + + +```r +mat3 <- matrix( + data = c(4, 5, 1, 5, 1, 5), + nrow = 2, + ncol = 3, + byrow = T +) + +mat3^2 +``` + +Once you have this saved, highlight all of the code in the Workspace window and then click the "Run" button on the top right side of this pane (see yellow arrow below). + +![Workspace](images/workspace.jpg){width=100%} + +This will execute the code in your Console and print the results. Let's say when we ran this code, we realized that we actually wanted to raise `mat3` to the 3rd power or we typed one number in our data incorrectly. We can make those changes and then select the code and click run again to do this. This is the true power of scripts in that they allow us to make changes and modify our code easily as we go without retyping commands. Anything you can do in the console you can first set up in the Workspace pane. + +## Installing and Using Packages{ #InstallPackages} + +So far, everything we have done has involved packages included in "base" R and only internal built-in functions. One of the best things about R is the ecosystem of packages created and peer reviewed by others for all manner of statistical analyses you can imagine. There is a package out there for just about everything so it is always a good idea to check before you start to write any complex script on your own. + +In order to install external packages, you need to know the name of the package you want and you simply type `install.packages("NameOfPackage")` at the console. Let's try installing the `vegan` package first which includes lots of useful functions for community ecology research. + + +```r +install.packages("vegan") +``` + +Once our package installs, we can "call" it or initialize it using the `libaray()` function. Notice that when we load this package it also loads `permute` and `lattice` which are two additional packages used within `vegan`. These dependencies were automatically installed when you installed the `vegan` package. + + +```r +library(vegan) +``` + +``` +## Warning: package 'vegan' was built under R version 4.2.3 +``` + +``` +## Loading required package: permute +``` + +``` +## Warning: package 'permute' was built under R version 4.2.3 +``` + +``` +## Loading required package: lattice +``` + +``` +## Warning: package 'lattice' was built under R version 4.2.3 +``` + +``` +## This is vegan 2.6-4 +``` + +Now we can use not just the base R functions, but also the functions within the `vegan` package. Within this package one particularly useful function is called `diversity()` which allows us to calculate all manner of common diversity measures. Remember to check `?diversity` if you want to learn more about the package and its arguments. Let's give it a try by creating a vector and then calculating two different diversity indices on that vector: + + +```r +vec1 <- c(1, 6, 2, 7, 45, 3, 6, 2, 4, 6, 7, 2) + +diversity(vec1, index = "shannon") +``` + +``` +## [1] 1.831803 +``` + +```r +diversity(vec1, index = "simpson") +``` + +``` +## [1] 0.7259993 +``` + +
+

As this example shows, once a package is loaded using the +library() function, there is nothing special about using +external functions. They are called at the Console just like built-in +functions. There is, however, one additional consideration. Since there +are so many packages and they are created by so many people, sometimes +two packages will use the same function name. For example, the +igraph and sna packages both use the function +name degree() for degree centrality. If both packages are +initialized in R, how will R know which one to use? The solution for +this is to use the package name directly in the function call like the +code below:

+
+ + + +```r +igraph::degree(data) # igraph degree function +sna::degree(data) # sna degree function +``` + +When writing code that others will use, it may be a good idea to include package names in function calls to avoid ambiguity. + +
+

There are tons of useful packages out there and it can sometimes be a +bit overwhelming trying to find them. Searching in a search engine for +the simple letter “R” can also yield unexpected results. One helpful tip +when searching for packages is to include “CRAN” or “package” in the +search terms. CRAN stands for the Comprehensive R Archive Network and +this is the archive that contains most of the peer reviewed and +established packages for R.

+
+ +## Working with External Files{ #WorkingWithFiles} + +In many cases we may wish to either write or read an external files with R. Frequently these files take the shape of spreadsheets such as Excel documents or csv (comma separated value) documents. R has many functions for reading in such data and most are built-in to base R. Let's try this out by first writing a .csv (comma separated value) file from a matrix we generate and then reading it back in. Note that any files you write from the console will go directly to the R working directory unless you otherwise specify. + +To write a csv file we use the `write.csv()` function. First we will create a simple matrix, add row names and column names, and then export it. Note that we write this file to a sub-folder of the working directory called "data" here. You could change that to something else but R will return an error if you attempt to read or write from folders that you haven't first created outside of R-Studio. + + +```r +vec2 <- c(4, 2, 65, 4, 2, 4, 6, 4, 2) + +# Notice in the matrix call below we don't enter 'nrow' +# and other argument names as R automatically expects +# them to occur in the order mentioned in the documentation +mat4 <- matrix(vec2, 3, 3) # 3 row 3 column matrix +row.names(mat4) <- c("row 1", "row 2", "row 3") # assign row names +colnames(mat4) <- c("A", "B", "C") #assign col names +mat4 # view matrix +``` + +``` +## A B C +## row 1 4 4 6 +## row 2 2 2 4 +## row 3 65 4 2 +``` + +```r +# Export the matrix as a csv file +dir.create("data", showWarnings = FALSE) +write.csv(mat4, file = "data/output_mat.csv") +``` + +Once you export this file, you should see it appear in the File pane in the bottom right of R-Studio within the working directory. + +![File pane](images/files.jpg) + +If you want to read this file back in, we can simply use the `read.csv()` function. Let's give it a try and create a new object called, `read_mat` from the results of the function. We use the argument `header = T` to indicate that the first row represents column names and `row.names = 1` to indicate that the first column includes the row names. + + +```r +read_mat <- + read.csv(file = "data/output_mat.csv", + header = T, + row.names = 1) +read_mat +``` + +``` +## A B C +## row 1 4 4 6 +## row 2 2 2 4 +## row 3 65 4 2 +``` + +
+

It is important to note here, however, that the +read.csv()function doesn’t know the difference between a +data frame and a matrix unless you specify. Indeed, if we check, R sees +read_mat as a data frame. For some purposes this doesn’t +matter but where it does, we can convert it to a matrix using the +as.matrix() function.

+
+ + +```r +str(read_mat) +``` + +``` +## 'data.frame': 3 obs. of 3 variables: +## $ A: int 4 2 65 +## $ B: int 4 2 4 +## $ C: int 6 4 2 +``` + +```r +read_mat2 <- as.matrix(read_mat) +is.matrix(read_mat2) +``` + +``` +## [1] TRUE +``` + +There are lots of different functions for reading in files in different formats and we will introduce some of these later in the subsequent sections of this tutorial where relevant. For an overview of some of the most common file types [see this Quick-R tutorial](https://www.statmethods.net/input/importingdata.html). + +## Plotting Data{ #Plotting} + +One of the great features of R is the ability to make all kinds of amazing data visualizations. Making simple graphics is very easy but as we will see, defining very specific details often requires a number of different packages and considerable care. Indeed, the vast majority of functions used in this Online Companion are used for visualizations. + +Let's start with something simple by creating two vectors and then creating a bi-plot comparing them. When you use the `plot()` function the plot will automatically appear in the bottom right pane of your R-Studio window. We use the `rnorm()` function here to generate random numbers from a normal distribution. + +
+

In the chunk of code below, and many other places in this document, +we use the set.seed() function. This function expects an +integer and uses that number to initialize the random number generator +built into R. If you use the same seed on your own computer, you will +get the same results we do here. If we entered a different number in +set.seed() we would get different results. This helps us +ensure our code is reproducible.

+
+ + + +```r +set.seed(465) +# Create a random normal variable with 5000 entries and +# a mean of 40 and standard deviation of 3 +x <- rnorm(5000, mean = 40, sd = 3) +# Create a random normal variable with 5000 entries and +# a mean of 5 and standard deviation of 0.5 +y <- rnorm(5000, mean = 5, sd = 0.5) +# plot the results +plot(x, y) +``` + + + +We can also easily create a histogram of a single variable with the additional argument `breaks` which determines how many bars the histogram will have: + + +```r +hist(x, breaks = 20) # breaks defines the number of bars +``` + + + +And boxplots: + + +```r +boxplot(x, y) +``` + + + +There are lots of data visualizations built into base R and we suggest exploring the [R Gallery Book](https://bookdown.org/content/b298e479-b1ab-49fa-b83d-a57c2b034d49/) which outlines many options. + +
+

In the remainder of the Online Companion we will go into detail in +how to modify and configure visualizations but it worth mentioning one +more common visualization tool that has almost eclipsed base R graphics +in popularity. That is the package ggplot2. This package +can be used for all sorts of visualizations and it uses a format that is +somewhat different from that of base R.

+
+ +Let's take a look at an example of a plot using the `ggplot2` format: + + +```r +install.packages("ggplot2") +``` + + +```r +library(ggplot2) +``` + +``` +## Warning: package 'ggplot2' was built under R version 4.2.3 +``` + +```r +df <- data.frame(x, y) + +ggplot(data = df) + + geom_point(aes(x = x, y = y)) +``` + + + +In the code chunk above, we created a data frame (which `ggplot2` requires) combining our random x and y variables. Next, we made a generic call to ggplot2 using the `ggplot(data = df)` line. This creates a ggplot object set up with `df` as the data considered. Notice this line is followed by a `+`. This package will continue to read lines until a line does not end with this symbol and `ggplot` calls can often be quite long. + +The next line was the `geom_point()` function. This package designates different kinds of visualizations as `geom_` and there are many options (`geom_histogram`, `geom_bar`, `geom_polygon`, etc.). The `geom_point` function refers to a simple point plot. The argument inside the function is defined as `aes(x = x, y = y)`. In this package `aes` stands for aesthetics. In this case, we are using this aesthetics call to designate which variable will be on the x and which on the y axis, which is easy here as we named our variable appropriately. + +From here `ggplot2` includes seemingly endless customization options. There are way too many options out there for us to cover here but a good place to start is the [R Graph Gallery](https://r-graph-gallery.com/) website. + +We will cover many examples and the [visualization section](#Visualization) of this tutorial in particular leans heavily on the `ggplot2` format but for now, let's just see a couple of additional examples. + + +```r +# Histogram example +ggplot(data = df) + + geom_histogram(aes(x = y), col = "blue", fill = "darkorchid4") + + xlab("Numbers!!") + + ylab("REALLY BIG NUMBERS") + + theme_minimal() +``` + + + +```r +# Bined biplot example +ggplot(data = df) + + geom_bin2d(aes(x = x, y = y)) + + scale_fill_continuous(type = "viridis") + + theme_bw() +``` + + + +```r +# Create data frame for bar plot example +df2 <- data.frame(d1 = rpois(50, lambda = 4), + gp = sample(size = 50, letters[1:4], replace = T)) +ggplot(data = df2) + + geom_bar(aes(x = d1, fill = factor(gp))) + + theme_dark() +``` + + + +## Warnings and Messages in R{ #Warnings} + +As you have run the code above or in other sections of this documents, you may have seen additional "warnings" or "messages" appear on the screen. For example something like: + +``stat_bin()` using `bins = 30`. Pick better value with `binwidth`` + +This output in the console is simply letting us know that we might want to select a different `binwidth` that better suits our data. Warnings and messages like this are often relatively benign like this, but may also indicate a bigger problem. For example, you may get a warning that a particular method is not appropriate for the data you have (perhaps because of missing data) even though results are provided. Keep a careful eye on these warnings and heed them when necessary. Often looking in the `help()` documentation for a given function will help you interpret these messages. + +For the purposes of this online companion, however, we have "muted" these warnings and messages in the output except in a couple of places where we are pointing out something specific. Any messages you generate should be innocuous but feel free to ask if you have questions or concerns. + +## More Advanced R Features{ #AdvancedR} + +The examples so far have covered most of the basic features of R and R-Studio. There are just a few more things that are implemented in this online document that need a bit of additional explanation. If you can follow along with the examples above, you will be able to replicate most of the work in this document. The features in this section will help you expand your skills and better understand the more complicated code in this document. + +### Conditional Statements{ #Conditionals} + +Another common need for programming in R is to conduct an action conditioned on another action or variable state. For example, if A is `TRUE` then do B. If statements like this are formally in R using the following format: + + +```r +# Example 1 +if (test) { + event1 +} + +# Example 2 +if (test) { + event1 +} else { + event2 +} +``` + +In Example 1 above if the statement called `test` is evaluated as `TRUE` then `event1` is executed. If `test` is evaluated as `FALSE` then nothing happens. + +Example 2 is an if...else statement. In this example if the statement called `test` is evaluated as `TRUE` then `event1` is executed. If `test` is evaluated as `FALSE` then `event2` is executed. + +Let's take a look at a worked example that will print output on the screen depending on the outcome of the `test` expression. + + +```r +x <- 40 + +if (x > 50) { + cat("Greater Than 50") +} else { + cat("Less Than 50") +} +``` + +``` +## Less Than 50 +``` + +```r +if (x * 2 > 50) { + cat("Greater Than 50") +} else { + cat("Less Than 50") +} +``` + +``` +## Greater Than 50 +``` + +```r +if (x > 50) { + cat("Greater Than 50") +} +``` + +In the first example above, the evaluation of `x > 50` was `FALSE` so the statement in brackets after `else` was evaluated. In the second example, the evaluation of `x*2 > 50` was `TRUE` so the first statement was evaluated. Finally, in the third example, `x > 50` was `FALSE` and since there is no `else` statement nothing happened. + +If you want to apply an `if...else` statement to a vector of values rather than one at a time, you can use a useful function `ifelse()`. The `ifelse()` function expects the first item in the parenthesis to be the test expression, followed by the event to execute if the statement is true and then the event to execute if the expression is false. + + +```r +x <- seq(5, 100, by = 5) +x +``` + +``` +## [1] 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 +## [20] 100 +``` + +```r +ifelse(x > 50, "Greater Than 50", "Less Than 50") +``` + +``` +## [1] "Less Than 50" "Less Than 50" "Less Than 50" "Less Than 50" +## [5] "Less Than 50" "Less Than 50" "Less Than 50" "Less Than 50" +## [9] "Less Than 50" "Less Than 50" "Greater Than 50" "Greater Than 50" +## [13] "Greater Than 50" "Greater Than 50" "Greater Than 50" "Greater Than 50" +## [17] "Greater Than 50" "Greater Than 50" "Greater Than 50" "Greater Than 50" +``` + +Another useful and frequently used conditional function is the `which()` function. This function allows you to evaluate which items in an object meet a given condition. Let's take a look at an example to see how this works: + + +```r +x <- seq(1, 10) # sequence of numbers 1 to 10 +x +``` + +``` +## [1] 1 2 3 4 5 6 7 8 9 10 +``` + +```r +which(x > 5) +``` + +``` +## [1] 6 7 8 9 10 +``` + +```r +y <- seq(2, 20, by = 2) # sequence of numbers 2 to 20 by 2s +y +``` + +``` +## [1] 2 4 6 8 10 12 14 16 18 20 +``` + +```r +which(y > 10) +``` + +``` +## [1] 6 7 8 9 10 +``` + +In the first example above, we created a sequence of numbers from 1 to 10 and then evaluated which were greater than 5. The results indicated that items 6, 7, 8, 9, and 10 in the vector were greater than 5. Note that these results are not referring to the values but instead are the numeric indexes of the values. The second example illustrates this. This is much like the first example by we create a sequence of numbers from 2 to 20 counting by 2s. When we evaluate which numbers in the vector are greater than 10, our results tell us the 6th, 7th, 8th, 9th, and 10th numbers are greater than 10. + +### Loops{ #Loops} + +A loop provides a set of instructions for R to repeat a code block some number of times based on rules we supply. The typical syntax is: + + +```r +for (value in sequence) { + event +} +``` + +What this means is that for every value in a sequence of values, evaluate the expression in the `event` chunk. Let's take a look at a worked example to help clarify this. + + +```r +for (i in 1:5) { # for every value in the sequence from 1:5 + print(i * 2) +} +``` + +``` +## [1] 2 +## [1] 4 +## [1] 6 +## [1] 8 +## [1] 10 +``` + +As this example helps illustrate, the `for (i in 1:5)` statement defines `i = 1` and then evaluates the statement `print(i * 2)`, and then defines `i = 2` and evaluates `print(i * 2)`, and so on until it completes the chunk for `i = 5`. The key feature of for loops is that we can use the value assigned to the iterator `i` in the statement inside the curly brackets `{}` to evaluate the statement for a range of values. The sequence of values assigned to the iterator are arbitrary and can occur in any order: + + +```r +val_seq <- c(5, 1, 8, 4, 1, 5, 7) + +for (m in val_seq) { + print(m) +} +``` + +``` +## [1] 5 +## [1] 1 +## [1] 8 +## [1] 4 +## [1] 1 +## [1] 5 +## [1] 7 +``` + +We can also assign the results of any expressions in the curly brackets to a new object. If you want to retain all results and not have the results rewritten, you will need to first define an output object before you start. + + +```r +# Compare these two chunks of code +for (z in 1:10) { + out <- z +} +out +``` + +``` +## [1] 10 +``` + +```r +out <- NULL +for (z in 1:10) { + out[z] <- z +} +out +``` + +``` +## [1] 1 2 3 4 5 6 7 8 9 10 +``` + +In the second example, the statement within the brackets tells R to assign the value of `z` to `out` at position `[z]` and therefore all results are retained rather than rewritten each sequence of the loop. + +There is a lot more than can be done with loops but this basic description should be all you need to know to understand the code in this document. + +### Custom Functions{ #CustomFunctions} + +Finally, we are going to end with a discussion of how R can be used to create custom functions. If there is some operation you do again and again, it doesn't make sense to keep copying and pasting the code every time. It makes more sense to define a function and then just call that. Once defined, a custom function works just like all the built-in and package functions we've seen above. Here is the basic syntax of a function in R: + + +```r +function_name <- function(arguments) { + result <- expression_to_evaluate + return(result) +} + +# Once defined function can be run as +function_name(arguments) +``` + +This format is a fairly simple example. Some functions can be quite complex, but that complexity is usually a product of combining loops and conditional statements and other processes discussed above within the function rather than anything new or beyond what we've shown you so far. Let's take a look at a simple worked example to see how custom functions work: + + +```r +do_something <- function(x, y) { + result <- (x * y) + (x - y)^2 + return(result) +} + +do_something(4, 5) +``` + +``` +## [1] 21 +``` + +```r +do_something(10, 5) +``` + +``` +## [1] 75 +``` + +As this shows, any named argument in the function call can be used in the expression evaluated within the brackets. Functions can contain many lines of code and many arguments but the features and format are the same as the simple examples here. Let's look at a somewhat more complex function to see how this works: + + +```r +myfunct <- function(x) { + z <- NULL + for (i in seq_len(length(x))) { + z[i] <- (x[i] * i) / 5 + } + return(z) +} + +val_seq <- seq(1:10) +myfunct(val_seq) +``` + +``` +## [1] 0.2 0.8 1.8 3.2 5.0 7.2 9.8 12.8 16.2 20.0 +``` + +Let's break down what is happening in the chunk of code above. First, we defined a function with one argument `x`. Inside the function expression we then initialized a new variable for the output called `z` by simply setting it to `NULL` or empty. We then enter a for loop that iterates values of `i` for a sequence of numbers from 1 to the length of vector `x` using the `seq_len()` call.. The value of `z` at position `i` is defined as the value of `x` at position `i` times `i` divided by `5`. Once this loop finishes, the function returns the vector `z` with the results. As this example shows, arguments need not be limited to single values and can include vectors, data.frames, matrices, lists, or any type of R object. + +To clarify how the iterator works, the function `seq_len()` creates a sequence of numbers from 1 to the number indicated. When you are setting the number of runs of a loop based on the length of some other object it is good practice to use this function. + + +```r +seq_len(10) +``` + +``` +## [1] 1 2 3 4 5 6 7 8 9 10 +``` + + +## Test Your Skills{ #TestYourSkills} + +If you've followed along with this tutorial so far, you should be able to do many basic operations in R and R-Studio. Let's now put your skills to the test. Use what you have learned above to create a function that converts Fahrenheit temperatures to Celsius. The formula for this conversion is `(F_temp - 32) * 5 / 9`. Create a function that reads in the F temperature and outputs C and the run it for the sequence of values below. + +Hints: Remember that you can't use `F` as an object name because that is the designation R uses for `FALSE`. Also, think about what you are trying to accomplish here. You want to create a function that iterates across a vector. That's very much what the previous example did so you can use that code as inspiration. + + +```r +f_temp <- c(44, 59, 59, 39, 50, 59, 35) +``` + +Once you have a working function, the use the `round()` function to convert your results to integers (check `?round()` if you need hints on how to do that) and output these results into an object called `res`. Finally run the chunk of code below for a surprise: + + +```r +paste(c(LETTERS[res]), collapse = "") +``` + +We have provided the answer below but give this a try on your own first before peeking at the answer. + +No peeking until you try!! + +![Artwork by Allison Horst `@allison_horst`](images/monster_support.jpg){width=100%} + +Here is our solution below: + + +```r +convert_temp <- function(f) { + results <- NULL + for (i in seq_len(length(f))) { + results[i] <- ((f[i] - 32) * 5 / 9) + } + return(results) +} + +f_temp <- c(44, 59, 59, 39, 50, 59, 35) +out <- convert_temp(f_temp) +out +``` + +``` +## [1] 6.666667 15.000000 15.000000 3.888889 10.000000 15.000000 1.666667 +``` + +```r +res <- round(out, digits = 0) +res +``` + +``` +## [1] 7 15 15 4 10 15 2 +``` + +```r +paste(c(LETTERS[res]), collapse = "") +``` + +``` +## [1] "GOODJOB" +``` + diff --git a/inst/01-data.Rmd b/inst/01-data.Rmd index 7e49323..5fd0cdd 100644 --- a/inst/01-data.Rmd +++ b/inst/01-data.Rmd @@ -163,7 +163,7 @@ If you choose to install everything, however, you can simply run the chunk of co ```{r, message=F, warning=F, eval=F} packages <- c("ape", "devtools", "igraph", "statnet", "intergraph", "tnet", "ggplot2", "rjson", "d3r", "cccd", "networkD3", "visNetwork", - "GISTools", "rgeos", "maptools", "sf", "igraphdata", "ggrepel", + "GISTools", "sf", "igraphdata", "ggrepel", "ggsn", "tidyverse", "superheat", "ggplotify", "ggforce", "colorspace", "ggmap", "dplyr", "ggpubr", "ggraph", "reshape2", "multinet", "RColorBrewer", "Rcpp", "deldir", "vegan", "geosphere", "networkDynamic", diff --git a/inst/02-network-data-formats.Rmd b/inst/02-network-data-formats.Rmd index 19f3675..1b3baa8 100644 --- a/inst/02-network-data-formats.Rmd +++ b/inst/02-network-data-formats.Rmd @@ -278,7 +278,7 @@ weighted_net <- E(weighted_net)$weight <- cibola_edgelist$weight # Explore the first few rows and columns of network object -head(get.data.frame(weighted_net)) +head(igraph::as_data_frame(weighted_net)) # View network as adjacency matrix. Notice the attr="weight" command that # indicates which edge attribute to use for values in the matrix diff --git a/inst/03-exploratory-analysis.Rmd b/inst/03-exploratory-analysis.Rmd index 73ef4f3..e4953ff 100644 --- a/inst/03-exploratory-analysis.Rmd +++ b/inst/03-exploratory-analysis.Rmd @@ -123,10 +123,10 @@ igraph::degree(directed_net, mode = "out")[1:5] # outdegree igraph::degree(simple_net, normalize = T)[1:5] # it is also possible to directly plot the degree distribution for -# a given network using the degree.distribution function. +# a given network using the degree_distribution function. # Here we embed that call directly in a call for a histogram plot # using the "hist" function -hist(igraph::degree.distribution(simple_net)) +hist(igraph::degree_distribution(simple_net)) # graph level centralization igraph::centr_degree(simple_net) @@ -442,10 +442,10 @@ R allows you to use a variety of common cluster detection algorithms to define g #### Girvan-Newman Clustering{#GirvanNewman} -Girvan-Newman clustering is a divisive algorithm based on betweenness that defines a partition of network that maximizes modularity by removing nodes with high betweenness iteratively (see discussion in Brughmans and Peeples 2022 Chapter 4.6). In R this is referred to as the `igraph::edge.betweenness.community` function. This function can be used on directed or undirected networks with or without edge weights. This function outputs a variety of information including individual edge betweenness scores, modularity information, and partition membership. See the help documents for more information +Girvan-Newman clustering is a divisive algorithm based on betweenness that defines a partition of network that maximizes modularity by removing nodes with high betweenness iteratively (see discussion in Brughmans and Peeples 2022 Chapter 4.6). In R this is implemented with the `igraph::cluster_edge_betweenness` function. This function can be used on directed or undirected networks with or without edge weights. This function outputs a variety of information including individual edge betweenness scores, modularity information, and partition membership. See the help documents for more information ```{r} -gn <- igraph::edge.betweenness.community(simple_net) +gn <- igraph::cluster_edge_betweenness(simple_net) set.seed(4353) plot(simple_net, vertex.color = gn$membership) ``` diff --git a/inst/05-visualization.Rmd b/inst/05-visualization.Rmd index 9638884..7b16c59 100644 --- a/inst/05-visualization.Rmd +++ b/inst/05-visualization.Rmd @@ -223,7 +223,7 @@ base_cibola <- get_stamenmap( ) # Extract edgelist from network object -edgelist <- get.edgelist(net) +edgelist <- igraph::as_edgelist(net) # Create dataframe of beginning and ending points of edges edges <- data.frame(xy[edgelist[, 1], ], xy[edgelist[, 2], ]) @@ -839,7 +839,7 @@ nodes_wide <- igraph::as_data_frame(g, "vertices") nodes_long <- nodes_wide %>% dplyr::select(c1:c4) %>% mutate(id = seq_len(nrow(nodes_wide))) %>% - gather("attr", "value", c1:c4) + tidyr::pivot_longer(c1:c4, names_to = "attr", values_to = "value") nodes_out <- NULL for (j in seq_len(nrow(nodes_long))) { temp <- do.call("rbind", replicate(round(nodes_long[j, ]$value * 50, 0), @@ -1026,7 +1026,7 @@ xy <- as.data.frame(coord1) colnames(xy) <- c("x", "y") # Extract edgelist from network object -edgelist <- get.edgelist(road_net) +edgelist <- igraph::as_edgelist(road_net) # Create dataframe of beginning and ending points of edges edges <- as.data.frame(matrix(NA, nrow(edgelist), 4)) @@ -1129,7 +1129,7 @@ base3 <- get_stamenmap( ) # Extract edgelist from network object -edgelist <- get.edgelist(g.net) +edgelist <- igraph::as_edgelist(g.net) # Create dataframe of beginning and ending points of edges edges2 <- data.frame(zz[edgelist[, 1], ], zz[edgelist[, 2], ]) @@ -1222,7 +1222,7 @@ my_map <- get_stamenmap(bbox = c(22, 34.5, 29, 38.8), maptype = "terrain-background") # Extract edgelist from network object for road_net -edgelist1 <- get.edgelist(aegean_net) +edgelist1 <- igraph::as_edgelist(aegean_net) # Create dataframe of beginning and ending points of edges edges1 <- as.data.frame(matrix(NA, nrow(edgelist1), 4)) @@ -2316,7 +2316,7 @@ xy <- as.data.frame(coord1) colnames(xy) <- c("x", "y") # Create edgelist with xy coordinates for each source and target -edgelist2 <- get.edgelist(r_net) +edgelist2 <- igraph::as_edgelist(r_net) edges2 <- data.frame(xy[edgelist2[, 1], ], xy[edgelist2[, 2], ]) colnames(edges2) <- c("X1", "Y1", "X2", "Y2") diff --git a/inst/06-spatial-networks.Rmd b/inst/06-spatial-networks.Rmd index 8edc8ce..2abf30c 100644 --- a/inst/06-spatial-networks.Rmd +++ b/inst/06-spatial-networks.Rmd @@ -76,7 +76,7 @@ locations_sf <- xy <- data.frame(x = nodes$long, y = nodes$lat) # Extract edgelist from network object -edgelist <- get.edgelist(road_net) +edgelist <- igraph::as_edgelist(road_net) # Create data frame of beginning and ending points of edges edges <- as.data.frame(matrix(NA, nrow(edgelist), 4)) @@ -314,7 +314,7 @@ ggraph(mst_net, layout = "kk") + geom_node_point(size = 4) + theme_graph() # Extract edgelist from network object -edgelist <- get.edgelist(mst_net) +edgelist <- igraph::as_edgelist(mst_net) # Create dataframe of beginning and ending points of edges edges <- as.data.frame(matrix(NA, nrow(edgelist), 4)) colnames(edges) <- c("X1", "Y1", "X2", "Y2") @@ -419,11 +419,11 @@ nn1 <- nng(x = nodes[, c(3, 2)], k = 1) # Calculate k=6 nearest neighbor graph nn6 <- nng(x = nodes[, c(3, 2)], k = 6) el1 <- as.data.frame( - rbind(cbind(get.edgelist(nn6), - rep("K=6", nrow(get.edgelist(nn1)) + rbind(cbind(igraph::as_edgelist(nn6), + rep("K=6", nrow(igraph::as_edgelist(nn1)) )), - cbind(get.edgelist(nn1), - rep("K=1", nrow(get.edgelist(nn1)) + cbind(igraph::as_edgelist(nn1), + rep("K=1", nrow(igraph::as_edgelist(nn1)) )))) colnames(el1) <- c("from", "to", "K") g <- graph_from_data_frame(el1) @@ -813,7 +813,7 @@ base <- get_stamenmap( color = "bw" ) # Extract edgelist from network object -edgelist <- get.edgelist(g36_net) +edgelist <- igraph::as_edgelist(g36_net) # Create dataframe of beginning and ending points of edges edges <- as.data.frame(matrix(NA, nrow(edgelist), 4)) colnames(edges) <- c("X1", "Y1", "X2", "Y2") diff --git a/inst/08-spatial-interaction.Rmd b/inst/08-spatial-interaction.Rmd index d41cd47..8ad6c3b 100644 --- a/inst/08-spatial-interaction.Rmd +++ b/inst/08-spatial-interaction.Rmd @@ -143,7 +143,7 @@ net <- weighted = TRUE) # Extract edgelist from network object -edgelist <- get.edgelist(net) +edgelist <- igraph::as_edgelist(net) # Create dataframe of beginning and ending points of edges edges <- data.frame(xy[edgelist[, 1], ], xy[edgelist[, 2], ]) @@ -222,7 +222,7 @@ net2 <- weighted = TRUE) # Extract edgelist from network object -edgelist <- get.edgelist(net2) +edgelist <- igraph::as_edgelist(net2) # Create dataframe of beginning and ending points of edges edges <- data.frame(xy[edgelist[, 1], ], xy[edgelist[, 2], ]) @@ -741,7 +741,7 @@ net <- weighted = TRUE) # Extract edgelist from network object -edgelist <- get.edgelist(net) +edgelist <- igraph::as_edgelist(net) # Create dataframe of beginning and ending points of edges edges <- diff --git a/inst/09-affiliation.Rmd b/inst/09-affiliation.Rmd index a9370d9..6130974 100644 --- a/inst/09-affiliation.Rmd +++ b/inst/09-affiliation.Rmd @@ -346,12 +346,12 @@ where $N_p$ is the number of connections from mode 1 to mode 2 for node $p$ and The Newman method of weighting bipartite networks has been implemented in an R package called `tnet`. This package has a few other useful functions for the analysis of bipartite, weighted, and longitudinal networks so it is worth investigating (see `?tnet`). Unfortunately, it is no longer being actively maintained. ``` -Let's take a look at Newman's method using the `tnet` package. This package expects a simple two-column edge list with only integers for the node identifiers which we can generate using the `igraph` `get.edgelist` function and including the `names = FALSE` argument: +Let's take a look at Newman's method using the `tnet` package. This package expects a simple two-column edge list with only integers for the node identifiers which we can generate using the `igraph::as_edgelist` function and including the `names = FALSE` argument: ```{r, warning=F, message=F, fig.height=7, fig.width=7} library(tnet) -tm_el <- get.edgelist(cibola_inc, names = FALSE) +tm_el <- igraph::as_edgelist(cibola_inc, names = FALSE) head(tm_el) proj_newman <- as.matrix(projecting_tm(tm_el, method = "Newman")) @@ -416,7 +416,7 @@ col_net <- graph_from_adjacency_matrix(crossprod(as.matrix(cibola_clust)), diag = FALSE) # Combine both edgelists into a single frame -el_com <- rbind(get.edgelist(cibola_om_reduced), get.edgelist(col_net)) +el_com <- rbind(igraph::as_edgelist(cibola_om_reduced), igraph::as_edgelist(col_net)) # Define composite network object and add Edge and Vertex attributes net2 <- graph_from_edgelist(el_com, directed = FALSE) diff --git a/inst/scripts/map_net.R b/inst/scripts/map_net.R index f3bf02f..1da6831 100644 --- a/inst/scripts/map_net.R +++ b/inst/scripts/map_net.R @@ -25,14 +25,35 @@ map_net <- xy <- as.data.frame(coord1) colnames(xy) <- c("x", "y") - # Download and extract stamenmap data - my_map <- - get_stamenmap(bbox = bounds, - maptype = gg_maptype, - zoom = zoom_lev) + # Normalize legacy Stamen map names to Stadia's current naming scheme. + stadia_maptype <- switch( + gg_maptype, + watercolor = "stamen_watercolor", + toner = "stamen_toner", + terrain = "stamen_terrain", + terrain_background = "stamen_terrain_background", + terrain_labels = "stamen_terrain_labels", + terrain_lines = "stamen_terrain_lines", + toner_background = "stamen_toner_background", + toner_labels = "stamen_toner_labels", + toner_lines = "stamen_toner_lines", + gg_maptype + ) + + # Use a bundled fallback map when no Stadia API key is configured. + if (nzchar(Sys.getenv("STADIAMAPS_API_KEY"))) { + my_map <- + ggmap::get_stadiamap( + bbox = bounds, + maptype = stadia_maptype, + zoom = zoom_lev + ) + } else { + load("data/road_base.Rdata") + } # Extract edgelist from network object for road_net - edgelist1 <- get.edgelist(net) + edgelist1 <- igraph::as_edgelist(net) # Create dataframe of beginning and ending points of edges edges1 <- as.data.frame(matrix(NA, nrow(edgelist1), 4)) diff --git a/scripts/map_net.R b/scripts/map_net.R index f3bf02f..1da6831 100644 --- a/scripts/map_net.R +++ b/scripts/map_net.R @@ -25,14 +25,35 @@ map_net <- xy <- as.data.frame(coord1) colnames(xy) <- c("x", "y") - # Download and extract stamenmap data - my_map <- - get_stamenmap(bbox = bounds, - maptype = gg_maptype, - zoom = zoom_lev) + # Normalize legacy Stamen map names to Stadia's current naming scheme. + stadia_maptype <- switch( + gg_maptype, + watercolor = "stamen_watercolor", + toner = "stamen_toner", + terrain = "stamen_terrain", + terrain_background = "stamen_terrain_background", + terrain_labels = "stamen_terrain_labels", + terrain_lines = "stamen_terrain_lines", + toner_background = "stamen_toner_background", + toner_labels = "stamen_toner_labels", + toner_lines = "stamen_toner_lines", + gg_maptype + ) + + # Use a bundled fallback map when no Stadia API key is configured. + if (nzchar(Sys.getenv("STADIAMAPS_API_KEY"))) { + my_map <- + ggmap::get_stadiamap( + bbox = bounds, + maptype = stadia_maptype, + zoom = zoom_lev + ) + } else { + load("data/road_base.Rdata") + } # Extract edgelist from network object for road_net - edgelist1 <- get.edgelist(net) + edgelist1 <- igraph::as_edgelist(net) # Create dataframe of beginning and ending points of edges edges1 <- as.data.frame(matrix(NA, nrow(edgelist1), 4)) diff --git a/scripts/netGCD.R b/scripts/netGCD.R index 57f47e9..7bbab1d 100644 --- a/scripts/netGCD.R +++ b/scripts/netGCD.R @@ -5,8 +5,8 @@ require(orca) netGCD <- function(net1,net2) { -net1a <- t(apply(igraph::get.edgelist(net1,names = F),1,as.integer)) -net2a <- t(apply(igraph::get.edgelist(net2,names = F),1,as.integer)) +net1a <- t(apply(igraph::as_edgelist(net1, names = FALSE), 1, as.integer)) +net2a <- t(apply(igraph::as_edgelist(net2, names = FALSE), 1, as.integer)) orb1 <- orca::count4(net1a)[,-c(4,13,14,15)] orb2 <- orca::count4(net2a)[,-c(4,13,14,15)]