Posts By :

cleandata

Efektívne vizualizácie dát pomocou Gestalt princípov 150 150 cleandata

Efektívne vizualizácie dát pomocou Gestalt princípov

Gestalt princípy zohrávajú kľúčovú úlohu pri tvorbe zrozumiteľných vizualizácií dát. Tento článok skúma, ako tieto princípy pomáhajú organizovať vizuálne prvky tak, aby efektívne komunikovali informácie a minimalizovali kognitívne zaťaženie užívateľa. Prostredníctvom príkladov v R je ukázané, ako správna aplikácia týchto princípov zvyšuje čitateľnosť a prehľadnosť grafov, čo vedie k lepšiemu porozumeniu prezentovaných dát.

gestalt-principles

Úvod

Gestalt princípy sú základné pravidlá, ktoré vysvetľujú, ako ľudia vnímajú vizuálne elementy ako celok. Svoj pôvod majú v Gestalt (celostnej) psychológii zo začiatku 20. storočia. Základnú myšlienku vyslovil Kurt Koffka: “The whole is something else than the sum of its parts”, čiže celok je niečo iné, ako suma jeho častí. Tieto princípy sú kľúčové pri tvorbe efektívnych a zrozumiteľných vizualizácií dát (čiže takých, ktoré jasne komunikujú, čo je “signál” v našich dátach). Dobrá vizualizácia je vytváraná ako celok. Jej jednotlivé časti meníme takým spôsobom, aby tento celok jasne komunikoval hlavnú myšlienku a nevytváral zbytočne veľké kognitívne zaťaženie užívateľa. V tomto článku sa pozrieme na 6 všeobecne rozoznávaných Gestalt princípov a ukážeme si, ako ich aplikovať pri vizualizácii dát pomocou R.

Princíp blízkosti (proximity)

Objekty, ktoré sú blízko pri sebe máme tendenciu považovať za časť jednej skupiny.

Gestalt princíp blízkosti

V BI tento princíp môžeme využiť napr.v dizajne reportov. Aj tak jednoduchý element ako medzera medzi vizuálmi dokáže jednoducho sprehľadniť celý report. Oblasť filtrov, KPI kariet, grafov alebo tabuliek nepotrebujú špeciálne orámovanie. Stačí ich rozumne vzdialiť od seba a efekt je rovnaký a navyše čistejší.

Princíp podobnosti (similarity)

Objekty, ktoré maju podobný tvar, farbu alebo veľkosť sú vnímané ako jedna skupina. Tento princíp využíva napr. PowerBI v základnom dizajne tabuliek. V nich sa farba pozadia riadkov striedavo mení medzi bielou a sivou, čo pomáha čítať riadky, najmä ak je tabuľka široká. Typicky tento princíp využívame pri porovnávaní rôznych kategórií, ktorým priraďujeme rôznu farbu.

Gestalt princíp podobnosti

Princíp oblasti/ohraničenia (enclosure)

Objekty, ktoré sú spoločne uzavreté, považujeme za súčasť jednej skupiny. Uzavretie nemusí byť výrazné, aby sme dokázali využiť tento princíp. Vďaka tomuto princípu môžeme upriamiť pozornosť na tú časť dát, ktorú považujeme za najdôležitejšiu.

Gestalt princíp ohraničenia

Princíp uzavretosti (closure)

Tento princíp asi najlepšie vystihuje citát z úvodu: “The whole is something else than the sum of its parts”. Na obrázku nižšie ako prvé zbadáme kruh. Nerozmýšlame nad ním ako množstvom čiar, ale považujeme ho za celok. V našich mysliach máme množstvo takýchto konštruktov. Rovnako, ak nejaká časť celku chýba, dokážeme si ju podvedome doplniť.

Gestalt princíp uzavretosti

V praxi vieme tento princíp použiť pri odstraňovaní zbytočných elementov ako orámovanie, farebné pozadie, vodiace čiary atď. Napriek tomu užívateľ bez problému rozozná, o aký typ vizualizácie sa jedná a informácia v nej môže vďaka čistejsiemu dizajnu vyniknúť viac.

Princíp plynulosti (continuity)

Princíp plynulosti hovorí, že ľudia majú tendenciu sledovať hladké kontinuálne vzory. Toto môžeme využiť pri čiarových grafoch, kde plynulé línie pomáhajú sledovať trendy v dátach. Druhou možnou aplikáciou je zoraďovanie podľa hodnoty. Pri doplnkových elementoch ako nadpis, podnadpis, značenie osí atď. by mali mať rovnaké zarovnanie a odsadenie.

Gestalt princíp plynulosti

Princíp prepojenia (connection)

Posledný princíp hovorí, že objekty, ktoré sú prepojené, vnímame ako jednu entitu. Toto môžeme aplikovať pri vizualizáciách, kde sú dátové body prepojené čiarami alebo inými vizuálnymi prvkami ako čiarové a pásové grafy, “slope” grafy a iné. Prepojenie vyvoláva väčšinou silnejšiu asociáciu ako tvar alebo farba, ale menšiu ako ohraničenie.

Gestalt princíp prepojenia

Praktický príklad

Pozrime sa na príklad aplikácie rôznych princípov a ako menia naše vnímanie.

Začneme s bodovým grafom.

Základný graf

Pri prvom pohľade sa zrejme jedná o zobrazenie dvoch skupín, keďže v každom mesiaci máme dve hodnoty. Nevieme ale určiť, kam jednotlivé body patria, či existuje nejaký signál v dátach. Skôr to vyzerá na náhodný zhluk bodov.

Pri aplikácii princípu podobnosti (tvar a farba) sa čitateľnosť aj množstvo informácií, ktoré vieme vyčítať, výrazne zvyšuje.

Základný graf + princíp podobnosti

Jedna skupina dosahuje vyššie hodnoty na začiatku aj konci roka, mesiace apríl až jún ma však slabé. Druhá skupina má naopak mesiace apríl až jún silnejšie, v máji je pritom rozdiel najväčší z celého roka. Napriem tomuto zlepšeniu sa graf stále nečíta veľmi dobre. Body príliš skáču a je ťažké udržat na nich pozornosť.

Skúsme teda ešte aplikovať princíp prepojenia.

Základný graf + princíp podobnosti + princíp prepojenia

Čitateľnosť je oveľa ľahšia. Vďaka prepojeniu bodov nemusíme vynakladať veľa úsilia na sledovanie a porovnanie trendu. Zmena v tomto kroku sa mi zdá byť ešte výraznejšia než predchádzajúca. Pri prvom grafe by väčšina používateľov strávila len minimum času a rýchlo by ho preskočila, pretože informácie neboli na prvý pohľad zaujímavé. Druhý graf už poskytol potrebný kontext – jasne vidíme rozdiely medzi dvomi radmi, ktoré by sme nemali prehliadať. Avšak, porozumenie tomuto grafu vyžaduje veľa sústredenia a môže byť únavné. Pridanie prepojenia výrazne zjednodušilo graf a umožnilo nám rýchlejšie a ľahšie pochopiť viac informácií. Mimochodom, všimli ste si, že sme odstránili jeden komponent podobnosti? Rovnaký tvar nežnížil vnímanie rozdielov. Mohli sme ho ponechať, ale všeobecne by som odporučil odstrániť každý element, ktorý sám o sebe nepridáva hodnotu.

Ako posledný aplikujeme princíp ohraničenia.

Základný graf + princíp podobnosti + princíp prepojenia + princíp ohraničenia

Keď sa na graf pozrieme teraz, zrejme nám ako prvá padne do oka výrazná červená oblasť, ktorá je v jasnom kontraste so zvyškom grafu. Typicky sa môže jednať o predpokladaný vývoj do konca roka a neohraničená oblasť bude doterajší reálny vývoj. Následne sa presúvame k vnímaniu informácií z predchádzajúcich krokov ako rozdiel v trendoch dvoch skupín.

Záver

Gestalt princípy sú mocným nástrojom pri navrhovaní vizualizácií dát, ktoré sú nielen esteticky príjemné, ale aj efektívne v komunikácii komplexných informácií. V tomto blogovom príspevku sme si prešli 6 kľúčových princípov blízkosť, podobnosť, ohraničenie, uzavretosť, plynulosť a prepojenie a ukázali sme si, ako ich aplikovať pri tvorbe vizualizácií v jazyku R. Tieto princípy nám pomáhajú usmerňovať pozornosť užívateľa, znižovať kognitívne zaťaženie a zvyšovať zrozumiteľnosť prezentovaných dát.

Pri tvorbe vizualizácií je dôležité mať na pamäti, že každý element by mal byť v návrhu zámerne umiestnený a mal by prispievať k hlavnému cieľu vizualizácie. Zbytočné prvky by mali byť eliminované, aby sa predišlo preťaženiu užívateľa informáciami. Používanie Gestalt princípov vám umožní vytvárať vizualizácie, ktoré sú nielen vizuálne príťažlivé, ale aj funkčné a efektívne v sprostredkovaní informácií.

Ak by ste sa chceli dozvedieť viac o problematike ako vizualizovať a komunikovať dáta, odporúčam knihu od Cole Nussbaumer Knaflic – Storytelling with Data, ktorá odkazuje na mnoho ďaľších zdrojov, a tiež súvisiacu stránku https://www.storytellingwithdata.com/.

Vývoj a nasadenie Shiny aplikácií v jazyku R 150 150 cleandata

Vývoj a nasadenie Shiny aplikácií v jazyku R

Shiny aplikácie umožňujú interaktívnu analýzu dát a vizualizácie v reálnom čase. Shiny, ako súčasť R ekosystému, ponúka flexibilitu a široké možnosti prispôsobenia, čo ho robí atraktívnou alternatívou k tradičným BI nástrojom. V článku jej výhody a nevýhody voči klasickým BI riešeniam, ako sú Power BI alebo Tableau. Zároveň sa pozriem na výzvy spojené s nasadením a údržbou takýchto aplikácií v produkčnom prostredí aj ukážkou reálnej aplikácie.

shiny_app

Úvod

Analyzovanie dát a vytváranie predikčných modelov sú dôležitými krokmi v rámci dátovej analýzy a data science workflow-u. Avšak, tento proces sa nekončí len analýzou samotnou. Konečným krokom je komunikovanie výsledkov. Či už ide o prezentovanie kľúčových zistení alebo o výstup v podobe dátového produktu, je nevyhnutné tieto výsledky efektívne odkomunikovať koncovým užívateľom a stakeholderom.

Existuje mnoho nástrojov, ktoré môžu byť použité na túto komunikáciu – od základných tabuliek a grafov v Exceli, cez prezentácie v PowerPointe, až po použitie pokročilejších dashboardových nástrojov ako PowerBI a Tableau. Pre špecifické potreby a väčšiu flexibilitu však môže byť nevyhnutné vytvoriť vlastné webové aplikácie, ktoré umožnia interaktívnu prácu s dátami. Jedným z nástrojov, ktorý umožňuje vytvárať takéto custom webové aplikácie, je Shiny v R (a aktuálne už aj Shiny v Pythone).

Čo je R Shiny

Shiny je framework od spoločnosti Posit (pôvodne R Studio), ktorý umožňuje vytvárať interaktívne webové aplikácie priamo v R. Je to nástroj pre R programátorov, ktorí chcú prezentovať svoje analýzy a vizualizácie interaktívnym spôsobom bez potreby vedieť HTML, CSS alebo JavaScript (hoci znalosť týchto technológií rozširuje flexibilitu, ktorú Shiny ponúka).

Shiny vs PowerBI (a iné nástroje)

Pomenujme najskôr “slona v miestnosti”. Je Shiny ten najlepší nástroj? Nie, nie je. Pre väčšinu prípadov si bohato vystačíte s Excelom a PowerBI (alebo Tableau či inými alternatívami, ale keďže mám bohaté skúsenosti s PowerBI, budem ho uvádzať ako príklad). Tieto nástroje sú nenáročné na používanie, ich „learning curve“ je ľahko zvládnuteľná na bežnej používateľskej úrovni a väčšinou sa jednoducho integrujú do spoločnosti. Poskytujú tiež dostatočnú flexibilitu, najmä MS produkty vďaka Power Query a jazyku DAX.

