Przedstawiamy architekturę i reaktywność aplikacji Shiny

W najprostszej formie aplikacja Shiny wymaga serwera i interfejsu użytkownika (UI). Te komponenty tworzą podstawową architekturę wszystkich aplikacji Shiny. Obiekt ui steruje układem i wyglądem aplikacji, a funkcja server zawiera logikę wymaganą przez aplikację. Jeśli wiesz, jak działają aplikacje internetowe, możesz traktować je odpowiednio jako frontend i backend. Funkcja shinyApp() tworzy i uruchamia aplikację Shiny z jawnej pary interfejs użytkownika / serwer. Skompiluje kod R do przyjaznych dla sieci języków HTML, JavaScript i CSS. Poniżej mamy najprostszą możliwą aplikację Shiny, która ma pusty serwer i UI z podstawowym komunikatem. Jeśli jesteś w interaktywnej sesji języka R, twoja przeglądarka internetowa powinna się uruchomić i pokazać aplikację. Jeśli tak się nie stanie, możesz samodzielnie przejść do adresu URL, który znajduje się w formularzu http://127.0.0.1:6924/, gdzie 127.0.0.1 jest IP twojego komputera a 6924 jest portem, którego Shiny używa do nasłuchiwania połączeń. Jak widzisz w swojej przeglądarce, nie jest to nic niesamowitego, ale jest to działająca aplikacja internetowa stworzona wyłącznie przy użyciu R:

library(shiny)

server <- function(input, output) { }

ui <- fluidPage(„This is Shiny application.”)

shinyApp(ui, server)

Posiadanie stałego portu zamiast losowo przypisanego portu, który zmienia się przy każdym wywołaniu sinyApp(), ułatwia programowanie. Aby użyć stałego portu, zmień wywołanie funkcji shinyApp(ui, server. options = list(port = 6924)) na port, który Ci odpowiada.

Zauważ, że twoja sesja R będzie zajęta, gdy aplikacja jest aktywna, więc nie będziesz mógł uruchamiać żadnych poleceń R. R monitoruje aplikację i wykonuje jej reakcje. Aby odzyskać sesję R, naciśnij Ctrl + C lub, jeśli używasz RStudio, kliknij ikonkę znaku stopu. Dla uproszczenia tworzymy nasze aplikacje Shiny w jednym pliku. Jednak w przypadku większych aplikacji prawdopodobnie podzielisz komponenty na pliki i (które są standardowymi plikami używanymi w aplikacjach Shiny). Jak zobaczymy, Shiny zapewnia świetny paradygmat do tworzenia aplikacji internetowych, który jest obecnie używany w wielu nowatorskich systemach. Nazywa się to funkcjonalnym programowaniem reaktywnym. Nie jest to prosta koncepcja do zrozumienia, ale jest bardzo potężna i nauczymy się korzystać z jej podstaw. Jednak zanim to zrobimy, spróbuję w prosty sposób wyjaśnić, czym jest i jak działa w Shiny.

Dodawanie interaktywności z pulpitami nawigacyjnymi

Shiny umożliwia pisanie potężnych, interaktywnych aplikacji internetowych w całości w R. Używając języka R, możesz stworzyć interfejs użytkownika i serwer, a Shiny skompiluje Twój kod R do kodu HTML, CSS i JavaScript potrzebnego do wyświetlenia aplikacji w sieci. To, co sprawia, że ​​aplikacja Shiny jest szczególnie wydajna, to fakt, że może ona wykonywać kod R na zapleczu, dzięki czemu aplikacja może wykonywać dowolne obliczenia języka R, które można uruchomić na pulpicie. Możesz chcieć, aby aplikacja przetwarzała niektóre dane na podstawie danych wejściowych użytkownika i zapewniała pewną interaktywność, aby analiza danych była bardziej intuicyjna. W tym rozdziale pokażemy, jak to osiągnąć. Shiny implementuje funkcjonalny paradygmat programowania reaktywnego, który obsługuje wiele z najnowocześniejszych aplikacji internetowych. Wyjaśnimy, co to jest i jak działa w Shiny. Pokażemy, jak pracować ze strumieniami zdarzeń pochodzących od użytkowników aplikacji i jak odpowiednio na nie reagować. Aby to zrobić, przeanalizujemy przykład, który otrzymuje dane wejściowe od użytkowników i dostarcza w zamian dane i wykresy. Pod koniec tego rozdziału zdasz sobie sprawę, jak łatwe może być tworzenie potężnych aplikacji internetowych, które przenoszą Twoje umiejętności R na wyższy poziom. Niektóre z ważnych tematów omawianych w tym rozdziale są następujące:

* Architektura aplikacji internetowych Shiny

* Funkcjonalny paradygmat programowania reaktywnego