Prečo teda použiť Shiny? Odpoveď je jednoduchá: kedykoľvek vytvárate pokročilejšiu analytiku v R, bude jednoduchšie ju dať do produkcie natívnym nástrojom, ktorý nijakým spôsobom neobmedzuje funkcionalitu. Áno, PowerBI umožňuje použitie R a Pythonu, ale existujú obmedzenia na počet záznamov, ktoré môžete spracovať (150 000 riadkov je naozaj málo), podpora knižníc je obmedzená a vizualizácie sú statické obrázky.

Ďalším dôvodom na použitie Shiny môžu byť špecifické požiadavky na dizajn. Vďaka HTML, CSS a JavaScriptu máte prakticky neobmedzené možnosti. Rovnako máte viacero možností, kde si aplikáciu budete hostovať, čo zvyšuje flexibilitu pri nasadení v rôznych prostrediach.

Okrem toho, Shiny v kombinácii s frameworkami ako Golem alebo Rhino, poskytuje silné nástroje pre Continuous Integration/Continuous Deployment (CI/CD) vďaka integrácii s Gitom. Toto umožňuje automatizovaný a kontrolovaný proces vývoja, testovania a nasadzovania aplikácií, čo je obzvlášť užitočné v tímoch alebo pri práci na väčších projektoch. Takýto prístup zvyšuje kvalitu kódu, zrýchľuje vývoj a umožňuje rýchlejšie reagovať na zmeny a požiadavky.

Pre zosumarizovanie:

Výhody Shiny:

  • Flexibilita: Umožňuje používať celú silu R, vrátane knižníc a vlastných skriptov.
  • Open-source: Bezplatný a komunitou podporovaný nástroj.
  • Jednoduché rozšírenie: Možnosť integrácie s ďalšími nástrojmi a technológiami.
  • Výkon: Dokáže spracovať veľké množstvo dát.

Nevýhody Shiny:

  • Optimalizácia: Je nutná optimalizácia skriptov a modulov, aby užívateľ zbytočne nečakal pri dlho bežiacich výpočtoch.
  • Náročnosť: Vyžaduje znalosti R a programovania.

Výhody tradičných nástrojov:

  • Jednoduchosť použitia: Intuitívne rozhranie vhodné pre neprogramátorov.
  • Výkon: Optimalizované pre rýchle spracovanie veľkých dát.

Nevýhody tradičných nástrojov:

  • Náklady: Licenčné poplatky môžu byť vysoké.
  • Obmedzená flexibilita: Menej možností pre pokročilé analýzy a customizáciu oproti Shiny.
  • Výkon: Aj keď je PowerBI optimalizovaný pre veľké dáta, pri nesprávnej práci s ETL v Power Query a zle napísaných DAX funkciách môže výkon výrazne klesnúť, čo môže spôsobiť pomalé načítavanie a spracovanie dát.

“Vanilla” Shiny vs špecializované frameworky

Pri vývoji aplikácií v Shiny máte na výber medzi tzv. “vanilla” Shiny a špecializovanými frameworkami, ako sú Golem a Rhino. Každá možnosť má svoje výhody a nevýhody, ktoré by ste mali zvážiť v závislosti od vašich projektových potrieb.

“Vanilla” Shiny

“Vanilla” Shiny je základná verzia Shiny frameworku, ktorú väčšina používateľov začína používať. Je rýchly na nasadenie a umožňuje rýchle vytváranie prototypov a jednoduchých aplikácií. Táto verzia je ideálna pre menšie projekty alebo pre rýchle vytvorenie funkčného prototypu ako proof of concept.

Výhody:

  • Jednoduchosť a rýchlosť nasadenia.
  • Skvelé pre začínajúcich používateľov alebo menšie projekty.
  • Žiadna dodatočná záťaž z používania frameworkov, priamočiary kód.

Nevýhody:

  • Menej vhodné pre veľké a komplexné aplikácie.
  • Môže byť ťažké udržať čistý a organizovaný kód pri rozrastaní aplikácie.
  • Nedostatok robustných nástrojov na správu veľkých projektov, ako sú moduly, testovanie a CI/CD.

Golem

Golem je špecializovaný framework, navrhnutý pre vývoj robustných a produkčných Shiny aplikácií. Poskytuje silnú štruktúru pre váš projekt a podporuje modulárny prístup k vývoju, čo umožňuje lepšiu organizáciu a škálovateľnosť aplikácie. Výsledný projekt je zabalený ako R knižnica, čo prináša niekoľko výhod, vrátane jednoduchšej správy závislostí, opätovnej použiteľnosti kódu a jednoduchého nasadzovania. Benefitom je tiež kniha Engineering Production-Grade Shiny Apps od autorov Golemu.

Výhody:

  • Robustná štruktúra projektu, ktorá podporuje modularizáciu kódu.
  • Výsledok vo forme R knižnice, čo zjednodušuje správu a opätovné použitie kódu.
  • Umožňuje lepšiu správu veľkých a komplexných aplikácií.
  • Obsahuje nástroje na testovanie, nasadzovanie a CI/CD procesy.

Nevýhody:

  • Strmšia “learning curve” v porovnaní s “vanilla” Shiny.
  • Vyžaduje viac počiatočného nastavenia a organizácie.

Rhino

Rhino je moderný framework od spoločnosti Appsilon pre Shiny aplikácie, zameraný na väčšie projekty a kolaboráciu v tíme. Je postavený na princípoch moderného vývoja softvéru, vrátane automatizovaného testovania a CI/CD, čo ho robí ideálnym pre vývoj v prostrediach, kde sú požiadavky na kvalitu a škálovateľnosť vysoké. Rhino je navyše aktívne vo vývoji a pripravuje sa aj verzia pre Python, čo prináša sľubné rozšírenie možností pre používateľov preferujúcich Python.

Výhody:

  • Integrácia s modernými vývojovými nástrojmi a postupmi.
  • Flexibilná štruktúra, ktorá umožňuje pokročilú customizáciu a integráciu s front-end technológiami.
  • Silná podpora pre tímovú prácu a verziovanie kódu.
  • Aktívny vývoj, vrátane pripravovanej podpory pre Python.

Nevýhody:

  • Vyššia komplexnosť a náročnosť na vývoj v porovnaní s “vanilla” Shiny.
  • Vyžaduje znalosť moderných vývojárskych nástrojov a postupov.

Možnosti deploymentu

Po vytvorení Shiny aplikácie prichádza na rad jej nasadenie (deployment), aby bola dostupná pre koncových užívateľov. Existuje niekoľko možností, ako Shiny aplikáciu nasadiť, pričom každá z nich má svoje výhody a nevýhody.

Shinyapps.io

Najjednoduchší spôsob nasadenia je prostredníctvom shinyapps.io, ktorý je možný na pár kliknutí priamo cez RStudio IDE. Tento hostingový servis od Posit umožňuje rýchle a jednoduché nasadenie aplikácie.

Výhody:

  • Jednoduchosť nasadenia: Nasadenie aplikácie je možné vykonať na pár kliknutí priamo z RStudio.
  • Bez nutnosti infraštruktúry: Nemusíte sa starať o servery, firewall ani hardvér – všetko je spravované v cloude.
  • Free tier: Umožňuje hosťovať až 5 aplikácií zdarma, čo je výhodné pre menšie projekty alebo testovanie.

Nevýhody:

  • Výkon: Vo free verzii môže byť výkon aplikácie pomalší, čo sa prejavuje najmä pri vyššej záťaži alebo zložitých aplikáciách.
  • Zabezpečenie: Shinyapps.io ponúka základné možnosti autentifikácie, ale pokročilé zabezpečenie a kontrolu prístupu je obmedzené.
  • Zdieľaná platforma: Aplikácie bežia na zdieľanej infraštruktúre, čo môže mať vplyv na spoľahlivosť a výkon v prípade veľkého počtu používateľov.

Posit Connect

Posit Connect je profesionálna platforma pre nasadenie Shiny aplikácií a iných dátových produktov (napr. R Markdown, Plumber API, Python aplikácie), ktorá je navrhnutá pre podnikové prostredia. Poskytuje pokročilé možnosti nasadzovania, správu prístupov a monitorovanie aplikácií.

Výhody:

  • Široká podpora obsahu: Podporuje rôzne typy obsahu vrátane Shiny aplikácií, R Markdown reportov, Python skriptov a viac.
  • Bezpečnosť: Umožňuje pokročilé možnosti autentifikácie a kontroly prístupu, čím poskytuje lepšie zabezpečenie dát.
  • Škálovateľnosť: Podpora pre škálovanie aplikácií a správu veľkého počtu používateľov a prístupových práv.
  • Zjednodušené nasadenie: Možnosť nasadenia aplikácií cez push-button alebo pomocou Git.
  • Monitoring a správa: Poskytuje nástroje na sledovanie výkonu aplikácií a správu servera za účelom optimalizácie prevádzky.

Nevýhody:

  • Náklady: Posit Connect je platená služba, čo môže byť finančne náročné pre menšie tímy alebo jednotlivcov.
  • Komplexnosť: Vyžaduje infraštruktúru a určité technické znalosti pre správu a údržbu.

Docker + Cloud služby (GCP, AWS, Azure)

Pre pokročilejších používateľov je možné nasadiť Shiny aplikáciu pomocou Dockeru a cloudových služieb ako Google Cloud Platform (GCP), Amazon Web Services (AWS) alebo Microsoft Azure.

Výhody:

  • Flexibilita: Možnosť nasadiť aplikáciu na rôzne cloudové platformy podľa preferencií.
  • Škálovateľnosť: Vysoká škálovateľnosť a možnosť spravovať zdroje podľa aktuálnych potrieb.
  • Kontrola: Úplná kontrola nad infraštruktúrou a konfiguráciou prostredia.

Nevýhody:

  • Komplexnosť: Vyžaduje pokročilé technické znalosti na konfiguráciu Dockeru a cloudových služieb.
  • Náklady: Prevádzka v cloude môže byť nákladná, najmä pri nesprávnej konfigurácii zdrojov.

Docker + Lokálny RStudio Server

Nasadenie Shiny aplikácie pomocou Dockeru a lokálneho RStudio Servera je ďalšou možnosťou pre tímy, ktoré chcú mať úplnú kontrolu nad svojou infraštruktúrou.

Výhody:

  • Kontrola: Úplná kontrola nad prostredím a konfiguráciou servera.
  • Izolácia: Docker poskytuje izolované prostredie pre aplikácie, čo zlepšuje bezpečnosť a stabilitu.
  • Nákladová efektívnosť: Ak už máte existujúcu infraštruktúru, náklady môžu byť nižšie v porovnaní s cloudovými službami.

Nevýhody:

  • Komplexnosť: Vyžaduje správu a údržbu vlastnej infraštruktúry, čo môže byť náročné na čas, financie a technické zdroje.
  • Obmedzená škálovateľnosť: Lokálna infraštruktúra môže byť limitovaná dostupnými zdrojmi.

Príkladová Shiny Aplikácia

Táto príkladová Shiny aplikácia bola vytvorená pomocou frameworku Rhino a predstavuje posledný krok v rámci projektu, ktorý začal získavaním dát pomocou web scrapingu, spracovaním dát, a následne tvorbou predikčného modelu. Celý projekt bol zdokumentovaný v sérii článkov:

Aplikácia umožňuje používateľom preskúmať a vizualizovať tieto dáta. Ako hlavný benefit, ktorý demonštruje výhody použitia Shiny (okrem custom dizajnu), je integrácia predikčného modelu. Používatelia môžu zadať parametre bytu a aplikácia im poskytne odhadovanú cenu, čo je praktický príklad využitia pokročilej analytiky a interaktívnych možností Shiny.

Aplikáciu si môžete vyskúšať na shinyapps.io alebo ako kontajnerizovanú aplikáciu na Google Cloud Platform (GCP).

Machine learning v jazyku R – Odhad cien bytov 150 150 cleandata

Machine learning v jazyku R – Odhad cien bytov

Machine Learning (Strojové učenie) je podmnožina umelej inteligencie, ktorá sa zameriava na vývoj algoritmov schopných učiť sa a robiť predikcie na základe dát. V jazyku R existuje množstvo nástrojov a frameworkov, ktoré uľahčujú vytváranie a tréning ML modelov. Medzi najpopulárnejšie patrí tidymodels, ktorý poskytujú robustné a efektívne prostriedky na tvorbu rôznych modelov a predikciu. V tomto článku sa zameriam na vytvorenie modelu na predikovanie cien bytov, pričom priblížim aj tému explainable ML na lepšie porozumenie výsledkom modelu.

Vytvorenie ML Modelu v jazyku R

Úvod

V tomto blogovom príspevku prejdem procesom prípravy a tvorby ML modelu, ktorý bude na základe vložených parametrov predikovať cenu bytu. Dáta boli vopred nachystané, ich čistenie a manipuláciu si môžete pozrieť v tomto článku.

Explainable ML

V rámci strojového učenia predstavuje koncept “explainable/interpretable ML” (vysvetliteľné strojové učenie) kľúčový posun smerom k transparentnosti a interpretovateľnosti modelov. Napriek tomu, že modely ako XGBoost ponúkajú výnimočnú prediktívnu silu, ich interné rozhodovacie procesy môžu byť zložité a nejasné, a teda môžu byť tiež vnímané ako “black box”, čo znamená, že nie je ľahké pochopiť, ako sa k predikciám dospelo. Toto vnímanie komplikuje dôveru a akceptáciu modelov v kritických aplikáciách, kde je potrebné pochopenie dôvodov za predikciami.

Vysvetliteľné strojové učenie sa snaží preklenúť túto priepasť poskytujúc nástroje a metódy na objasnenie, ako modely dospeli k svojim rozhodnutiam. Knižnice ako “vip” v jazyku R a metódy ako SHAP (Shapley Additive exPlanations) hodnoty umožňujú analytikom a vývojárom lepšie pochopiť príspevky jednotlivých funkcií k výslednému predikovanému výstupu. Táto schopnosť detailne rozložiť predikčný proces umožňuje nielen hlbšiu analýzu a optimalizáciu modelov, ale tiež zvyšuje transparentnosť a dôveru zo strany koncových užívateľov.

Vysvetliteľné strojové učenie. Zdroj: Lundberg, Scott & Erion, Gabriel & Chen, Hugh & DeGrave, Alex & Prutkin, Jordan & Nair, Bala & Katz, Ronit & Himmelfarb, Jonathan & Bansal, Nisha & Lee, Su-In. (2019). Explainable AI for Trees: From Local Explanations to Global Understanding.

Dôležitosť explainable/interpretable ML naberá na váhe najmä v sektoroch, kde sú dôsledky rozhodnutí založených na predikciách modelu vysoké, ako sú zdravotníctvo, financie alebo právo. V týchto oblastiach je kľúčové, aby boli modely nielen presné, ale aj ich rozhodnutia pochopiteľné a spravodlivé. Vysvetliteľné strojové učenie tak stojí v centre úsilia o vytvorenie technológií, ktoré sú nielen inteligentné, ale aj zrozumiteľné a etické.

Výborná kniha na túto tému je napríklad Interpretable Machine Learning: A Guide For Making Black Box Models Explainable, ktorá je veľmi zrorumiteľne napísaná a obsahuje aj odkazy na knižnice v R aj Pythone. Navyše je dostupná aj online. Náročnejšia publikácia je potom Explainable AI for Trees: From Local Explanations to Global Understanding, ktorá ide viac do detailu a “matematiky” na pozadí.

Načítanie knižníc a dát

Klasicky začíname s načítaním knižníc. Aj tento krát použijeme tidyverse – súbor knižníc navrhnutých na prácu s dátami v R, ktorý zahŕňa napr. ggplot2 pre vizualizáciu, dplyr pre manipuláciu s dátami, tidyr pre úpravu tvaru dát, a iné. Opäť sa objavuje knižnica sf, keďže dáta obsahujú priestorové objekty. Novikou v tomto blogu je tidymodels. Je to framework pre modelovanie a strojové učenie, ktorý poskytuje koherentnú súpravu knižníc na predspracovanie dát, rozdelenie dát, cross-validáciu, výber modelu atď. Je navrhnutý tak, aby bol v súlade s princípmi tidyverse a umožňoval ľahkú integráciu s inými nástrojmi z tohto ekosystému. Dve nasledujúce knižnice pomáhajú s konceptom “explainable/interpretable ML”. Knižnica vip je určená na vizualizáciu dôležitosti premenných v rôznych modeloch strojového učenia. Umožňuje ľahko identifikovať, ktoré premenné majú najväčší vplyv na predikcie modelu, čo je kľúčové pre interpretáciu modelu a pochopenie dát. Knižnica shapviz je špecificky navrhnutá na výpočet a vizualizáciu SHAP hodnôt pre modely vytvorené pomocou XGBoost v R. SHAP hodnoty poskytujú podrobné vysvetlenie predikcií modelu na úrovni jednotlivých pozorovaní, čo pomáha v interpretácii “black box” modelov. Umožnujú pochopiť, ako dôležité sú premenné v rámci modelu. doParallel slúži na paralelné výpočty, a teda skrátenie času trénovania modelu. Posledná knižnica extrafont slúži na načítanie fontov inštalovaných na Windows-e.

Knižnice
# import libraries
if (!require("pacman")) {
  install.packages("pacman")
}

pacman::p_load(
  tidyverse,
  sf, # data contain geometry
  tidymodels,
  vip,
  shapviz,
  doParallel,
  extrafont,
  vetiver,
  xgboost
)

loadfonts(device = "win")
options(scipen = 999)

unregister_dopar <- function() {
  env <- foreach:::.foreachGlobals
  rm(list = ls(name = env), pos = env)
}
Dáta
# import data
original_conditions <- c(
  "Pôvodný stav", "Čiastočná rekonštrukcia", "Kompletná rekonštrukcia",
  "Novostavba", "Vo výstavbe", "Developerský projekt"
)
english_conditions <- c(
  "Original condition", "Partial reconstruction", "Complete reconstruction",
  "New building", "Under construction", "Development project"
)

original_type <- c("3 izbový byt", "1 izbový byt", "2 izbový byt", "4 izbový byt", "Garsónka", "5 a viac izbový byt", "Dvojgarsónka")
english_type <- c("3-room apartment", "1-room apartment", "2-room apartment", "4-room apartment", "Studio", "5 or more room apartment", "Two-room apartment")

apartments_analysis_data <- readRDS("data/apartments_final_data.rds") |>
  filter(!is.na(price)) |>
  mutate(
    coord = st_coordinates(st_centroid(geometry)),
    lon = coord[, 1],
    lat = coord[, 2],
    type = str_replace_na(recode(type, !!!setNames(original_type, english_type)),"Neznáme"),
    condition = str_replace_na(recode(condition, !!!setNames(original_conditions, english_conditions)),"Neznáme"),
    certificate = str_replace_na(str_replace(certificate, "none", "Nemá"), "Neznáme")
  ) |>
  select(-coord) # Optionally remove the original coordinates column

# remove geometry since we have coordinates now
apartments_analysis_data$geometry <- NULL

saveRDS(apartments_analysis_data, "data/apartments_data_App.RDS")

Tidymodels framework


Tidymodels je súbor knižníc v R, ktorý poskytuje jednotné a flexibilné rozhranie pre celý proces strojového učenia, od predspracovania dát cez ich analýzu až po modelovanie. Vytvorenie modelu v rámci frameworku tidymodels pozostáva z niekoľkých základných krokov:

  • Príprava a rozdelenie dát: Prvý krok je rozdelenie nášho “dátového budgetu”. Typicky sa dáta delia na trénovaciu (slúži na odhad parametrov modelu) a testovaciu (slúži na nezávislé zhodnotenie modelu) sadu pomocou funkcie initial_split(). Tento krok je základom pre overovanie modelu a zabránenie overfittingu. S využitím vfold_cv() alebo podobných funkcií vytvoríme schému krížovej validácie (rôzne verzie tréningových dát – tzv. “folds”), ktorá sa použije na evaluáciu modelu.
    Ako rozdelenie na tréningovú a testovaciu sadu, tak aj vytvorenie validačných schém umožnuje specifikovať premennú, ktorej rozdelenie ostane (približne) zachované (strata =).

Rozdelenie dátového budgetu. Zdroj: https://www.tidymodels.org/start/resampling/#data-split
  • Vytvorenie receptu (recipe): recipe() definuje predspracovanie dát, vrátane výberu premenných, transformácií, normalizácie, kódovania kategorických premenných a riešenia chýbajúcich hodnôt. Jednotlivé kroky sa pridávajú pomocou step_*() funkcií. Recipies zabezpečujú, že predspracovanie je konzistentné a reprodukovateľné.

  • Špecifikácia modelu: Model sa špecifikuje nezávisle od dát. Pomocou funkcií ako linear_reg(), rand_forest(), boost_tree() a iných definujeme typ modelu, mód (regresia, klasifikácia), engine(xgboost, lightgbm…) a jeho hlavné parametre bez toho, aby sme ich ihneď fitovali na dáta. Tento krok umožňuje flexibilitu v experimentovaní s rôznymi modelmi.

  • Nastavenie workflow: workflow() integruje recept a model do jednotného objektu. Workflow umožňuje efektívnejšie spracovanie, keďže spojíme predspracovanie dát a modelovanie do jednej operácie, čo zjednodušuje evaluáciu a porovnávanie modelov.

  • Výber a nastavenie hyperparametrov: Pomocou parameters() môžeme definovať a prispôsobiť rozsahy hyperparametrov pre tuning modelu. Tidymodels ponúka rôzne metódy pre vyhľadávanie optimálnych hodnôt, napr. tune_grid(), tune_bayes(), tune_race() a iné.

  • Cross-validácia a tuning modelu: Tuning hyperparametrov prebieha na trénovacej sade (resp. na jednotlivých “fold-och”) s cieľom nájsť najlepšiu kombináciu hyperparametrov. Najlepší model vyberieme pomocou funkcie select_best(), pričom špecifikujeme metriku, podľa ktorej model vyberáme.

  • Finalizácia a fitovanie modelu: Po vybraní najlepších hyperparametrov finalizujeme model pomocou finalize_model() a potom ho fitujete na trénovacie dáta s fit(). Tento krok produkuje finálny model pripravený na evaluáciu a predikcie.

  • Evaluácia modelu: Pomocou testovacej sady dát overíme výkonnosť modelu. Metriky ako RMSE, presnosť (accuracy), AUC a mnoho iných (výber by mal zodpovedať nášmu cieľu, čo platí najmä pri klasifikácii) poskytujú hodnotenie, ako dobre model predpovedá nevidené dáta.

Tieto kroky poskytujú ucelený prístup k vytváraniu, optimalizácii a evaluácii prediktívnych modelov. Tidymodels zabezpečuje konzistenciu a reprodukovateľnosť po celom procese.

Toto je opis základného procesu tvorby ML modelu pomocou tidymodels frameworku. Je možné samozrejme vytvoriť aj omnoho komplikovanejší proces s postupným tuningom hyperparametrov, tvorbou stacked modelu (meta-learner modelu) atď.

Tvorba modelu

Rozdelenie dát
# split dataframes to train(80)/test(20)
set.seed(123)
apartments_train_split <- initial_split(apartments_analysis_data, prop = 0.7, strata = price)

apartments_train <- training(apartments_train_split)
apartments_test <- testing(apartments_train_split)

folds <- vfold_cv(apartments_train, v = 5, strata = price)
saveRDS(apartments_test, "data/test_set.RDS")
Spracovanie dát
apartments_xgboost_recipe <- recipe(apartments_train, price ~ .) |>
  step_rm(name_nsi) |>
  step_string2factor(all_nominal_predictors()) |>
  step_impute_knn(all_nominal_predictors()) |>
  step_unknown(all_nominal_predictors()) |>
  step_dummy(all_nominal_predictors())