* Jak reaktywność jest implementowana w Shiny

* Odbieranie danych wejściowych z interakcji użytkowników

* Wysyłanie wyników w odpowiedziach do przeglądarki internetowej

* Dodawanie interakcji do aplikacji Shiny

Wymagane pakiety

Pracowaliśmy już z pierwszymi dwoma pakietami, a mianowicie ggplot2 i lubridate . Pakiet shiny jest używany do tworzenia aplikacji internetowych bezpośrednio z języka R, a pakiety shinythemes i ggthemr są używane do stosowania motywów w celu nadania stylu naszej aplikacji internetowej. Pakiety wymagane dla tej części przedstawiono  poniżej:

Pakiet : Powód

ggplot2 : Wysokiej jakości wykresy

lubridate : Łatwo zmieniaj daty

shiny : Twórz nowoczesne aplikacje internetowe

ggthemr : Zastosuj motywy do wykresów ggplot2

shinythemes : Zastosuj motywy do aplikacji Shiny

Wyspecjalizowane dystrybucje R

Wreszcie, jeśli żadna z poprzednich opcji nie zadziałała, możesz również użyć wyspecjalizowanych dystrybucji R. Te dystrybucje są utrzymywane niezależnie od zwykłej dystrybucji R i koncentrują się na optymalizacji określonych aspektów w R. Niektóre z nich są zbudowane w celu zapewnienia precyzyjnej kontroli równoległości, a inne robią to automatycznie. Nauka korzystania z tych dystrybucji może wymagać dużo czasu, co może być korzystne lub nie w Twoim konkretnym przypadku.

Podsumowanie

Zobaczyliśmy najważniejsze przyczyny powolnego kodu R: programowanie bez zrozumienia niezmienności obiektów, charakter interpretowanych typów dynamicznych, procesy związane z pamięcią i procesy jednowątkowe. Dowiedzieliśmy się, że pierwszy można zredukować za pomocą odpowiedniego R, drugi można zredukować, delegując do języków statystycznie typowanych, takich jak Fortran lub C ++, trzeci można zredukować za pomocą mocniejszych komputerów (szczególnie z większą ilością pamięci RAM), oraz wreszcie czwarty można zredukować za pomocą równoległości. Wspomnieliśmy również o niektórych zmiennych, które możemy chcieć wziąć pod uwagę przy podejmowaniu decyzji, czy optymalizować nasze implementacje, jak mała różnica we wdrożeniu może skutkować dużymi ulepszeniami wydajności oraz jak wzrost wydajności wynikający z tych ulepszeń może rosnąć wraz ze wzrostem rozmiaru nakładów wzrasta. Na koniec dowiedzieliśmy się, jak profilować i porównywać w celu ulepszenia naszych wdrożeń.

Elastyczność i moc dzięki przetwarzaniu w chmurze

Czasami nie potrzebujesz nawet większej mocy obliczeniowej ani efektywnego wykorzystania zasobów. Czasami wystarczy uruchomić R na innym komputerze bez wiązania własnego przez wiele godzin lub dni. W takich przypadkach korzystanie z zasobów przetwarzania w chmurze może być bardzo przydatne. Zasoby przetwarzania w chmurze są przydatne nie tylko wtedy, gdy chcesz użyć dodatkowej maszyny, ale są bardzo wydajnym sposobem na pozyskanie superkomputerów do wykonania pracy za Ciebie. Bardzo łatwo jest zbudować maszynę z 64 rdzeniami procesora i 512 GB pamięci RAM. Korzystanie z takiego systemu może być tańsze niż myślisz i może być wykorzystane do bardzo kosztownych obliczeń, które zajęłyby zbyt dużo czasu w standardowym sprzęcie

Korzystanie ze specjalistycznych pakietów do wydajności

Innym dobrym sposobem na zwiększenie wydajności implementacji jest wyszukanie wyspecjalizowanych funkcji opublikowanych w pakietach CRAN lub gdzie indziej. Zanim przejdziesz do zmiany własnego kodu, spójrz i zobacz, czy możesz znaleźć bardzo wydajną implementację w innym miejscu. Istnieją ogromne różnice w jakości i szybkości pakietów CRAN, ale ich wykorzystanie może zdecydowanie zaoszczędzić sporo czasu. Dwa bardzo wydajne pakiety, które pomogą Ci opracować wydajne implementacje, to pakiety dta.table i dplyr. Mogą zapewnić wydajne sposoby radzenia sobie z ramkami danych, a także w przypadku dplyr innych obiektów. Biblioteka podprogramów podstawowej algebry liniowej (BLAS) może być również bardzo pomocna podczas wykonywania operacji algebry liniowej. Jest napisany przy użyciu języka Fortran i jest wysoce zoptymalizowany.

Poprawa zarządzania danymi i pamięcią