Špeficikácia modelu
xgb_model <-
  boost_tree(
    trees = tune(), loss_reduction = tune(),
    tree_depth = tune(), min_n = tune(),
    mtry = tune(), sample_size = tune(),
    learn_rate = tune()
  ) |>
  set_mode("regression") |>
  set_engine("xgboost")
Workflow
apartments_workflow <- workflow() |>
  add_recipe(apartments_xgboost_recipe) |>
  add_model(xgb_model)
Výber parametrov
apartments_xgboost_params <- parameters(
  trees(), learn_rate(), loss_reduction(),
  tree_depth(), min_n(),
  sample_size = sample_prop(),
  finalize(mtry(), apartments_train)
)

apartments_xgboost_params <- apartments_xgboost_params |> update(trees = trees(c(300, 600)))


Pri tuningu použijeme paralelné spracovanie na x – 2 jadrách. Niektoré algoritmy umožnujú aj spracovanie pomocou GPU ak je dostupná kompatibilná grafická karta (bavíme sa o tvorbe modelu na lokálnom zariadení, samozrejme v produkcii je vhodnejšie využiť služby ako GCP, AWS a iné)

Tuning parametrov
registerDoParallel(cores = detectCores() - 1)

xgboost_tune <-
  apartments_workflow |>
  tune_bayes(
    resamples = folds,
    param_info = apartments_xgboost_params,
    iter = 100,
    metrics = metric_set(rmse, mape),
    control = control_bayes(
      no_improve = 20,
      save_pred = T, verbose = F
    )
  )

unregister_dopar()
Finalizácia modelu
apartments_best_model <- select_best(xgboost_tune, metric = "rmse")
apartments_final_model <- finalize_model(xgb_model, apartments_best_model)
apartments_workflow <- apartments_workflow |> update_model(apartments_final_model)
apartments_xgb_fit <- fit(apartments_workflow, data = apartments_train)

saveRDS(apartments_xgb_fit, "data/xgb_fit.RDS")
Výsledné metriky a testovacia predikcia
apartments_final_res <- last_fit(apartments_workflow, split = apartments_train_split)
apartments_pred <-
  predict(apartments_xgb_fit, apartments_test) |>
  bind_cols(apartments_test)

Vyhodnotenie modelu

Z grafu je zrejmé, že výsledný model vcelku dobre predikuje ceny bytov s hodnotou do približne 300 tisíc EUR. Pri drahších bytoch je variabilita odchýlok od reálnych cien vyššia.

Porovnanie predikcie s reálnou cenou 1
plot1 <-
  apartments_pred |>
  ggplot(aes(x = .pred, y = price)) +
  geom_point() +
  geom_smooth(method = "loess", color = "red") +
  scale_y_continuous(labels = function(x) format(x, big.mark = " ", scientific = FALSE), breaks = c(200000, 400000, 600000)) +
  scale_x_continuous(labels = function(x) format(x, big.mark = " ", scientific = FALSE), breaks = c(200000, 400000, 600000)) +
  labs(
    title = NULL,
    x = "Predikovaná cena",
    y = "Reálna Cena"
  ) +
  theme_minimal() +
  theme(
    text = element_text(family = "Courier New", size = 12)
  ) +
  coord_fixed()

plot1

Korelácia reálnych a predikovaných cien

Pri pohľade na rozloženie reálnych a predikovaných cien môžeme vidieť kde presne dochádza k najväčším odchýlkam. Model predikoval viac bytov v cene približne 100 až 170 tisíc EUR a približne 200 až 225 tisíc EUR. Naopak menej zastúpené sú najmä byty s cenou približne 250 až 300 tisíc EUR. Teraz je potrebné rozhodnutie, či sa treba vrátiť k procesu feature engineering, čiže manipulácii s premennými alebo pokračovať s model v stave v akom je.

Porovnanie predikcie s reálnou cenou 2
plot2 <-
  apartments_pred |>
  select(predikcia = .pred, realita = price) |>
  gather(key, value) |>
  ggplot(aes(x = value, color = key)) +
  geom_density(alpha = .5) +
  labs(
    title = NULL,
    x = "Cena",
    y = "Hustota",
    color = ""
  ) +
  scale_y_continuous(labels = function(x) format(x, big.mark = " ", scientific = FALSE)) +
  scale_x_continuous(labels = function(x) format(x, big.mark = " ", scientific = FALSE)) +
  theme_minimal() +
  theme(
    text = element_text(family = "Courier New", size = 12),
    legend.position = "bottom"
  )

plot2

Rozloženie reálnych vs predikovaných cien

Model v priemere mierne nadhodnocuje (do 5 percent) byty s reálnou cenou medzi 100 až 170 tisíc EUR. Najvyššie nadhodnotenie (viac ako 5 percent) je pri cenách do 100 tisíc EUR. Naopak byty s cenou od 180 tisíc EUR do 260 tisíc eur mierne podhodnocuje. V prípade bytov od 260 tisíc EUR (približne 7 percent z inzerátov) sú už odchýlky nestabilné, trend je však smerom k výraznejšiemu podhodnocovaniu.

Odchýlky
# Calculate the difference between prediction and actual value
plot3_data <- apartments_pred |>
  select(.pred, price) |> 
  mutate(diff = .pred - price,
         bin = floor(price/10000)*10000) |>
  group_by(bin) |>
  summarize(mean_diff = mean(diff)) |> 
  mutate(rel_diff = case_when(
    mean_diff/bin >= 0.05 ~ 0.05,
    mean_diff/bin >= 0 ~ 0,
    mean_diff/bin <= -0.05 ~ -0.05,
    mean_diff/bin < 0 ~ -0.02
    ))


# Plot with coloring based on mean difference
plot3 <- ggplot(plot3_data, aes(x = bin, y = mean_diff, color = factor(rel_diff))) +
  geom_point() +
  labs(
    title = NULL,
    x = "Cena",
    y = "Priemerná chyba"
  ) +
  scale_color_manual(values = c("#506BA0", "#55B9F5", "#90ee90","#037f51" )) +
  scale_y_continuous(labels = function(x) format(x, big.mark = " ", scientific = FALSE)) +
  scale_x_continuous(labels = function(x) format(x, big.mark = " ", scientific = FALSE)) +
  theme_minimal() +
  theme(
    text = element_text(family = "Courier New", size = 12),
    legend.position = "none")

plot3

Odchýlky predikcie od reality

Pri XGBoost je dôležitosť premenných založená na Gain, tento termín odkazuje na príspevok každej premennej k celkovému zlepšeniu modelu, ktoré je dosiahnuté vďaka rozdeleniam (splits) na konkrétnej premennej. Keď XGBoost vytvára stromy, každé rozdelenie v strome sa vyberá tak, aby maximalizovalo “zisk”, čo znamená, že sa snaží o čo najväčšie zlepšenie prediktívnej presnosti. Gain v tomto kontexte meria, o koľko sa zlepšuje predikcia, keď sa použije rozdelenie založené na danej premennej. Tento zisk je často vážený a sumarizovaný cez všetky stromy v modeli. Premenné, ktoré najviac prispievajú k zlepšeniu sú tie, ktoré model najviac využíva na dosiahnutie presnejších predikcií, a preto sú považované za dôležitejšie. Viac informácií môžete nájsť napríklad v tomto článku na medium.com.

Najdôležitejšou premennou je podľa modelu geografická dĺžka. Nasledujú rozloha bytu a lokalita Bratislava I. Až 4 z top 10 premenných súvisia s indexom bývania.

vip – Variable Importance Plots
apartments_xgb_fit |>
  fit(data = apartments_train) |>
  pull_workflow_fit() |>
  vip(geom = "point", include_type = TRUE) + 
  theme_minimal() +
  theme(
    panel.grid.minor = element_blank(),
    text = element_text(family = "Courier New", size = 12)
  ) + 
  labs(
    y = "Dôležitosť",
    x = "Premenná"
  ) +
  theme(
    panel.grid.minor = element_blank(),
    text = element_text(family = "Courier New", size = 12)
  )

Dôležitosť premenných v modeli


SHAP (Shapley Additive exPlanations) hodnoty sú metóda používaná na vysvetlenie príspevku jednotlivých premenných k predikcii konkrétneho modelu strojového učenia. SHAP hodnoty poskytujú detailné vysvetlenie predikcie pre každý záznam (riadok dát) tým, že ukážu, ako každá premenná prispieva k výslednej predikcii, či už zvyšovaním alebo znížením predikovanej hodnoty. Tiež zaručujú konzistentnosť, čo znamená, že ak máme dve premenné a jedna konzistentne prispieva k predikcii viac než druhá, bude mať aj vyššiu SHAP hodnotu. Okrem poskytovania vysvetlení na úrovni jednotlivých predikcií (lokálne vysvetlenie) môžu byť SHAP hodnoty agregované na poskytnutie prehľadu o dôležitosti premenných v celom modeli (globálne vysvetlenie).

SHAP hodnoty nám poskytujú ďaľšie informácie k dôležitosti premenných. Napr. sa potvrdzuje západo-východný gradient, ktorý sme spomínali v závere tretej časti tejto série. Ak sa byt nachádza v okrese Bratislava I, môže mu to pridať na hodnote 45 až 95 tisíc EUR. Ak sa jedná o novostavbu cena môže stúpnuť o 15 až 50 tisíc EUR, kompletná rekonštrukcia pridáva na hodnote do 25 tisíc EUR, naopak pôvodný stav má negatívny efekt do približne -20 tisíc EUR.

SHAP
fitted_data <- apartments_xgboost_recipe |>
  prep() |>
  bake(new_data = apartments_analysis_data) |>
  select(-price)

set.seed(123)
shp <- shapviz(extract_fit_engine(apartments_xgb_fit), X_pred = fitted_data |> as.matrix())

shap_plot <- sv_importance(shp, kind = "beeswarm")  +
  scale_x_continuous(labels = function(x) format(x, big.mark = " ", scientific = FALSE)) +
  scale_color_gradient(low = "blue", high = "red",
                      breaks = c(0,1),
                      labels = c("nízka", "vysoká"),
                      guide = guide_colorbar(barwidth = 12, barheight = 0.3)) + # Customize based on your exact needs
  labs(x = "SHAP hodnota (vplyv na predikovanú cenu)",
       y = "Premenná",
       color = "Hodnota premennej "
       ) +
  theme_minimal() +
  theme(
    panel.grid.minor = element_blank(),
    text = element_text(family = "Courier New", size = 10),
    legend.position = "bottom"
  )

Top 15 premenných podľa SHAP hodnôt

Okrem celkového príspevku jednotlivých premenných v rámci modelu ako celku (globálne vysvetlenie) sa vieme pozrieť aj na jednotlivé predikcie. Napr. keď sa pozrieme na inzerát na riadku 1804, môžeme vidieť, že lokalita v okrese Bratislava I mu pridala na hodnote 63 tisíc EUR, geografická dĺžka ďalších 33 tisíc EUR a rozloha 84 m2 20 tisíc EUR. Naopak to, že sa nejedná o novostavbu znižuje cenu o 12 tisíc EUR a pôvodný stav uberá ďalších 9 tisíc EUR.

Príklad SHAP hodnôt jednej predikcie

Uloženie modelu a dát pre aplikáciu

Aby sa model dal použiť v Shiny aplikácii, je nutné ho uložiť vo vhodnom formáte. Natrénovaný model transformujeme na vetiver model. Tento objekt obsahuje všetko, čo treba, aby sa dal použiť v novom prostredí. Ďalej ukladáme engine pomocou knižnice xgboost. Budeme ho potrebovať pri SHAP grafe.

Exploratory data analysis (EDA) v jazyku R 150 150 cleandata

Exploratory data analysis (EDA) v jazyku R

Exploratory Data Analysis (EDA) je proces analýzy dát s cieľom porozumieť ich základným charakteristikám, často pomocou základných štatistických a vizuálnych metód. V jazyku R sa EDA realizuje rôznymi nástrojmi a technikami, ktoré umožňujú odhaľovať vzory, anomálie a vzťahy medzi premennými, čo je kľúčové pre efektívnu prípravu dát na ďalšie analýzy a modelovanie.

consolidate_sk

Úvod

V tomto blogu sa budem venovať Exploratory Data Analysis (EDA), čiže úvodnej analýze údajov, ktorej cieľom je zistiť aká je kvalita, obsah a štruktúra údajov. V tomto prípade ide o dáta z inzercií nehnuteľností. Dáta sú scrape-nuté z webu Nehnutelnosti, procesmi webscraping-u a geokódovania som prešiel v predchádzajúcich blokoch “Web scrapingpomocou jazyka R” a “Geocoding pomocou jazyka R”.

Čo je EDA


Exploratory Data Analysis je neoddeliteľnou súčasťou dátovej analytiky/dátovej vedy (Data science).

EDA v data science projekte. Zdroj: https://commons.wikimedia.org/wiki/File:Data_visualization_process_v1.png


Účelom EDA je zhrnúť hlavné charakteristiky súboru údajov (ako kvalita, obsah a štruktúra), objaviť vzorce a vzťahy medzi premennými a identifikovať trendy. Malo by nás viesť k pochopeniu údajov a identifikácii kritických premenných vzhľadom na naše ciele. Ako je znázornené na obrázku, ide o iteratívny proces. Na základe vašich zistení môžete buď pokračovať v modelovaní/testovaní hypotéz a reportovaní, alebo sa vrátiť k čisteniu/spracovaniu údajov.
EDA zvyčajne začína načítaním údajov a kontrolou niekoľkých riadkov, aby ste získali prvotný “pocit” z údajov spolu s kontrolou štruktúry údajov, veľkosti vzorky, typov údajov, chýbajúcich hodnôt atď. Potom pokračuje podrobnejšou analýzou, ktorá nám pomáha pochopiť vzťahy a identifikovať odľahlé hodnoty a dôležité premenné. V EDA používame rôzne techniky a nástroje. Vo všeobecnosti ich možno rozdeliť do niekoľkých skupín:

  • Súhrnné (jednopremenné) štatistiky – min, max, priemer, medián, kvartily, IQR, štandardná odchýlka, počty, frekvencia atď.
  • Vizualizácia dát – histogram, boxplot, Paretov graf, bodové grafy, korelačná matica, čiarové grafy (pre časové rady), heatmapy atď.
  • Bi-/viacpremenné štatistiky – korelácia, t-test, chí-kvadrát test, ANOVA, Kruskal-Wallisov test atď.

Na základe zistení vytvoríme záver a buď pokračujeme v projekte, alebo sa vrátime k dodatočnému upratovaniu dát. Je to teda iteratívny proces.
Aj keď radšej robím EDA manuálne, existuje niekoľko R knižníc pre automatizované EDA. Sú užitočné pri prvotnom skúmaní údajov a identifikácii napr. dátových typov, premenných s veľkou časťou chýbajúcich hodnôt a iných “high-level” charakteristík. Sú to napríklad:

  • DataExplorer
  • ExPanDaR
  • dataMaid
  • dlookr

Úvodné čistenie dát


Začínam klasicky, načítaním knižníc pomocou funkcie p_load z knižnice pacman.

Knižnice
# import libraries
if (!require("pacman")) {
  install.packages("pacman")
}

pacman::p_load(
  janitor, # clean_names()
  skimr, # skim()
  sf, # geospatial data
  ggpubr,
  ggQC, # pareto chart
  scales, # scales
  GGally, # eval_data_col
  knitr,
  modelsummary, # datasummary_correlation()
  gtsummary, # tables
  ggstatsplot, # ggwithinstats()
  effectsize, # interpret_kendalls_w()
  tidyverse, # data wrangling
  kableExtra, # tables
  extrafont # fonts
)

loadfonts(device = "win")


Nasleduje prvotné čistenie dát. V nasledujúcom kóde spájam 3 rôzne súbory. Keďže sú z rôznych zdrojov, je potrebné niektoré hodnoty upraviť do rovnakého tvaru (prípad názvov obcí).
Následne upravujem premenné do správnych typov, odfiltrujem preč záznamy, ktorých hodnoty sú odľahlé alebo úplne chýbajú a nemá zmysel ich imputovať.
Krok preloženia slovenských výrazov do angličtiny nie je nevzhnutný. Robím ho jednak z dôvodu, že som zvyknutý pracovať s anglickými výrazmi pri kódovaní a chcem aby aj dataset bol v tomto ohľade konzistentný. Druhým dôvodom je, že budem dataset nahrávať na Kaggle.
V poslednom kroku robím dve verzie datasetu. Jedna obsahuje premennú ‘geometry’ typu sfc_MULTIPOLYGON, ktorá robí problém alebo extrémne spomaluje výpočty niektorých sumačných funkciách, ak sú aplikované na celý dataset. Preto na všetku EDA budem používať verziu bez nej.

Feature engineering
# Load advertisements data from RDS file
advertisements <- readRDS("data/advertisements.RDS")

# Clean and restructure advertisements data
advertisements <- advertisements %>%
  separate(type_of_real_estate, c("type", "area"), sep = " • ", remove = TRUE) %>%
  select(link, type)

# Load and process districts mapping data from Excel file
districts_mapping <- openxlsx::read.xlsx("data/obce_okresy.xlsx") %>%
  mutate(
    municipality = str_replace(municipality, "Košice - ", "Košice - mestská časť "),
    municipality = str_replace(municipality, "Bratislava - ", "Bratislava - mestská časť ")
  )

# Load and process scraped data with geocoding
scraped_data <- readRDS("data/advertisements_complete_geocoded.RDS") %>%
  filter(!is.na(link)) %>%
  select(-c(address1, address2, info_text, district, municipality, address)) %>%
  rename(quality_of_living = quanlity_of_living) %>%
  mutate(
    NAME_NSI = str_replace(NAME_NSI, "Hodruša-Hámre", "Hodruša - Hámre"),
    NAME_NSI = str_replace(NAME_NSI, "Perín-Chym", "Perín - Chym"),
    NAME_NSI = str_replace(NAME_NSI, "Šaštín-Stráže", "Šaštín - Stráže"),
    NAME_NSI = str_replace(NAME_NSI, "Kostolná-Záriečie", "Kostolná - Záriečie")
  )

# Join advertisements and scraped data
joined_data <- scraped_data %>%
  left_join(advertisements, by = "link", multiple = "first", keep = FALSE) %>%
  clean_names() %>%
  filter(!is.na(price)) %>%
  mutate(
    # Convert relevant columns to numeric format
    pocet_izieb_miestnosti = as.numeric(pocet_izieb_miestnosti),
    uzit_plocha = str_replace(str_replace(uzit_plocha, ",", "."), " m2", ""),
    energie = str_replace(str_replace(energie, ",", "."), " €/mesiac", ""),
    provizia_zahrnuta_v_cene = str_replace_na(provizia_zahrnuta_v_cene, "Nie"),
    # Create a 'rooms' column based on 'type' and handle missing values
    rooms = case_when(type == "1 izbový byt" ~ 1,
      type == "2 izbový byt" ~ 2,
      type == "3 izbový byt" ~ 3,
      type == "4 izbový byt" ~ 4,
      type == "5 a viac izbový byt" ~ 5,
      type == "Garsónka" ~ 1,
      type == "Dvojgarsónka" ~ 2,
      .default = NA
    ),
    pocet_izieb_miestnosti = coalesce(pocet_izieb_miestnosti, rooms, pocet_izieb_miestnosti)
  ) %>%
  mutate_at(c(
    "index_of_living",
    "uzit_plocha",
    "energie",
    "pocet_nadzemnych_podlazi",
    "podlazie",
    "pocet_izieb_miestnosti",
    "rok_vystavby",
    "rok_poslednej_rekonstrukcie",
    "pocet_balkonov",
    "pocet_lodzii"
  ), as.numeric) %>%
  select(-link) %>%
  filter(pocet_izieb_miestnosti < 10 & !is.na(pocet_izieb_miestnosti)) %>%
  mutate(
    type = coalesce(type, case_when(
      pocet_izieb_miestnosti == 1 ~ "1 izbový byt",
      pocet_izieb_miestnosti == 2 ~ "2 izbový byt",
      pocet_izieb_miestnosti == 3 ~ "3 izbový byt",
      pocet_izieb_miestnosti == 4 ~ "4 izbový byt",
      pocet_izieb_miestnosti >= 5 ~ "5 a viac izbový byt"
    ))
  ) %>%
  select(-rooms) %>%
  filter(!(type %in% c("Apartmán", "Mezonet", "Iný byt", "Loft"))) %>%
  rename(
    index = index_of_living,
    condition = stav,
    area = uzit_plocha,
    provision = provizia_zahrnuta_v_cene,
    certificate = energeticky_certifikat,
    energy_costs = energie,
    construction_type = typ_konstrukcie,
    year_built = rok_vystavby,
    last_reconstruction = rok_poslednej_rekonstrukcie,
    total_floors = pocet_nadzemnych_podlazi,
    floor = podlazie,
    lift = vytah,
    balkonies = pocet_balkonov,
    loggia = pocet_lodzii,
    cellar = pivnica,
    orientation = orientacia
  ) %>%
  mutate(
    # Recreate 'rooms' column after filtering and handle missing values
    rooms = as.numeric(case_when(
      type == "1 izbový byt" ~ 1,
      type == "2 izbový byt" ~ 2,
      type == "3 izbový byt" ~ 3,
      type == "4 izbový byt" ~ 4,
      type == "5 a viac izbový byt" ~ 5,
      type == "Garsónka" ~ 1,
      type == "Dvojgarsónka" ~ 2,
      .default = NA
    )),
    # Transform 'provision' to binary
    provision = as.numeric(case_when(
      provision == "Áno" ~ 1,
      provision == "Nie" ~ 0,
      .default = NA
    )),
    # Transform 'lift' to binary
    lift = as.numeric(case_when(
      lift == "Áno" ~ 1,
      .default = 0
    )),
    # Transform 'cellar' to binary
    cellar = as.numeric(case_when(
      cellar == "Áno" ~ 1,
      .default = 0
    )),
    certificate = if_else(certificate == "nemá", "none", certificate)
  ) %>%
  select(-pocet_izieb_miestnosti) %>%
  mutate(
    # Convert relevant columns to numeric format
    across(c(
      "environment", "safety", "transport", "relax", "quality_of_living", "services"
    ), na_if, "0"),
    across(c(
      "environment", "safety", "transport", "relax", "quality_of_living", "services"
    ), as.numeric)
  )

# Translating Slovak terms into English
# Define mapping vectors
original_conditions <- c(
  "Pôvodný stav", "Čiastočná rekonštrukcia", "Kompletná rekonštrukcia",
  "Novostavba", "Vo výstavbe", "Developerský projekt"
)
english_conditions <- c(
  "Original condition", "Partial reconstruction", "Complete reconstruction",
  "New building", "Under construction", "Development project"
)
original_construction_type <- c("Tehlová", "Panelová", "Iná", "Kvádrová", "Zmiešaná", "Panelová, Tehlová", "Skeletová", "Tehlová, Železobetónová", "Kamenná", "Montovaná", "Drevená")
english_construction_type <- c("Brick", "Panel", "Other", "Cube", "Mixed", "Panel, Brick", "Skeletal", "Brick, Reinforced concrete", "Stone", "Mounted", "Wooden")
original_orientation <- c("Juhozápadná", "Južná", "Západná", "Východná", "Juhovýchodná", "Severovýchodná", "Severozápadná", "Severná")
english_orientation <- c("Southwest", "South", "West", "East", "Southeast", "Northeast", "Northwest", "North")
original_type <- c("3 izbový byt", "1 izbový byt", "2 izbový byt", "4 izbový byt", "Garsónka", "5 a viac izbový byt", "Dvojgarsónka")
english_type <- c("3-room apartment", "1-room apartment", "2-room apartment", "4-room apartment", "Studio", "5 or more room apartment", "Double studio")