R, jak każdy język programowania, jest ograniczony przez procesor, pamięć RAM i wejścia / wyjścia, a my skupiliśmy się na zwiększeniu szybkości części procesora. Jednak znaczny wzrost wydajności można osiągnąć, zwiększając efektywność wykorzystania pamięci RAM i we / wy. Pomiar wykorzystania pamięci RAM (pamięci) najlepiej wykonywać poza językiem R przy użyciu narzędzi dostarczonych przez system operacyjny do tego właśnie celu. Informacje, które raportują te narzędzia, różnią się w zależności od systemu operacyjnego, ale oto kluczowe wskaźniki, na które należy zwrócić uwagę: użycie procesora, wolna pamięć, pamięć fizyczna, rozmiar wymiany i bajty odczytu / zapisu na sekundę. Jeśli napotkasz wysokie wykorzystanie procesora, procesor jest prawdopodobnie głównym wąskim gardłem dla wydajności R. Użyj technik profilowania opisanych w tym rozdziale, aby zidentyfikować, które części kodu zajmują większość czasu procesora. Jeśli napotkasz wystarczającą ilość wolnej pamięci systemowej z wysokim poziomem operacji we / wy dysku, kod prawdopodobnie wykonuje wiele operacji odczytu / zapisu na dysku. Usuń wszystkie niepotrzebne operacje we / wy i przechowuj dane pośrednie w pamięci, jeśli jest wystarczająca ilość pamięci. Jeśli napotkasz niskie wykorzystanie procesora i małą ilość wolnej pamięci systemowej przy dużym rozmiarze wymiany, prawdopodobnie systemowi zabraknie pamięci fizycznej i tym samym wymienia pamięć na dysk. W takim przypadku sprawdź, czy masz wystarczająco dużo zasobów, aby obsłużyć obciążenia, które wysyłasz do R, a jeśli to zrobisz, spróbuj użyć funkcji rm(), aby usunąć nieużywane obiekty, które czekają na pamięć z sesji R. Jeśli napotkasz scenariusz podobny do poprzedniego, ale wiesz, że nie masz wystarczającej ilości pamięci, aby obsłużyć pełne dane, z którymi pracujesz, nawet jeśli zrobiłeś to skutecznie, możesz spróbować podzielić swoje dane na partycje. Czy możesz pracować z podzbiorem danych według części, a następnie połączyć wyniki? Jeśli tak, powinieneś spróbować. Na przykład, jeśli pełne dane nie mieszczą się w pamięci i próbujesz znaleźć maksymalną wartość, możesz podzielić dane na cztery części, załadować je po kolei, obliczyć maksimum dla każdej z nich, i usuń je z pamięci po wykonaniu tej czynności, zachowując maksimum, a następnie uzyskując maksimum z czterech maksimów obliczonych oddzielnie. Inną możliwością dla scenariusza takiego jak poprzedni jest po prostu migracja obsługi danych do bazy danych. Bazy danych są wyspecjalizowanymi narzędziami do przetwarzania danych i pozwalają uniknąć wąskiego gardła w R, ponieważ tylko wstępnie przetworzony podzbiór potrzebnych danych jest wprowadzany do R. Większość baz danych obecnie wykonuje również bardzo wydajne proste operacje, takie jak znajdowanie maksimum.

Korzystanie z zapamiętywania lub warstw pamięci podręcznej

Jeśli masz algorytmy deterministyczne, za każdym razem, gdy zapewniasz równe dane wejściowe, powinieneś otrzymywać równe wyniki, a jeśli tak jest, a proces przejścia od danych wejściowych do wyjściowych jest bardzo czasochłonny, możesz użyć zapamiętywania lub warstw pamięci podręcznej. Podstawową ideą jest to, że przechowujesz kilka kopii danych wejściowych i wyjściowych, a za każdym razem, gdy dane wejściowe są wysyłane do funkcji, przed obliczeniem danych wyjściowych, sprawdzasz, czy dane wyjściowe tego konkretnego wejścia zostały wcześniej obliczone. Jeśli tak, wyślij to, zamiast wykonywać całą pracę ponownie. Oznacza to, że dane wyjściowe dla każdego wejścia należy obliczać tylko raz. Powinieneś spróbować zaimplementować taką warstwę w funkcji  fibonacci_recursive() utworzoną na początku , aby zobaczyć, jak duży wpływ mogą mieć tego rodzaju techniki, nawet przy użyciu powolnych algorytmów. Czasami tego typu techniki są również używane, nawet jeśli dane wyjściowe dla danego wkładu zmieniają się w czasie. Wszystko, co musisz zrobić w takich przypadkach, to zapewnić mechanizm, który unieważni lub usunie przechowywaną relację wejście / wyjście po określonym czasie, tak aby została ponownie obliczona następnym razem, gdy dane wejściowe zostaną użyte.