# Translate values
joined_data <- joined_data %>%
  mutate(
    condition = recode(condition, !!!setNames(english_conditions, original_conditions)),
    construction_type = recode(construction_type, !!!setNames(english_construction_type, original_construction_type)),
    orientation = recode(orientation, !!!setNames(english_orientation, original_orientation)),
    type = recode(type, !!!setNames(english_type, original_type))
  )

# Join with districts mapping data
joined_data <- joined_data %>%
  left_join(districts_mapping, join_by(name_nsi == municipality), keep = FALSE, multiple = "first")

# Create a copy of joined data without geometry information
joined_data_wo_geom <- joined_data
joined_data_wo_geom$geometry <- NULL

write.csv2(joined_data_wo_geom, "data/apartments_appraisal.csv", row.names = F)

EDA

Záver a nasledujúce kroky


EDA poskytla cenné poznatky, ktoré budú zohľadnené v predikčnom modeli:

  • Rozloženie cien je vychýlené doprava – ponuky drahých bytov sú obmedzené
  • Geopriestorové rozloženie má západ-východný gradient – nižšie ceny sú na východe a juhu, s výnimkou niekoľkých regionálnych centier.
  • Väčšina miest v datasete má pomerne vysokú úroveň indexu bývania. Vo všeobecnosti existuje pozitívny vzťah medzi jeho hodnotou a cenou.
  • Existujú preukázané rozdiely medzi cenami bytov s rôznymi stavmi. Nie je prekvapujúce, že nové byty majú najvyššie ceny.
  • Podobný efekt je pri energetickom certifikáte. Počet chýbajúcich údajov je v tomto prípade vysoký a budem ho riešiť imputáciou.
  • Veľká väčšina bytov v súbore má 2 a 3 izby. Cena rastie s rastúcou veľkostnou triedou. Zvýšenie ceny z 2 izbovej na 3 izbovú skupinu je v však priemere dosť nízke. Dva možné dôvody sú – dopyt po 2 izbových bytoch (keďže sú stále lacnejšie ako 3 izbové) a lokalita. Ak by sa väčšina 2-izbových bytov nachádzala na západe, ich cena by bola v priemere za celú krajinu vyššia v porovnaní s rovnomerným priestorovým rozložením.
Geocoding pomocou jazyka R 150 150 cleandata

Geocoding pomocou jazyka R

Geocoding je proces prevádzania adries na zemepisné súradnice, které sa môžu použiť pre umiestnenie týchto adries na mapu. Využitie takto obohatených dát je široké. Od analytiky po praktické použitie v oblastiach ako logistika, marketing, verejná správa, zdravotníctvo a mnoho iných.

geocode_sk

Knižnice pre geocoding

V dnešnom príspevku sa budem venovať procesu geokódovania inzerátov získaných webscrapingom (procesu som sa venoval v tomto článku) a následnému spojeniu týchto geokódovaných údajov s údajmi o obciach v SR. Tento proces otvára dvere k hlbšiemu porozumeniu vašich dát a umožňuje pokročilé geopriestorové analýzy.
Tentokrát si vystačíme s tromi knižnicami. S tidyverse na manipuláciu s dátami, tidygeocoder na samotný geocoding adries a sf pre prácu s priestorovými dátami (v našom prípade bodmi a polygónmi) sme pripravení načítať a spracovať naše dáta.

Knižnice
# import libraries

pacman::p_load(
  tidyverse, 
  tidygeocoder, 
  sf
)

Načítanie dát a geocoding

Načítame geopriestorové údaje o obciach a údaje o inzerátoch. Údaje o obciach sa dajú jednoducho získať pomocou knižnice giscoR a funkcie gisco_get_communes(). Ja som mal tieto dáta už pripravené vo formáte RDS.

Načítanie dát
# Read geospatial data for communes
communes <- readRDS("data/geospatial_data/communes.RDS") |> 
  select(NAME_NSI)

# Read advertisements data
advertisements_complete <- readRDS("data/advertisements_complete.RDS") 


Každý z inzerátov v datasete obsahuje adresu. Cieľom je extrahovať unikátne adresy z týchto reklám a premeniť ich na geopriestorové údaje. Geokódovanie vykonávam pomocou funkcie geocode() metódou OpenStreetMap. Tento proces môže trvať dlho, keďže každú sekundu sa odošle jedna požiadavka.

Geokódovanie adries
# Extract unique addresses from advertisements data
advertisements <- advertisements_complete |> 
  select(address) |> 
  rename(address1 = address) |> 
  as_tibble() |> 
  mutate(address1 = str_extract(address1, 
                                "^[^,]+,[^,]+")) |> 
  unique()

# Geocode addresses using OpenStreetMap method
geocoded_advertisements <- geocode(advertisements, 
                            address = address1, 
                            method = 'osm', 
                            lat = latitude , 
                            long = longitude)


Po geokódovaní adries ich spojím späť s údajmi z inzerátov. To umožní priradiť každému inzerátu geografickú polohu. V tejto fáze tiež aplikujem filter, aby som zabezpečil, že v analýze zohľadňujem iba reklamy v špecifickom geografickom rozsahu, ktorý odpovedá oblasti Slovenska. Občas sa totiž stalo, že adresa bola lokalizovaná v zahraničí.

Prepojenie inzerátov a geografických dát
# Join geocoded addresses with advertisements data
advertisements_complete_geocoded <- advertisements_complete |> 
  left_join(geocoded_advertisements, by = c("address" = "address1"), keep = FALSE, multiple = "first") |> 
  filter(latitude <= 49.613611 & latitude >= 47.75740 & longitude <= 22.565833 & longitude >= 16.833333) |>  
  st_as_sf(coords = c("longitude", "latitude"), crs = 4326)


Posledným krokom je priestorové spojenie geokódovaných reklám s dátami o obciach. Tento proces mi umožní zistiť, v ktorých obciach sa nachádzajú inzerované byty. Využívam k tomu knižnicu sf, ktorá je štandardom pre prácu s priestorovými dátami v R. Keďže už pracujem s priestorovými údajmi, musím použit špecifický typ spojenia st_join metódou st_contains, ktorý spája dve priestorové jednotky (body, polygóny) podľa toho, či jedna obsahuje druhú. Výsledný dataset integruje originálne informácie o inzerciách s ich geopriestorovými vlastnosťami a príslušnosťou k obciam.

Uloženie finálneho súboru
# Spatial join with communes data
advertisements_complete_geocoded <- communes %>% 
  st_join(advertisements_complete_geocoded, join = st_contains)

# Save the final geocoded advertisements data
saveRDS(advertisements_complete_geocoded, "data/advertisements_complete_geocoded.RDS")

Nasledujúce kroky


Geokódovanie a práca s geopriestorovými dátami otvárajú nové možnosti pre analýzu a vizualizáciu informácií. V tomto prípade som prešiel od jednoduchých adresných údajov k ich geografickej reprezentácii a integrácii s údajmi o obciach. Tento prístup umožňuje lepšie porozumieť distribúcii inzerovaných bytov na území Slovenska, čo je jednak dôležité pre nacenenie nehnuteľnosti a v praxi často kľúčové pre efektívne plánovanie marketingových a obchodných stratégií. Výhodou, pri tvorbe modelu strojového učenia (napr. algoritmy ako random forest, xgboost a iné) zas je, že súradnice môžeme rotovať a tým optimalizovať ako budú výsledné rozhodovacie stromy vyzerať.

Web scraping pomocou jazyka R 150 150 cleandata

Web scraping pomocou jazyka R

Ako použiť voľne dostupné dáta pre vlastnú analýzu? V tomto blogovom príspevku prejdem procesom zvaným web scraping. Vďaka nemu môžeme analyzovať voľne dostupné dáta z internetu.

webscraping-sk

Upozornenie


Predtým než sa dostanem k téme tohto blogu, upozorňujem, že tento článok slúži výhradne k informačným účelom a akékoľvek informácie uvedené nižšie nie sú právne rady. Z tohto dôvodu pred akýmkoľvek zbieraním údajov z webu by ste mali získať vhodnú profesionálnu právnu radu týkajúcu sa vášho konkrétneho prípadu.

Web scraping


V tomto blogovom príspevku prejdem procesom zvaným web scraping s využitím programovacieho jazyka R. Predtým než sa pustím do samotného procesu, chcel by som sa trochu venovať téme z trochu širšej prespektívny.
Web scraping je proces získavania obsahu alebo (väčšinou) štruktúrovaných údajov z webových stránok automatizovaným spôsobom (a obvykle vo veľkom množstve). Táto definícia prirodzene vyvoláva otázku o legalite takéhoto procesu. V zásade web scraping nie je ilegálny alebo zakázaný sám osebe (v EÚ, k júnu 2024). Avšak, používanie nástrojov na sťahovanie údajov je z právneho hľadiska riskantné z niekoľkých dôvodov:

  • Porušenie duševného vlastníctva
  • Porušenie zmluvy
  • Obavy o ochranu osobných údajov

Pre zminimalizovanie obáv by malo scrapovanie prebiehať diskrétne, rešpektovať podmienky používania webových stránok, v rámci procesu by ste mali kontrolovať, či stránky používajú protokol robots.txt na oznámenie, že scrapovanie je zakázané, vyhnúť sa scrapovaniu osobných údajov a, ak je to nevyhnutné, uistiť sa, že nedochádza k porušeniu GDPR, taktiež sa vyhnúť scrapovaniu súkromných alebo utajovaných informácií (Zdroj).
Existujú niektoré všeobecné etické zásady, ktoré by ste mali dodržiavať, keď chcete scrapovať údaje z webu. Najčastejšie spomínané sú:

  • Ak existuje verejné API, ktoré poskytuje požadované údaje, použite ho namiesto scrapovania.
  • Sťahujte údaje v rozumnom tempe, aby scrapovanie nebolo škodlivé pre server a nemohlo byť zamieňané za DDoS útok.
  • Rešpektujte duševné vlastníctvo iných. Použite údaje na vytvorenie nového hodnotného obsahu, nie na duplikovanie a predávanie ich ako vlastné alebo nelegálne predávanie.
  • Nepoužívajte scrapovanie osobných alebo súkromných údajov alebo dokumentov, rešpektujte GDPR.
  • Skontrolujte súbor robots.txt, aby ste zistili, ako by mal byť web prehľadávaný.
  • Zdieľajte to, čo môžete. Ak sú údaje, ktoré ste scrapovali, verejne dostupné, alebo ste získali povolenie na ich zdieľanie, zverejnite ich pre iných (napríklad na GitHub alebo Kaggle). Ak ste napísali webový scraper na prístup k nim, zdieľajte jeho kód.
  • Hľadajte spôsoby, ako vrátiť hodnotu webovým stránkam, ktoré scrapujete, napríklad odkazovaním na stránku v článku alebo príspevku, aby ste na ňu priviedli návštevníkov.
  • Ak sa jedná o súkromný projekt (ako tento), počkajte kým sa štruktúra stránky zmení a kód nie je možné použiť bez ďalšej úpravy.

Niekoľko článkov venujúcim sa téme môžete nájsť na towardsdatascience.com, Data Fluency github page alebo scrapingrobot page.
Príklad komerčného využitia web scrapingu je napr. Apify, ktoré ponúka množstvo produktov a riešení v oblasti web scrapingu.
V tomto článku budem scrapovať údaje zo stránky Nehnutelnosti, ktorá sa špecializuje na realitné inzeráty a služby. Pre dodržanie etických zásad scrapovania som prijal niekoľko preventívnych opatrení:

  • Táto konkrétna stránka používa protokol robots.txt. Nescrapujem žiadnu časť, ktorá je zakázaná a vo vybraných častiach kódu som pridal funkciu Sys.sleep(), aby som spomalil proces a žiadal údaje s primeranou frekvenciou.
  • Scrapujem len verejné údaje, ktoré potrebujem na ďaľšiu analýzu a vytvorenie ML modelu.
  • Výsledný dataset je dostupný na Kaggle.

    Robots.txt protokol stránky Nehnuteľnosti.sk