Kompilacja Just-in-time (JIT) kodu R

R obsługuje również kompilację Just-in-Time (JIT). Gdy kompilacja JIT jest włączona, język R automatycznie kompiluje bajtami kod, który jest wykonywany bez jawnego wywoływania jednej z funkcji kompilacji. Aby aktywować kompilację JIT, użyj funkcji enableJIT(). Argument level mówi R, ile kodu należy skompilować przed wykonaniem; 0 wyłącza JIT, 1 kompiluje funkcje przed ich pierwszym użyciem, 2 kompiluje także funkcje przed ich zduplikowaniem, a 3 także kompiluje pętle przed ich wykonaniem:

library(compiler)

enableJIT(level=3)

Kompilację JIT można również włączyć, ustawiając środowisko R_ENABLE_JIT w systemie operacyjnym przed uruchomieniem R. Wartość R_ENABLE_JIT należy ustawić na wartość argumentu level.

Trochę szybsze tworzenie kodu R dzięki kompilacji kodu bajtowego

Mimo że R jest językiem interpretowanym, może przejść przez krótką fazę przed wykonaniem kodu zwaną kompilacją kodu bajtowego, która jest mniej rygorystyczną procedurą kompilacji. W niektórych scenariuszach może zaoszczędzić od 5% do 10% czasu, jeśli już zoptymalizowane funkcje nie są intensywnie używane. Wszystkie podstawowe funkcje języka R są domyślnie kompilowanymi kodami bajtowymi. Aby kompilować funkcje w kodzie bajtowym, po załadowaniu pakietu compiler ,należy użyć funkcji cmpfunc() owiniętej wokół funkcji, którą chcesz skompilować. Możesz również przesłać argumenty optios, takie jak options = list(optimize=3)), gdzie element optymalizacji powinien być liczbą całkowitą z przedziału od 0 do 3. Im wyższa liczba, tym więcej wysiłku R włoży w optymalizację kompilacji. Poniższe wiersze pokazują, jak utworzyć funkcję sma_efficient_2_compiled(), która jest skompilowaną wersją funkcji sma_efficient)2():

library(compiler)

sma)efficient_2_compiled <-

cmpfun(sma_efficient_2, options = list(optimize = e))

Inne tematy związane z poprawą wydajności

Zobaczyliśmy przegląd najważniejszych i najczęściej używanych technik optymalizacji implementacji języka R. Jednak wciąż jest wiele rzeczy, których nie omówiliśmy. W kolejnych sekcjach pokrótce omówimy niektóre z nich.

Wstępne przydzielanie pamięci w celu uniknięcia powielania

Wstępna alokacja pamięci jest ważną techniką, którą omawialiśmy niejawnie, kiedy używaliśmy funkcji lapply(), ponieważ wykonuje ona za nas prealokację. Przydatne może być jednak bardziej wyraźne wyjaśnienie. Jak już widzieliśmy, dynamicznie rosnące obiekty w R nie są świetne pod względem wydajności. Zamiast tego należy zdefiniować obiekt o pełnym rozmiarze, jakiego będziesz potrzebować, a następnie wykonać aktualizacje jego elementów zamiast ich ponownego tworzenia. Aby to osiągnąć, możesz użyć czegoś podobnego do double(10) dla zdefiniowania wektora double, który będzie zawierał najwyżej 10 elementów. Ilekroć określisz rozmiar obiektu przed rozpoczęciem korzystania z niego, pomoże ci to uniknąć ponownego tworzenia nowych obiektów za każdym razem, gdy jego rozmiar zostanie zwiększony i zaoszczędzi ci dużo czasu. Jednak dokładna alokacja wstępna nie zawsze jest możliwa, ponieważ wymaga znajomości całkowitej liczby przed iteracją. Czasami możemy tylko poprosić o wielokrotne przechowywanie wyniku, nie znając dokładnej całkowitej liczby. W takim przypadku może nadal dobrym pomysłem jest wstępne przydzielenie listy lub wektora o rozsądnej długości. Po zakończeniu iteracji, jeśli liczba iteracji nie osiągnie wstępnie przydzielonej długości, możemy wziąć podzbiór listy lub wektora. W ten sposób możemy uniknąć intensywnej realokacji struktur danych. Jeśli chodzi o wstępne przydzielanie pamięci, R nie różni się od innych języków programowania. Jednak będąc językiem interpretowanym, nakłada mniej ograniczeń; w ten sposób użytkownicy mogą łatwo przeoczyć tego typu problemy. R nie zgłosi żadnego błędu kompilacji, jeśli pamięć wektora nie jest wstępnie przydzielona. Należy o tym pamiętać podczas pisania szybkiego kodu.