Proces scrapovania


Použité knižnice


Ako obvykle, začínam s načítaním balíkov potrebných pre tento projekt. Používam na to packman knižnicu a funkciu p_load(), ktorá ma dve výhody oproti základnej funkcii library():

  • ak potrebnú knižnicu nemám nainštalovanú, funkcia to rovno napraví
  • môžem načítať viacero knižníc naraz
Knižnice
if (!require("pacman")) {
  install.packages("pacman")
}
pacman::p_load(
  tidyverse,
  rvest, # scraping, part of tidyverse
  httr, # working with html
  RSelenium, # scraping in Google Chrome
  netstat, # free_port()
  doParallel, # parallel processing
  furrr # future map
)

V tomto projekte máme tri skupiny balíkov:

  • na načítanie údajov a manipuláciu s nimi – rio, tidyverse
  • na web scrapovanie – rvest, RSelenium, netstat, httr. Poznámka: V čase uverejnenia tohto príspevku bola dostupná už aj nová funkcia v knižnici rvest read_html_live(). Podľa popisu by mohla aspon čiastočne nahradiť RSelenium.
  • na paralelné spracovanie – doParallel a furrr

Webové elementy


Bez ohľadu na to, ktorý programovací jazyk alebo balík si vyberiete na scrapovanie, musíte byť schopní nájsť elementy v zdrojovom kóde webovej stránky. To môžete jednoducho urobiť vo vašom webovom prehliadači (pre potreby tohto blogu používam Google Chrome). Stlačte CRTL + SHIFT + I na otvorenie nástrojov vývojára. Teraz, keď sa pohybujete kurzorom po kóde v okne, dynamicky vám ukáže, ktorá časť stránky je s ňou spojená. Jednoduchším spôsobom, ako získať správnu referenciu na element (alebo inú časť stránky), je stlačiť CRTL + SHIFT + C a vybrať priamo na stránke požadovaný element.

DevTools v Google Chrome


Ak ste našli správny element, musíte skopírovať jeho CSS selektor alebo cestu XPath. Obe možnosti môžu byť použité ako argumenty v rvest a RSelenium.

Kopírovanie XPath web elementu


Teraz ste pripravení získať obsah zo stránky. Výsledok, ak ste všetko správne urobili, môže mať rôzne formy. Môže to byť jedna hodnota, reťazec, zoznam atď. Na základe toho buď presnejšie špecifikujete, ktorú časť obsahu potrebujete, alebo pracujete s výsledkom v R a používate funkcie na manipuláciu s dátami s cieľom získania požadovaných informácií.

Scraping časť I. – rvest


Jedným z najbežnejších balíkov na web scrapovanie v R je rvest. Poskytuje funkcie na prístup k verejnej webovej stránke a na vyhľadávanie špecifických prvkov pomocou selektorov CSS a XPath. Tento balík nespúšťa javascript, čo znamená, že načíta html stránky rýchlejšie, ale vynechá všetky prvky načítané javascriptom po pôvodnom načítaní stránky. Preto je tento balík dobrá voľba, ak scrapujete statické stránky.

V tomto príklade začínam vytvorením premennej pre zdrojovú URL adresu: https://www.nehnutelnosti.sk/slovensko/byty/predaj/?p[param1][from]=1000&p[param1][to]=&p[page]=

Následne skontrolujem počet stránok:

Number of pages with search results


A nakoniec pripravím a spustím multisession na získanie ceny, adresy, typu nehnuteľnosti a najdôležitejšie – odkazu na inzerát, ktorý bude použitý v ďalšej časti s balíkom RSelenium. Pridal som aj plan(sequential) na zastavenie multisession, ale musím priznať, že nie som tak znalý paralelného programovania v R, aby som úplne pochopil dôležitosť tohto kroku.

Scraping statických dát
# apartments page
site <- "https://www.nehnutelnosti.sk/slovensko/byty/predaj/?p[param1][from]=1000&p[param1][to]=&p[page]="

# scrape the number of pages
number_of_pages <- read_html(site) %>%
  html_nodes(xpath = '//*[@id="content"]/div[7]/div/div/div[1]/div[17]/div/div/ul/li[5]') %>%
  html_elements("a") %>%
  html_text(trim = TRUE) %>%
  as.numeric()

# create a cluster of worker processes (cores)
plan(multisession, workers = 6)

advertisements <- future_map_dfr(1:number_of_pages, function(i) {
  page_content <- read_html(paste0(site, i))
  
  price <- page_content %>%
    html_nodes(xpath = '//*[@class="advertisement-item--content__price col-auto pl-0 pl-md-3 pr-0 text-right mt-2 mt-md-0 align-self-end"]') %>%
    html_attr("data-adv-price")
  
  type_of_real_estate <- page_content %>%
    html_nodes(xpath = '//*[@class="advertisement-item--content__info"]') %>%
    html_text2()
  
  address <- page_content %>%
    html_nodes(xpath = '//*[@class="advertisement-item--content__info d-block text-truncate"]') %>%
    html_text2()
  
  link <- page_content %>%
    html_nodes(xpath = '//*[@class="mb-0 d-none d-md-block"]') %>%
    html_nodes("a") %>%
    html_attr("href")
  
  tibble(price = price, type_of_real_estate = type_of_real_estate, address = address, link = link)
})

plan(sequential)

Scraping časť II. – RSelenium


RSelenium poskytuje súbor R väzieb pre Selenium 2.0 WebDriver. Na rozdiel od rvest spúšťa skutočný webový prehliadač, takže načíta akýkoľvek javascript obsiahnutý na webovej stránke. S týmto balíkom budete schopní interagovať so stránkou, napríklad posúvať, klikať na tlačidlo, vyplňovať vstupné formuláre atď. Na druhej strane je použitie tohto balíka náročnejšie, vyžaduje inštalovaný jazyk Java vo vašom systéme, a ja som sa stretol s viacerými problémami, kým som ho správne spustil. Viac sa tejto téme budem venovať v jednom z budúcich blogov.
Začínam definovaním niekoľkých pomocných funkcií. Jedna je na neúspešnú navigáciu na stránku, k čomu obvykle dochádzalo, keď som čítal príliš veľa stránok a musel som vymazať históriu prehliadania. Niekedy táto funkcia spôsobila nekonečnú slučku, ktorá bežala niekoľko hodín, než som si to všimol (napríklad počas noci), ale aspoň kód nezhavaroval. Tiež som skúsil vytvoriť funkciu na vymazanie histórie v prípade neúspešnej navigácie, ale nefungovala, a som spokojný s týmto súčasným riešením. Druhá je na spracovanie chyby pri hľadaní elementu. V takýchto prípadoch táto funkcia vráti NA. Posledná je podobná predchádzajúcej, ale v tomto prípade vráti NA, ak sa element nenájde.
V skripte tiež používam funkciu tryCatch() na vrátenie NA v prípade chyby.

Definovanie funkcií
# Define a function that handles the errors in page load
# Delete all cookies from the last 24 hours
clearCookies <- function(remDr) {
  remDr$deleteAllCookies()
}

navigate_with_retry <- function(link, remDr) {
  success <- FALSE
  while (!success) {
    tryCatch(
      {
        remDr$navigate(link)
        Sys.sleep(5)
        success <- TRUE
      },
      error = function(e) {
        cat("Failed to navigate to", link, "- Retrying in 10 seconds...\n")
        clearCookies(remDr)
        Sys.sleep(10)
      }
    )
  }
}

# Define a wrapper function that handles the errors in element search
safe_find_element <- possibly(function(page, xpath) {
  page$findElement(using = "xpath", xpath)
}, NA)

# function to get text or return NA if element not found
get_text_or_na <- function(nodes) {
  tryCatch(
    {
      text <- nodes %>%
        html_text2() %>%
        as.character() # %>%
      # str_trim() %>%
      # str_squish()
      if (text == "") NA else text
    },
    error = function(e) {
      NA
    }
  )
}


Teraz rozdeľujem odkazy na inzeráty do 10 setov. Tento krok nie je nutný, pretože kód funguje, ale robím to v prípade neočakávanej chyby počas procesu scrapovania, aby som zachránil aspoň časť údajov. Tento krok som zaradil až po jednej veľmi zlej skúsenosti. Ďalšia časť scrapovania totiž trvá viac ako dva dni a môžete mi veriť, že nechcete stratiť celý pokrok kvôli výpadku WiFi. Taktiež vytváram prázdny dataframe so všetkými možnými stĺpcami, ktorý sa bude napĺňať dátami.

Rozdelenie dát do menších celkov
# Additional info from web

# number of splits
num_splits <- 10
split_size <- ceiling(nrow(advertisements) / num_splits)

# split the data frame into subsets
advertisments_list <- split(advertisements, rep(1:num_splits, each = split_size, length.out = nrow(advertisements)))

for (i in seq_along(advertisments_list)) {
  assign(paste0("advertisements_", i), advertisments_list[[i]])
}

# create empty dataframe outside of the loop to hold additional info
additional_info_df <- tibble(
  link = character(),
  info_text = character(),
  additional_characteristics = character(),
  index_of_living = character(),
  environment = character(),
  quality_of_living = character(),
  safety = character(),
  transport = character(),
  services = character(),
  relax = character(),
  info_details = character(),
  stringsAsFactors = FALSE
)


Až teraz prichádzam k samotnému scrapovaniu s použitím RSelenium. Používam len samotnú knižnicu RSelenium, existuje tiež možnosť použiť Docker na spustenie servera Selenium a pripojenie sa k tejto inštancii pomocou RSelenium, túto možnosť som však nepoužil.
Ako už bolo spomenuté, na jeho spustenie musím dodržať niekoľko krokov. Na nastavenie servera Selenium a prehliadača musíte použiť funkciu rsDriver() a volať $client na vytvorenie klienta. Funkcia rsDriver očakáva niekoľko argumentov:

  • browser – používam Chrome, takže argument je “chrome”,
  • chromever – tento argument je často zdroj chýb. Jeho riešenie popíšem v samostatnom BLOGU, ale v tomto prípade píšem verziu “119.0.6045.105”,
  • verbose – nastavujem FALSE,
  • port – port, na ktorom sa má spúšťať. Používam funkciu z balíka netstat free_port(random = TRUE) pre automatický výber voľného portu.

Následne skriptom otvorím prehliadač, maximalizujem okno, prejdem na stránku Nehnuteľnosti a prijmem súbory cookie. Potom je nutné manuálne prihlásenie, aby sa zobrazili hodnoty jednotlivých komponentov “indexu bývania”. Tento krok by sa dal zautomatizovať, avšak je to jednorazová aktivita, takže ju ponechávam takto. Keď je toto všetko hotové, skutočné scrapovanie prebieha v cykle. Nejdem do všetkých detailov, ale logika je dosť jednoduchá, skript:

  1. Prejde na odkaz v cykle.

  2. Posunie sa na (výšku stránky/10*4,2), aby sa spustil javascript na zobrazenie indexu bývania (táto hĺbka posuvu je založená na manuálnych testoch, napriek tomu musím nájsť inú metódu alebo spustiť viacero posunov, aby sa script skutočne spustil na všetkých načítaných stránkach).

  3. Počká 3 sekundy na vykonanie javascriptu a spomalenie procesu (aby sa zbytočne nezaťažovala stránka).

  4. Načíta obsah stránky.

  5. Ak sa stránka nedá načítať, pridá prázdny záznam do dataframe-u.

  6. Ak sa stránka dá načítať, prejde html stránky, scrapuje nasledujúce informácie a pridá nové riadky k dataframe-u:

    • info_text – celý text z inzerátu. Momentálne nie je používaný v ML modeli, ale plánujem použiť NLP na získanie kľúčových slov/tém a vytvorenie wordcloud v Shiny appke.
    • info_details – obsahuje 4 premenné a bude vyčistený v nasledujúcich krokoch. Premenné sú oddelené symbolom “”.
    • index_of_living – hodnota od 0 do 10, vypočítava ju slovenský startup City Performer. Zohľadňuje šesť kategórií: prostredie, kvalita bývania, bezpečnosť, doprava, služby a oddych.
    • additional_characteristics – obsahuje viacero premenných, v nasledujúcich krokoch vyberiem 12 z nich. Premenné sú oddelené symbolom "\n".
  7. Zatvorí klienta a zastaví server.


Scraping pomocou RSelenium
for (i in 1:10) {
  # get the current dataframe
  current_df <- get(paste0("advertisements_", i))

  # start the server
  rs_driver_object <- rsDriver(
    browser = "chrome",
    chromever = "119.0.6045.105",
    verbose = FALSE,
    port = free_port(random = TRUE)
  )

  # create a client object
  remDr <- rs_driver_object$client

  # open a browser
  remDr$open()
  remDr$maxWindowSize()
  
  # navigate to a website
  remDr$navigate("https://www.nehnutelnosti.sk/")
  Sys.sleep(5) # wait for 5 seconds

  # accept cookies
# switch to cookie iframe
remDr$switchToFrame(remDr$findElement(using = "xpath", '//*[@id="sp_message_iframe_920334"]'))
remDr$findElement(using = "xpath", '//*[@id="notice"]/div[2]/button')$clickElement()

# switch back to default frame
remDr$switchToFrame(NA)

# MANUAL LOG IN

  # loop through each link in the current dataframe
  for (link in current_df$link) {
    info_text <- NA
    additional_characteristics <- NA
    index_of_living <- NA
    environment <- NA
    quality_of_living <- NA
    safety <- NA
    transport <- NA
    services <- NA
    relax <- NA
    info_details <- NA

    navigate_with_retry(link, remDr)
    #remDr$executeScript("document.body.style.zoom = '50%';")
    height <- as.numeric(remDr$executeScript("return document.documentElement.scrollHeight"))/10*4.2 # Scroll to load index of living
    remDr$executeScript(paste("window.scrollTo(0, ", height, ");")) # scroll to living index
    
    
    Sys.sleep(1)
    page <- safe_find_element(remDr, '//*[@id="map-filter-container"]')

    if (is.na(page)) {
      new_row <- tibble(
        link = link,
        info_text = NA,
        additional_characteristics = NA,
        index_of_living = NA,
        environment = NA,
        quality_of_living = NA,
        safety = NA,
        transport = NA,
        services = NA,
        relax = NA,
        info_details = NA
      )

      # bind new row to additional info dataframe
      additional_info_df <- rbind(additional_info_df, new_row)
    } else {
      page_html <- page$getElementAttribute("outerHTML")
      page_html <- read_html(page_html[[1]])

      info_text <- page_html %>%
        html_nodes(xpath = '//*[contains(concat( " ", @class, " " ), concat( " ", "text-inner", " " ))]') %>%
        get_text_or_na()

      info_details <- page_html %>%
        html_nodes(xpath = '//*[@id="map-filter-container"]/div[2]/div/div[1]/div[2]/div[5]/ul') %>%
        html_text2()
      
      tryCatch(
        {
          index_of_living <- page_html %>%
            html_nodes(xpath = '//*[@id="totalCityperformerWrapper"]/div/p[1]/span') %>%
            get_text_or_na()
        },
        error = function(e) {
          index_of_living <- NA
        }
      )

      tryCatch(
        {
          environment <- page_html %>%
            html_nodes(xpath = '//*[@id="map-filter-container"]/div[2]/div/div[1]/div[4]/div[1]/div[1]/div/div[2]/div[2]/div[1]/div[1]/div[2]/span[1]/span') %>%
            get_text_or_na()
        },
        error = function(e) {
          environment <- NA
        }
      )
      
      tryCatch(
        {
          quality_of_living <- page_html %>%
            html_nodes(xpath = '//*[@id="map-filter-container"]/div[2]/div/div[1]/div[4]/div[1]/div[1]/div/div[2]/div[2]/div[1]/div[2]/div[2]/span[1]/span') %>%
            get_text_or_na()
        },
        error = function(e) {
          quality_of_living <- NA
        }
      )
      
      tryCatch(
        {
          safety <- page_html %>%
            html_nodes(xpath = '//*[@id="map-filter-container"]/div[2]/div/div[1]/div[4]/div[1]/div[1]/div/div[2]/div[2]/div[1]/div[3]/div[2]/span[1]/span') %>%
            get_text_or_na()
        },
        error = function(e) {
          safety <- NA
        }
      )
      
      tryCatch(
        {
          transport <- page_html %>%
            html_nodes(xpath = '//*[@id="map-filter-container"]/div[2]/div/div[1]/div[4]/div[1]/div[1]/div/div[2]/div[2]/div[2]/div[1]/div[2]/span[1]/span') %>%
            get_text_or_na()
        },
        error = function(e) {
          transport <- NA
        }
      )
      
      tryCatch(
        {
          services <- page_html %>%
            html_nodes(xpath = '//*[@id="map-filter-container"]/div[2]/div/div[1]/div[4]/div[1]/div[1]/div/div[2]/div[2]/div[2]/div[2]/div[2]/span[1]/span') %>%
            get_text_or_na()
        },
        error = function(e) {
          services <- NA
        }
      )
      
      tryCatch(
        {
          relax <- page_html %>%
            html_nodes(xpath = '//*[@id="map-filter-container"]/div[2]/div/div[1]/div[4]/div[1]/div[1]/div/div[2]/div[2]/div[2]/div[3]/div[2]/span[1]/span') %>%
            get_text_or_na()
        },
        error = function(e) {
          relax <- NA
        }
      )
      
     tryCatch(
        {
          additional_characteristics <- page_html %>%
            html_nodes(xpath = '//*[@id="additional-features-modal-button"]/ul') %>%
            html_text2() # %>%
          # str_squish()
        },
        error = function(e) {
          additional_characteristics <- NA
        }
      )

      new_row <- tibble(
        link = link, info_text = info_text,
        additional_characteristics = additional_characteristics,
        index_of_living = index_of_living,
        environment = environment,
        quality_of_living = quality_of_living,
        safety = safety,
        transport = transport,
        services = services,
        relax = relax,
        info_details = info_details
      )

      # bind new row to additional info dataframe
      additional_info_df <- rbind(additional_info_df, new_row)
    }
  }
}

# close remote driver
rs_driver_object$client$close()
rs_driver_object$server$stop()
rm(rs_driver_object, remDr)
gc()

Data wrangling


Scrapovanie je hotové a pokračujem s manipuláciou s dátami, aby som ich dal do (prvotnej) použiteľnej formy. Najprv upravujem adresu, aby som všetky okresy dostal do prvého stĺpca. Táto adresa sa neskôr používa na geokódovanie, ktoré opisujem v nasledujúcom blogu. Cena je taktiež upravená na číslo, aby sa mohla použiť v ML modeli.

Úprava dát 1
advertisements_cleaned <- advertisements %>%
  separate(type_of_real_estate, c("type", "area"), sep = " • ", remove = TRUE) %>% 
  separate(address, c("a", "b", "c"), sep = ", ", remove = TRUE) %>%
  unite("address", c(5, 4, 3), sep = ", ", na.rm = TRUE, remove = TRUE) %>% # reordering to keep all districts in first column
  mutate(
    price = str_replace_all(str_replace_all(price, " €", ""), " ", "") %>%
      as.integer(),
    address0 = address
  ) %>%
  separate(address0, c("district", "municipality", "street"), sep = ", ") %>%
  select(-street)


Teraz potrebujem rozdeliť hodnoty stĺpcov additional_characteristics a info_details na viac zmysluplných premenných. Preto som vytvoril dva zoznamy: characteristics1 a characteristics2. Každý z nich obsahuje názov premenných, ktoré chcem extrahovať. Používam tieto zoznamy na vytvorenie prázdneho dataframe-u, aby som sa uistil, že sú všetky stĺpce prítomné. Z additional_info_df vyberiem additional_characteristics a info_details a rozdelím hodnoty pomocou "\n" ako oddeľovača. Ďalej definujem dve funkcie: get_characteristics1 a get_characteristics2, ktoré tieto hodnoty mapujú na príslušné stĺpce. Nakoniec spájam output_df_characteristics1, output_df_characteristics2, vybrané stĺpce z additional_info_df a pripájam ich k advertisements_cleaned podľa linku inzerátu.

Úprava dát 2
# get additional information from scraped data
# First list of additional info details
characteristics1 <- c(
  "Stav", # condition
  "Úžit. plocha", # land area
  "Energie", # energy costs
  "Provízia zahrnutá v cene"
)

characteristics1_df <- data.frame(characteristics1, value = NA)
# Second list of additional info details
characteristics2 <- c(
  "Počet izieb/miestností", # number of rooms
  "Orientácia", # orientation
  "Rok výstavby", # built year
  "Rok poslednej rekonštrukcie", # year of last reconstruction
  "Energetický certifikát", # energy certificate
  "Počet nadzemných podlaží", # number of floors
  "Podlažie", # floor
  "Výťah", # lift
  "Typ konštrukcie", # construction type
  "Počet balkónov", # number of balconies
  "Počet lodžií", # number of loggias
  "Pivnica" # cellar
)

characteristics2_df <- data.frame(characteristics2, value = NA)

characteristics_wrangler <- additional_info_df %>%
  mutate(
    chars1_list = str_split(info_details, "\n"),
    chars2_list = str_split(additional_characteristics, "\n")
  ) %>%
  select(-additional_characteristics, -info_details)

get_characteristics1 <- function(x) {
  temp_df <- x %>%
    unlist() %>%
    as.data.frame()
  temp_df <- rename(temp_df, chars = .)
  temp_df <- temp_df %>%
    separate_wider_delim(chars,
      delim = ": ",
      names = c(
        "info",
        "status"
      )
    ) %>%
    filter(info %in% characteristics1) %>%
    full_join(characteristics1_df, join_by("info" == "characteristics1"), keep = FALSE) %>%
    select(-value) %>%
    pivot_wider(names_from = info, values_from = status)
  return(temp_df)
}

get_characteristics2 <- function(x) {
  temp_df <- x %>%
    unlist() %>%
    as.data.frame()
  temp_df <- rename(temp_df, chars = .)
  temp_df <- temp_df %>%
    separate_wider_delim(chars,
      delim = ": ",
      names = c(
        "info",
        "status"
      )
    ) %>%
    filter(info %in% characteristics2) %>%
    full_join(characteristics2_df, join_by("info" == "characteristics2"), keep = FALSE) %>%
    select(-value) %>%
    pivot_wider(names_from = info, values_from = status)
  return(temp_df)
}

# Apply get_characteristics1() and get_characteristics2() to each row in additional_info_df and combine the results
output_df_characteristics1 <- map_dfr(characteristics_wrangler$chars1_list, get_characteristics1)
output_df_characteristics2 <- map_dfr(characteristics_wrangler$chars2_list, get_characteristics2)

# Add the new columns to additional_info_df
additional_info_df_complete <- cbind(
  additional_info_df %>%
    mutate(index_of_living = str_replace_all(index_of_living, " /", "")) %>%
    select(c(link, 
             info_text, 
             index_of_living,
             environment,
             quality_of_living,
             safety,
             transport,
             services,
             relax)) %>% 
    mutate(flag = "x"), 
  output_df_characteristics1,
  output_df_characteristics2
)


advertisements_complete <- advertisements_cleaned %>%
  left_join(additional_info_df_complete, by = "link", multiple = "first") %>%
  filter(!is.na(flag)) %>% 
  select(-flag, -c, -type)


Posledný krok je uloženie dát vo formáte RDS.

Uloženie súborov
saveRDS(additional_info_df, "data/additional_info_df.RDS")
saveRDS(advertisements, "data/advertisements.RDS")

# create separate df for text analyses
text_long <- advertisements_complete$info_text

saveRDS(text_long, file = "data/text_long.rds")

saveRDS(advertisements_complete, file = "data/advertisements_complete.rds")
  • 1
  • 2