diff --git a/.github/workflows/DNA build.yml b/.github/workflows/DNA build.yml index c0f479eb..30cbea9e 100644 --- a/.github/workflows/DNA build.yml +++ b/.github/workflows/DNA build.yml @@ -1,6 +1,6 @@ name: build -on: [push, pull_request] +on: [push, pull_request, workflow_dispatch] jobs: Bibliography: @@ -33,7 +33,7 @@ jobs: run: pandoc --version - name: Setup Gradle - uses: gradle/gradle-build-action@v2 + uses: gradle/gradle-build-action@v2.4.2 - name: Execute Gradle build for bibliography run: ./gradlew :bibliography:build @@ -56,7 +56,7 @@ jobs: cache: gradle - name: Setup Gradle - uses: gradle/gradle-build-action@v2 + uses: gradle/gradle-build-action@v2.4.2 - name: Execute Gradle build for DNA run: ./gradlew :dna:build @@ -81,11 +81,16 @@ jobs: extra-packages: | any::ggplot2 any::roxygen2 + any::igraph + any::ggraph + any::askpass + any::cluster + any::sna - uses: gradle/wrapper-validation-action@v1 - name: Setup Gradle - uses: gradle/gradle-build-action@v2 + uses: gradle/gradle-build-action@v2.4.2 - name: Execute Gradle build for rDNA run: ./gradlew :rDNA:build diff --git a/.github/workflows/matrix-chat-message.yml b/.github/workflows/matrix-chat-message.yml new file mode 100644 index 00000000..264360fb --- /dev/null +++ b/.github/workflows/matrix-chat-message.yml @@ -0,0 +1,22 @@ +name: Matrix message + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + +jobs: + send-message: + runs-on: ubuntu-latest + name: Send message via Matrix + steps: + - name: Send message to DNA Matrix space development room + id: matrix-chat-message + uses: fadenb/matrix-chat-message@v0.0.6 + with: + homeserver: ${{ secrets.MATRIX_SERVER }} + token: ${{ secrets.MATRIX_TOKEN }} + channel: ${{ secrets.MATRIX_ROOM_ID }} + message: | + New [commit](https://github.com/leifeld/dna/commit/${{ github.sha }}) in the [dna](https://github.com/leifeld/dna/) GitHub repository (branch: ${{ github.ref_name }}). Commit message: ${{ github.event.head_commit.message }}. diff --git a/README.md b/README.md index 24746855..c4017835 100755 --- a/README.md +++ b/README.md @@ -12,13 +12,17 @@ The Java software Discourse Network Analyzer (DNA) is a qualitative content anal - The software comes with an R package called rDNA for remote controlling DNA and for further ways of analyzing the networks. +[DNA 3.0](https://github.com/leifeld/dna/releases) was first released on 12 June 2022. It constitutes a major rewrite from the previous version DNA 2.0 beta 25. DNA 3 comes with many new features and improvements. The [release](https://github.com/leifeld/dna/releases) page contains all the details (scroll to version 3.0.7 for the first DNA 3 release). + +If you require the latest (non-release) version of the DNA jar file from GitHub, you can clone the git repository to your computer and execute `./gradlew build` on your terminal or command line. This will build the jar file and store it in the `build/` directory of the cloned repository. Alternatively, you can try to download the latest artifact from the build process under [GitHub Actions](https://github.com/leifeld/dna/actions) by clicking on the latest build and scrolling down to "Artifacts". However, it is usually recommended to use the most recent [release](https://github.com/leifeld/dna/releases/) version. + [![DNA/rDNA build](https://github.com/leifeld/dna/actions/workflows/DNA%20build.yml/badge.svg)](https://github.com/leifeld/dna/actions/workflows/DNA%20build.yml) -## DNA 3.0: current development status +## rDNA 3.0: Connecting DNA to R -[DNA 3.0](https://github.com/leifeld/dna/releases) was first released on 12 June 2022. It constitutes a major rewrite from the previous version DNA 2.0 beta 25. DNA 3 comes with many new features and improvements. The [release](https://github.com/leifeld/dna/releases) page contains all the details (scroll to version 3.0.7 for the first DNA 3 release). +The R package rDNA connects DNA to R for data exchange and analysis. -Please note that the R package rDNA does not have the full functionality of the old 2.0 version yet. It can create networks, but please use the old DNA 2.0 beta 25 for now if you require more complex data management and analysis functionality in R. It is possible to import DNA 2 data into DNA 3 at any point. New R functions will be added in the future. +Please note that the current version 3.0 does not have the full functionality of the old 2.0 version yet. It can create networks, but please use the old version for now if you require more complex data management and analysis functionality in R. It is possible to import DNA 2 data into DNA 3 at any point (but not the other way around). New R functions will be added in the future. To install the new rDNA 3 directly from GitHub, try the following code in R: @@ -30,9 +34,24 @@ remotes::install_github("leifeld/dna/rDNA/rDNA@*release", Note that the package relies on `rJava`, which needs to be installed first. -If you require the latest (non-release) version of the DNA jar file from GitHub, you can clone the git repository to your computer and execute `./gradlew build` on your terminal or command line. This will build the jar file and store it in the directory `dna/build/libs/` of the cloned repository. Alternatively, you can try to download the latest artifact from the build process under [GitHub Actions](https://github.com/leifeld/dna/actions) by clicking on the latest build and scrolling down to "Artifacts". However, it is usually recommended to use the most recent [release](https://github.com/leifeld/dna/releases/) version. +## Installation of the old rDNA 2.1.18 +For data management, you may still want to use the old rDNA 2.1.18 with DNA 2.0 beta 25. You can install the package directly from GitHub as well. However, you will need to download the correct JAR file and store it either in your working directory or (recommended) in the library path of the installed R package in the "extdata" subdirectory. The following code can do this for you: +```r +# install.packages("remotes") +remotes::install_github("leifeld/dna/rDNA@v2.0-beta.25", + INSTALL_opts = "--no-multiarch") -## Documentation +# find out where to store the JAR file +dest <- paste0(dirname(system.file(".", package = "rDNA")), + "/extdata/dna-2.0-beta25.jar") + +# download JAR file and store in library path +u <- "https://github.com/leifeld/dna/releases/download/v2.0-beta.25/dna-2.0-beta25.jar" +download.file(url = u, destfile = dest, mode = "wb") + +``` + +## Documentation and community - This **tutorial on YouTube** describes installation of DNA, basic data coding, network export, and network analysis using visone. The video clip is 18 minutes long. @@ -46,6 +65,19 @@ If you require the latest (non-release) version of the DNA jar file from GitHub, - If you have questions or want to report bugs, please create an issue in the [issue tracker](https://github.com/leifeld/dna/issues). +- Join the the DNA community on [Matrix](https://matrix.to/#/#dna:yatrix.org). Matrix is a chat protocol. It's similar to Slack, Discord, or WhatsApp, but without the corporate shackles. It's free, open-source, decentralised, and secure. We have set up a public space called [#dna:yatrix.org](https://matrix.to/#/#dna:yatrix.org) with separate chat rooms for installation, research, and development. It's really easy to join: You first create an account on one of the many Matrix [servers](https://joinmatrix.org/servers/) (we use and recommend [yatrix.org](https://element.yatrix.org/)), then download one of the many Matrix [clients](https://matrix.org/ecosystem/clients/) on your phone, computer, or the web (e.g., Element) to use the account with, and finally join [#dna:yatrix.org](https://matrix.to/#/#dna:yatrix.org). To simplify the process, you can just click on [this invitation link](https://matrix.to/#/#dna:yatrix.org) for some sensible default choices. Make sure you join all four public rooms (you can mute their notifications as needed) and look at the rules in the #dna-welcome room upon arrival. + ## Support the project -Please consider contributing to the project by telling other people about the software, citing our underlying [research](https://www.philipleifeld.com/publications) in your publications, reporting or fixing [issues](https://github.com/leifeld/issues), or starting pull requests. +Please consider contributing to the project by: +- telling other people about the software, +- citing our underlying [research](https://www.philipleifeld.com/publications) in your publications, +- reporting or fixing [issues](https://github.com/leifeld/issues), or +- starting pull requests to contribute bug fixes or new functionality. + +Some suggestions of new functionality you could add via pull requests: +- Import filters for loading data from Nvivo, MaxQDA, and other software into DNA. +- Export filters for exporting networks to Gephi and other network analysis software. +- Analysis functions or unit tests for the rDNA package. +- Publications for the bibliography. +- Bug fixes. diff --git a/bibliography/bibliography.bib b/bibliography/bibliography.bib index 6bd6aeae..b22939fa 100644 --- a/bibliography/bibliography.bib +++ b/bibliography/bibliography.bib @@ -1,3 +1,12 @@ +@mastersthesis{absi2023hope, + title={Hope or Hype? {T}he Framing of Hydrogen Technology in {E}uropean Media}, + author={Absi, Abrar}, + year={2023}, + type={Master's Thesis}, + school={Radboud University Nijmegen, Nijmegen School of Management}, + url={https://theses.ubn.ru.nl/handle/123456789/15084} +} + @article{abzianidze2020us, title={Us vs. Them as Structural Equivalence: Analysing Nationalist Discourse Networks in the {G}eorgian Print Media}, author={Abzianidze, Nino}, @@ -9,6 +18,17 @@ @article{abzianidze2020us doi={10.17645/pag.v8i2.2605} } +@article{al2023sentiment, + title={Sentiment of {I}ndovac Vaccine Launch News on Detik.com Using Discourse Network Analysis}, + author={Al Khazim, Iqbal and Kani, Kayla Nur Alfia and Aurora, Olly}, + journal={Journal of Digital Media Communication}, + volume={2}, + number={1}, + pages={37--50}, + year={2023}, + doi={10.35760/dimedcom.2023.v2i1.8278} +} + @incollection{almiron2021think, title={Think Tanks Neoliberales y Falsos Debates: La Propuesta del Impuesto a la Carne para Combatir la Crisis Clim{\'a}tica}, author={Almiron, N{\'u}ria and Moreno, Jose A.}, @@ -22,6 +42,17 @@ @incollection{almiron2021think url={https://www.torrossa.com/en/resources/an/5093569#} } +@article{anshori2023who, + author={Anshori, Mahfud and Pawito and Kartono, Drajat Tri and Hastjarjo, Sri}, + title={Who Says What? {T}he Role of the Actor's Political Position in Ideograph Construction}, + journal={Jurnal Komunikasi: Malaysian Journal of Communication}, + year={2023}, + volume={39}, + number={2}, + pages={354--372}, + doi={10.17576/jkmjc-2023-3902-20} +} + @article{aryal2022navigating, title={Navigating Policy Debates of and Discourse Coalitions on {N}epal's Scientific Forest Management}, author={Aryal, Kishor and Laudari, Hari Krishna and Maraseni, Tek and Pathak, Bhoj Raj}, @@ -332,6 +363,15 @@ @mastersthesis{coronado2015reforma url={https://repositorio.uchile.cl/handle/2250/137806} } +@article{cross2023speaking, + author={Cross, James P. and Greene, Derek and Umansky, Natalia and Cal\`{o}, Silvia}, + title={Speaking in Unison? {E}xplaining the Role of Agenda-Setter Constellations in the {ECB} Policy Agenda Using a Network-Based Approach}, + journal={Journal of European Public Policy}, + year={2023}, + doi={10.1080/13501763.2023.2242891}, + note={Forthcoming} +} + @incollection{dalimunthe2022depicting, title={Depicting Mangrove's Potential as Blue Carbon Champion in Indonesia}, author={Dalimunthe, Syarifah Aini and Putri, Intan Adhi Perdana and Prasojo, Ari Purwanto Sarwo}, @@ -473,8 +513,19 @@ @article{edvra2023tipologi doi={10.38194/jurkom.v6i1.698} } +@article{elislah2023discourse, + author={Elislah, Neli}, + title={Discourse Network Analysis on Delaying Elections in {P}resident {J}oko {W}idodo's Era}, + journal={Jurnal Aspikom}, + volume={8}, + number={2}, + year={2023}, + pages={225--240}, + doi={10.24329/aspikom.v8i2.1255} +} + @article{eriyanto2020discourse, - title={Discourse Network of a Public Issue Debate: A Study on Covid-19 Cases in {I}ndonesia}, + title={Discourse Network of a Public Issue Debate: A Study on {C}ovid-19 Cases in {I}ndonesia}, author={Eriyanto and Ali, Denny Januar}, journal={Jurnal Komunikasi: {M}alaysian Journal of Communication}, pages={209--227}, @@ -622,9 +673,10 @@ @article{ghinoi2021local title={Local Policymakers' Attitudes towards Climate Change: A Multi-Method Case Study}, author={Ghinoi, Stefano and De Vita, Riccardo and Silvestri, Francesco}, journal={Social Networks}, - year={2021}, + year={2023}, publisher={Elsevier}, - note={Forthcoming}, + volume={25}, + pages={197--209}, doi={10.1016/j.socnet.2021.09.001} } @@ -682,6 +734,14 @@ @article{gkiouzepas2017climate doi={10.1080/17524032.2015.1047888} } +@mastersthesis{grunwald2023roadblocks, + title={Roadblocks of Polarization: {M}echanisms of Cultural Resistance to a Speed Limit on {G}erman Highways}, + author={Gr{\"u}nwald, Lotte}, + year={2023}, + school={Universiteit Utrecht, Copernicus Institute of Sustainable Development}, + doi={20.500.12932/44291} +} + @incollection{gupta2022discourse, title={Discourse Network Analysis of Nuclear Narratives}, author={Gupta, Kuhika and Ripberger, Joseph and Fox, Andrew and Jenkins-Smith, Hank C. and Silva, Carol}, @@ -715,6 +775,17 @@ @phdthesis{gutierrez2022redes doi={11651/5321} } +@article{hamanduna2023discourse, + author={Hamanduna, Antonius O. Lapu and Widjanarko, Putut}, + title={Discourse Network on the Revision of {I}ndonesian Information and Electronic Transaction Law}, + journal={Jurnal Studi Komunikasi}, + volume={7}, + number={2}, + pages={519--538}, + year={2023}, + doi={10.25139/jsk.v7i2.5496} +} + @phdthesis{hanschmann2019stalling, author={Hanschmann, Raffael}, title={Stalling the Engine? {EU} Climate Politics after the ``Great Recession''. {I}nvestigating the Impact of Economic Shocks on {EU} Climate Policy-Making in Three Case Studies}, @@ -787,10 +858,11 @@ @article{haunss2022multimodal title={Multimodal Mechanisms of Political Discourse Dynamics and the Case of {G}ermany's Nuclear Energy Phase-Out}, author={Haunss, Sebastian and Hollway, James}, journal={Network Science}, - pages={1--19}, - year={2022}, + pages={205--223}, + year={2023}, + volume={11}, + number={2}, publisher={Cambridge University Press}, - note={Forthcoming}, doi={10.1017/nws.2022.31} } @@ -974,6 +1046,15 @@ @article{howe2020media doi={10.17645/pag.v8i2.2595} } +@mastersthesis{hullmann2023case, + title={Case Study on the {G}erman Discourse of Industry Decarbonization}, + author={Hullmann, Charlotte}, + year={2023}, + type={Master's Thesis}, + school={Radboud University Nijmegen, Nijmegen School of Management}, + url={https://theses.ubn.ru.nl/handle/123456789/15079} +} + @article{hurka2013framing, title={Framing and Policy Change after Shooting Rampages: A Comparative Analysis of Discourse Networks}, author={Hurka, Steffen and Nebel, Kerstin}, @@ -1041,9 +1122,11 @@ @article{joshi2022fair author={Joshi, Bhavna and Swarnakar, Pradip}, journal={Environmental Sociology}, pages={1--14}, - year={2022}, + year={2023}, publisher={Taylor \& Francis}, - note={Forthoming}, + volume={9}, + number={2}, + pages={176--189}, doi={10.1080/23251042.2022.2151398} } @@ -1084,9 +1167,10 @@ @article{kammerer2021actors title={Actors and Issues in Climate Change Policy: The Maturation of a Policy Discourse in the National and International Context}, author={Kammerer, Marlene and Ingold, Karin}, journal={Social Networks}, - year={2021}, + year={2023}, publisher={Elsevier}, - note={Forthcoming}, + volume={75}, + pages={65--77}, doi={10.1016/j.socnet.2021.08.005} } @@ -1101,6 +1185,27 @@ @article{kasih2023pertarungan doi={10.52423/jikuho.v8i1.15} } +@mastersthesis{keller2023analysis, + title={Analysis of the Media Discourse about Meat and Meat Substitutes in {U.S.} Media between 2016 and 2021}, + author={Keller, Senta}, + year={2023}, + type={Master's Thesis}, + school={University of Bern, Faculty of Science, Oeschger Centre for Climate Change Research}, + url={https://occrdata.unibe.ch/students/theses/msc/394.pdf} +} + +@incollection{kenis2019analyzing, + title={Analyzing Policy-Making {II}: Policy Network Analysis}, + author={Kenis, Patrick and Schneider, Volker}, + year={2019}, + booktitle={The {P}algrave Handbook of Methods for Media Policy Research}, + editor={Van den Bulck, Hilde and Puppis, Manuel and Donders, Karen and Van Audenhove, Leo}, + pages={471--491}, + publisher={Palgrave Macmillan}, + address={Cham}, + doi={10.1007/978-3-030-16065-4_27} +} + @article{khatami2022discourse, title={Discourse Network Analysis (DNA): Aktivisme Digital dalam Perdebatan Isu ``Presiden Tiga Periode'' di {T}witter}, author={Khatami, Muhammad Iqbal}, @@ -1154,6 +1259,18 @@ @mastersthesis{kooistra2022space doi={20.500.12932/42903} } +@article{koop2023animals, + title={Animals and Climate Change: A Visual and Discourse Network Analysis of {I}nstagram Posts}, + author={Koop-Monteiro, Yasmin and Stoddart, Mark C. J. and Tindall, David B.}, + journal={Environmental Sociology}, + year={2023}, + publisher={Taylor \& Francis}, + volume={9}, + number={4}, + pages={409--426}, + doi={10.1080/23251042.2023.2216371} +} + @article{kukkonen2021actors, title={Actors and Justifications in Media Debates on {A}rctic Climate Change in {F}inland and {C}anada: A Network Approach}, author={Kukkonen, Anna and Stoddart, Mark C. J. and Yl{\"a}-Anttila, Tuomas}, @@ -1175,6 +1292,15 @@ @phdthesis{kukkonen2018discourse doi={10138/257508} } +@article{kukkonen2023cultural, + author={Kukkonen, Anna and Malkam{\"a}ki, Arttu}, + title={A Cultural Approach to Politicization of Science: {H}ow the Forestry Coalition Challenged the Scientific Consensus in the {F}innish News Media Debate on Increased Logging}, + journal={Society \& Natural Resources}, + year={2023}, + doi={10.1080/08941920.2023.2259326}, + note={Forthcoming} +} + @article{kukkonen2020science, title={The Science--Policy Interface as a Discourse Network: {F}inland’s Climate Change Policy 2002--2015}, author={Kukkonen, Anna and Yl{\"a}-Anttila, Tuomas}, @@ -1418,6 +1544,13 @@ @phdthesis{malkamaki2019human doi={10138/306940} } +@misc{malkamaki2023complex, + title={Complex Coalitions: {P}olitical Alliances across Relational Contexts}, + author={Malkam{\"a}ki, Arttu and Chen, Ted Hsuan Yun and Gronow, Antti and Kivelä, Mikko and Vesa, Juho and Yl{\"a}-Anttila, Tuomas}, + year={2023}, + howpublished={arXiv:2308.14422}, + doi={10.48550/arXiv.2308.14422} +} @article{malkamaki2021acoustics, title={On the Acoustics of Policy Learning: Can Co-Participation in Policy Forums Break Up Echo Chambers?}, author={Malkam{\"a}ki, Arttu and Wagner, Paul M. and Brockhaus, Maria and Toppinen, Anne and Yl{\"a}-Anttila, Tuomas}, @@ -1503,6 +1636,16 @@ @mastersthesis{mcdonald2019energy url={https://is.muni.cz/th/gqxcz/} } +@article{mijailoff2023fixing, + title={Fixing the Meaning of Floating Signifier: {D}iscourses and Network Analysis in the Bioeconomy Policy Processes in {A}rgentina and {U}ruguay}, + author={Mijailoff, Juli{\'a}n Daniel and Burns, Sarah Lilian}, + journal={Forest Policy and Economics}, + volume={154}, + pages={103039}, + year={2023}, + doi={10.1016/j.forpol.2023.103039} +} + @mastersthesis{miles2020changes, title={Changes in Social Networks and Narratives Associated with {L}ake {E}rie Water Quality Management after the 2014 {T}oledo Water Crisis}, author={Miles, Austin}, @@ -1548,10 +1691,11 @@ @article{mock2022relational title={Relational Coupling of Multiple Streams: The Case of {C}OVID-19 Infections in {G}erman Abattoirs}, author={M{\"o}ck, Malte and Vogeler, Colette S. and Bandelow, Nils C. and Hornung, Johanna}, journal={Policy Studies Journal}, - year={2022}, - publisher={Wiley Online Library}, - doi={10.1111/psj.12459}, - note={Forthcoming} + year={2023}, + volume={51}, + number={2}, + pages={351--374}, + doi={10.1111/psj.12459} } @article{morton2020feminism, @@ -1835,6 +1979,17 @@ @incollection{piereder2022ideology doi={10.4324/9781003026754-19} } +@article{pop2023microdosing, + title={Microdosing Psychedelics: {T}he Segregation of Spiritual and Scientific Narratives within the r/microdosing Online Community}, + author={Pop, Ioana and Gielens, Erwin and Kottmann, Hannah}, + journal={Journal of Psychedelic Studies}, + volume={7}, + number={2}, + pages={119--128}, + year={2023}, + doi={10.1556/2054.2023.00014} +} + @mastersthesis{pospivsilova2022liberty, title={Liberty, Equality, Hydrogen? {D}iscourse Network Analysis of {F}rench Hydrogen Politics}, author={Posp{\'\i}{\v{s}}ilov{\'a}, Tereza}, @@ -1855,6 +2010,17 @@ @article{pratama21discourse url={https://www.neliti.com/publications/223278/} } +@article{pratiwi2023discourse, + title={Discourse Network Analysis pada Stakeholder dan Integrated Value Creation dalam {CSR} {B}ank {M}andiri}, + author={Pratiwi, Monica and Murtiningsih, Bertha Sri Eko and Juliadi, Rismi}, + journal={Jurnal Komunikasi Profesional}, + volume={7}, + number={2}, + pages={256--274}, + year={2023}, + doi={10.25139/jkp.v7i2.5998} +} + @article{rantala2014multistakeholder, title={Multistakeholder Environmental Governance in Action: {REDD}+ Discourse Coalitions in {T}anzania}, author={Rantala, Salla and Di Gregorio, Monica}, @@ -1971,8 +2137,10 @@ @article{rone2022instrumentalising author={Rone, Julia}, journal={The British Journal of Politics and International Relations}, note={Forthcoming}, - year={2022}, - publisher={SAGE Publications Sage UK: London, England}, + year={2023}, + volume={25}, + number={3}, + pages={444--461}, doi={10.1177/13691481221089136} } @@ -2149,7 +2317,9 @@ @article{sconfienza2023discourse journal={French Politics}, year={2023}, publisher={Springer}, - note={Forthcoming}, + volume={21}, + number={2}, + pages={195--221}, doi={10.1057/s41253-023-00215-2} } @@ -2225,6 +2395,58 @@ @article{siagian2020mencari url={https://journal.ugm.ac.id/jkki/article/view/55475/27989} } +@article{sick2023rhetoric, + title={From Rhetoric to Regulation: {I}nferring Lobbying Influence on {EU} Efforts to Regulate {CO2} Emissions of Cars using Network Analysis}, + author={Sick, Harald}, + journal={Interest Groups \& Advocacy}, + year={2023}, + publisher={Springer}, + doi={10.1057/s41309-023-00195-2}, + note={Forthcoming} +} + +@article{silalahi2023analisis, + author={Silalahi, Evvy}, + title={Analisis Jaringan Wacana pada Pembentukan {UUTPKS} di Media Daring}, + journal={Jurnal Riset Komunikasi (JURKOM)}, + volume={6}, + number={2}, + pages={34--49}, + year={2023}, + doi={10.38194/jurkom.v6i2.812} +} + +@article{sofura2023discourse, + title={Discourse Network Analysis: {S}tudi Kasus pada Kebijakan Kenaikan Harga {B}ahan {B}akar {M}inyak (BBM) Pertamina}, + author={Sofura, Annisyu Mafa}, + journal={Kommunikatif}, + volume={12}, + number={1}, + pages={37--50}, + year={2023}, + doi={10.33508/jk.v12i1.4526} +} + +@article{sohn2023impact, + title={The Impact of Rebordering on Cross-Border Cooperation Actors' Discourses in the {\"O}resund Region. {A} Semantic Network Approach}, + author={Sohn, Christophe}, + journal={Geografiska Annaler: Series B, Human Geography}, + year={2023}, + doi={10.1080/04353684.2023.2266436}, + note={Forthcoming} +} + +@article{soraya2023jaringan, + title={Jaringan Wacana Isu Publik: Studi {DNA} pada Isu {ASN} Terpapar Radikalisme}, + author={Soraya, Rachmi}, + journal={Jurnal Interaksi: Jurnal Ilmu Komunikasi}, + volume={7}, + number={2}, + pages={130--145}, + year={2023}, + doi={10.30596/ji.v7i2.13161} +} + @inproceedings{stancioff2016locality, title={Locality and Landscape Change: Cultural Values and Social-Ecological Resiliency in the {K}alinago Territory}, author={Stancioff, Charlotte Eloise}, @@ -2245,6 +2467,16 @@ @mastersthesis{starke2016generating doi={20.500.12932/25449} } +@article{starke2023green, + title={`Green future' versus `Planetary Boundaries'? {E}volving Online Discourse Coalitions in {E}uropean Bioeconomy Conflicts}, + author={Starke, Jan R. and Metze, Tamara A. P. and Candel, Jeroen J. L. and Dewulf, Art R. P. J. and Termeer, Katrien J. A. M.}, + journal={Journal of Cleaner Production}, + pages={139058}, + volume={425}, + year={2023}, + doi={10.1016/j.jclepro.2023.139058} +} + @article{steinfeld2016f, title={The {F}-Campaign: A Discourse Network Analysis of Party Leaders' Campaign Statements on {F}acebook}, author={Steinfeld, Nili}, @@ -2356,6 +2588,17 @@ @article{stoddart2022roles doi={10.1111/ropr.12510} } +@article{sumirat2023koalisi, + author={Sumirat, Panji Arief and Eriyanto, Eriyanto}, + title={Koalisi Wacana dalam Debat Pemekaran {P}apua: {A}nalisis Jaringan Wacana Debat Pemekaran Tiga Provinsi {B}aru di {P}apua}, + journal={Jurnal Riset Komunikasi (JURKOM)}, + volume={6}, + number={2}, + pages={1--16}, + year={2023}, + doi={10.38194/jurkom.v6i2.739} +} + @article{swacha2022europeanization, title={The {E}uropeanization of {P}olish Climate Policy}, author={Swacha, Piotr and Karaczun, Zbigniew M. and Murawska, Daria}, @@ -2409,6 +2652,16 @@ @article{swinkels2022deciding doi={10.1002/epa2.1137} } +@inproceedings{Syafrudin2023, + author={Syafrudin, Muhammad and Sarwono and Hakim, Abdul and Solimun}, + title={Examine the Elements that Impact Food Security}, + booktitle={Proceedings of the Fifth Annual International Conference on Business and Public Administration (AICoBPA 2022)}, + publisher={Atlantis Press}, + pages={563--581}, + year={2023}, + doi={10.2991/978-2-38476-090-9_45} +} + @mastersthesis{taranger2020institutionalisation, title={The Institutionalisation of Climate Justice in the Global Governance Architecture}, author={Taranger, Karianne K.}, @@ -2742,6 +2995,15 @@ @article{wang2020understanding doi={10.14507/epaa.28.4451} } +@article{wesche2023influence, + title={The Influence of Visions on Cooperation among Interest Organizations in Fragmented Socio-Technical Systems}, + author={Wesche, J. P. and Negro, S. O. and Brugger, H. I. and Eichhammer, W. and Hekkert, M. P.}, + journal={Environmental Policy and Governance}, + year={2023}, + doi={10.1002/eet.2070}, + note={Forthcoming} +} + @article{westenberger2022soders, title={S{\"o}ders {\"O}kofeuerwerk und die Gr{\"u}nf{\"a}rbung der {CSU}: {D}iskursnetzwerke im bayrischen {T}hemenwettbewerb}, author={Westenberger, Gina-Julia and Schneider, Volker}, diff --git a/build/bibliography.md b/build/bibliography.md index 1cf27ab7..388c0163 100644 --- a/build/bibliography.md +++ b/build/bibliography.md @@ -4,7 +4,7 @@ author: bibliography: - bibliography.bib csl: apa-numeric-superscript-brackets.csl -date: 2023-05-13 +date: 2023-10-15 title: "Discourse Network Analysis: Bibliography" --- @@ -23,18 +23,36 @@ before contributing any citation entries.
+
+ +1\. Absi, A. (2023). *Hope or hype? The framing of hydrogen technology +in European media* \[Master's Thesis, Radboud University Nijmegen, +Nijmegen School of Management\]. + + +
+
-1\. Abzianidze, N. (2020). Us vs. Them as structural equivalence: +2\. Abzianidze, N. (2020). Us vs. Them as structural equivalence: Analysing nationalist discourse networks in the Georgian print media. *Politics and Governance*, *8*(2), 243–256.
+
+ +3\. Al Khazim, I., Kani, K. N. A., & Aurora, O. (2023). Sentiment of +Indovac vaccine launch news on detik.com using discourse network +analysis. *Journal of Digital Media Communication*, *2*(1), 37–50. + + +
+
-2\. Almiron, N., & Moreno, J. A. (2021). Think tanks neoliberales y +4\. Almiron, N., & Moreno, J. A. (2021). Think tanks neoliberales y falsos debates: La propuesta del impuesto a la carne para combatir la crisis climática. In D. Rodrigo-Cano, R. Mancinas Chávez, & R. Fernández Rial (Eds.), *La comunicación del cambio climático, una herramienta ante @@ -43,9 +61,18 @@ el gran desafío* (pp. 224–251). Dykinson.
+
+ +5\. Anshori, M., Pawito, Kartono, D. T., & Hastjarjo, S. (2023). Who +says what? The role of the actor’s political position in ideograph +construction. *Jurnal Komunikasi: Malaysian Journal of Communication*, +*39*(2), 354–372. + +
+
-3\. Aryal, K., Laudari, H. K., Maraseni, T., & Pathak, B. R. (2022). +6\. Aryal, K., Laudari, H. K., Maraseni, T., & Pathak, B. R. (2022). Navigating policy debates of and discourse coalitions on Nepal’s scientific forest management. *Forest Policy and Economics*, *141*, 102768. @@ -54,7 +81,7 @@ scientific forest management. *Forest Policy and Economics*, *141*,
-4\. Bandau, S. L. (2021). *Emerging institutions in the global space +7\. Bandau, S. L. (2021). *Emerging institutions in the global space sector. An institutional logics approach* \[Master's Thesis, Utrecht University, Copernicus Institute of Sustainable Development\]. @@ -63,7 +90,7 @@ University, Copernicus Institute of Sustainable Development\].
-5\. Bandelow, N. C., & Hornung, J. (2019). One discourse to rule them +8\. Bandelow, N. C., & Hornung, J. (2019). One discourse to rule them all? Narrating the agenda for labor market policies in France and Germany. *Policy and Society*, *38*(3), 408–428. @@ -72,7 +99,7 @@ Germany. *Policy and Society*, *38*(3), 408–428.
-6\. Barnickel, C. (2019). *Postdemokratisierung der +9\. Barnickel, C. (2019). *Postdemokratisierung der Legitimationspolitik: Diskursnetzwerke in bundesdeutschen Großen Regierungserklärungen und Aussprachen 1949–2014*. Springer VS. @@ -81,7 +108,7 @@ Regierungserklärungen und Aussprachen 1949–2014*. Springer VS.
-7\. Barnickel, C. (2020). Vorstellungen legitimen Regierens: +10\. Barnickel, C. (2020). Vorstellungen legitimen Regierens: Legitimationspolitik in der Großen Regierungserklärung der 19. Wahlperiode im Vergleich. *Zeitschrift für Vergleichende Politikwissenschaft*, *14*(3), 199–223. @@ -91,7 +118,7 @@ Politikwissenschaft*, *14*(3), 199–223.
-8\. Baulenas, E. (2021). She’s a rainbow: Forest and water policy and +11\. Baulenas, E. (2021). She’s a rainbow: Forest and water policy and management integration in Germany, Spain and Sweden. *Land Use Policy*, *101*, 105182. @@ -99,16 +126,17 @@ management integration in Germany, Spain and Sweden. *Land Use Policy*,
-9\. Belova, A. (2021). *Another silver bullet for the energy transition? -Discourse network analysis of German hydrogen debate* \[Master’s thesis, -Department of International Relations; European Studies, Energy Policy -Studies, Masaryk University\]. +12\. Belova, A. (2021). *Another silver bullet for the energy +transition? Discourse network analysis of German hydrogen debate* +\[Master’s thesis, Department of International Relations; European +Studies, Energy Policy Studies, Masaryk University\]. +
-10\. Belova, A., Quittkat, C., Lehotskỳ, L., Knodt, M., Osička, J., & +13\. Belova, A., Quittkat, C., Lehotskỳ, L., Knodt, M., Osička, J., & Kemmerzell, J. (2023). The more the merrier? Actors and ideas in the evolution of German hydrogen policy discourse. *Energy Research & Social Science*, *97*, 102965. @@ -117,7 +145,7 @@ Science*, *97*, 102965.
-11\. Bhattacharya, C. (2020). Gatekeeping the plenary floor: Discourse +14\. Bhattacharya, C. (2020). Gatekeeping the plenary floor: Discourse network analysis as a novel approach to party control. *Politics and Governance*, *8*(2), 229–242. @@ -125,7 +153,7 @@ Governance*, *8*(2), 229–242.
-12\. Bhattacharya, C. (2023). Restrictive rules of speechmaking as a +15\. Bhattacharya, C. (2023). Restrictive rules of speechmaking as a tool to maintain party unity: The case of oppressed political conflict in German parliament debates on the Euro crisis. *Party Politics*, *29*(3), 554--569. @@ -134,7 +162,7 @@ in German parliament debates on the Euro crisis. *Party Politics*,
-13\. Blažek, D. (2021). *Does the climate matter? Discourse network +16\. Blažek, D. (2021). *Does the climate matter? Discourse network analysis of climate delayism in the Czech Republic* \[Master’s thesis, Masaryk University, Faculty of Social Studies\]. @@ -143,7 +171,7 @@ Masaryk University, Faculty of Social Studies\].
-14\. Blessing, A., Blokker, N., Haunss, S., Kuhn, J., Lapesa, G., & +17\. Blessing, A., Blokker, N., Haunss, S., Kuhn, J., Lapesa, G., & Padó, S. (2019). An environment for relational annotation of political debates. *Proceedings of the 57th Annual Meeting of the Association for Computational Linguistics: System Demonstrations*, 105–110. @@ -153,7 +181,7 @@ Computational Linguistics: System Demonstrations*, 105–110.
-15\. Boas, I. (2015). *Climate migration and security: Securitisation as +18\. Boas, I. (2015). *Climate migration and security: Securitisation as a strategy in climate change politics*. Routledge. @@ -161,7 +189,7 @@ a strategy in climate change politics*. Routledge.
-16\. Bodišová, Ľ. (2018). *Európske záujmové skupiny a revízia smernice +19\. Bodišová, Ľ. (2018). *Európske záujmové skupiny a revízia smernice o energetickej efektívnosti – analỳza diskurzívnych sietí* \[Master's Thesis, Masaryk University, Faculty of Social Studies\]. @@ -170,7 +198,7 @@ Thesis, Masaryk University, Faculty of Social Studies\].
-17\. Bossner, F., & Nagel, M. (2020). Discourse networks and dual +20\. Bossner, F., & Nagel, M. (2020). Discourse networks and dual screening: Analyzing roles, content and motivations in political Twitter conversations. *Politics and Governance*, *8*(2), 311–325. @@ -179,7 +207,7 @@ conversations. *Politics and Governance*, *8*(2), 311–325.
-18\. Brandenberger, L. (2019). Predicting network events to assess +21\. Brandenberger, L. (2019). Predicting network events to assess goodness of fit of relational event models. *Political Analysis*, *27*(4), 556–571. @@ -187,7 +215,7 @@ goodness of fit of relational event models. *Political Analysis*,
-19\. Breindl, Y. (2013). Discourse networks on state-mandated access +22\. Breindl, Y. (2013). Discourse networks on state-mandated access blocking in Germany and France. *Info*, *15*(6), 42–62. @@ -195,7 +223,7 @@ blocking in Germany and France. *Info*, *15*(6), 42–62.
-20\. Broadbent, J., Sonnett, J., Botetzagias, I., Carson, M., Carvalho, +23\. Broadbent, J., Sonnett, J., Botetzagias, I., Carson, M., Carvalho, A., Chien, Y.-J., Edling, C., Fisher, D. R., Giouzepas, G., Haluza-DeLay, R., Hazegawa, K., Hirschi, C., Horta, A., Ikeda, K., Jin, J., Ku, D., Lahsen, M., Lee, H.-C., Lin, T.-L. A., … Zhengyi, S. (2016). @@ -207,7 +235,7 @@ Conflicting climate change frames in a global field of media discourse.
-21\. Broadbent, J., & Vaughter, P. (2014). Inter-disciplinary analysis +24\. Broadbent, J., & Vaughter, P. (2014). Inter-disciplinary analysis of climate change and society: A network approach. In M. J. Manfredo, J. J. Vaske, A. Rechkemmer, & E. A. Duke (Eds.), *Understanding society and natural resources. Forging new strands of integration across the social @@ -218,7 +246,7 @@ sciences* (pp. 203–228). Springer Open.
-22\. Brugger, F., & Engebretsen, R. (2022). Defenders of the status quo: +25\. Brugger, F., & Engebretsen, R. (2022). Defenders of the status quo: Making sense of the international discourse on transfer pricing methodologies. *Review of International Political Economy*, *29*(1), 307–335. @@ -227,7 +255,7 @@ methodologies. *Review of International Political Economy*, *29*(1),
-23\. Brugger, H., & Henry, A. D. (2021). Influence of policy discourse +26\. Brugger, H., & Henry, A. D. (2021). Influence of policy discourse networks on local energy transitions. *Environmental Innovation and Societal Transitions*, *39*, 141–154. @@ -236,7 +264,7 @@ Societal Transitions*, *39*, 141–154.
-24\. Brunner, L. (2021). *Der Einfluss von Interessensgruppen auf die +27\. Brunner, L. (2021). *Der Einfluss von Interessensgruppen auf die US-Außenpolitik. Ein Vergleich der Israelpolitik zwischen den Kabinetten Obama II und Trump* \[Master's Thesis, Leopold-Franzens-Universität Innsbruck, Fakultät für Soziale und Politische Wissenschaften\]. @@ -246,7 +274,7 @@ Innsbruck, Fakultät für Soziale und Politische Wissenschaften\].
-25\. Brutschin, E. (2013). *Dynamics in EU policy-making: The +28\. Brutschin, E. (2013). *Dynamics in EU policy-making: The liberalization of the European gas market* \[Doctoral Dissertation, University of Konstanz, Department of Politics; Public Administration\]. @@ -255,7 +283,7 @@ University of Konstanz, Department of Politics; Public Administration\].
-26\. Buckton, C. H., Fergie, G., Leifeld, P., & Hilton, S. (2019). A +29\. Buckton, C. H., Fergie, G., Leifeld, P., & Hilton, S. (2019). A discourse network analysis of UK newspaper coverage of the “sugar tax” debate before and after the announcement of the soft drinks industry levy. *BMC Public Health*, *19*(490), 1–14. @@ -265,7 +293,7 @@ levy. *BMC Public Health*, *19*(490), 1–14.
-27\. Cassiers, T. (2018). *“Politics... Or just work”? On the role of +30\. Cassiers, T. (2018). *“Politics... Or just work”? On the role of territoriality in policy networks. The case of transportation policies in the cross-border metropolitan regions of Brussels and Luxembourg* \[Doctoral Dissertation, KU Leuven, Division of Geography; Tourism\]. @@ -275,7 +303,7 @@ in the cross-border metropolitan regions of Brussels and Luxembourg*
-28\. Černỳ, O. (2018). *Limity české +31\. Černỳ, O. (2018). *Limity české energetické tranzice v politické perspektivě: Případ těžby uhlí* \[Diploma Thesis\]. @@ -283,7 +311,7 @@ energetické tranzice v politické perspektivě: Případ těžby uhlí*
-29\. Černỳ, O., & Ocelı́k, P. (2020). Incumbents’ strategies in media +32\. Černỳ, O., & Ocelı́k, P. (2020). Incumbents’ strategies in media coverage: A case of the Czech coal policy. *Politics and Governance*, *8*(2), 272–285. @@ -291,7 +319,7 @@ coverage: A case of the Czech coal policy. *Politics and Governance*,
-30\. Cicko, M. (2018). *Analỳza diskurzívnych sietí v energetickej +33\. Cicko, M. (2018). *Analỳza diskurzívnych sietí v energetickej politike Českej republiky* \[Master's Thesis, Masarykova univerzita, Fakulta sociálnı́ch studiı́\]. @@ -299,7 +327,7 @@ Fakulta sociálnı́ch studiı́\].
-31\. Ciordia Morandeira, A. (2020). *Less divided after ETA? Green +34\. Ciordia Morandeira, A. (2020). *Less divided after ETA? Green networks in the Basque country between 2007 and 2017* \[PhD thesis, University of Trento, School of Social Sciences\]. @@ -308,16 +336,25 @@ University of Trento, School of Social Sciences\].
-32\. Coronado Vigueras, R. A. (2015). *La reforma tributaria 2014: Un +35\. Coronado Vigueras, R. A. (2015). *La reforma tributaria 2014: Un análisis desde las coaliciones discursivas* \[Tesis Postgrado, Universidad de Chile, Facultad de Ciencias Físicas y Matemáticas\].
+
+ +36\. Cross, J. P., Greene, D., Umansky, N., & Calò, S. (2023). Speaking +in unison? Explaining the role of agenda-setter constellations in the +ECB policy agenda using a network-based approach. *Journal of European +Public Policy*. + +
+
-33\. Dalimunthe, S. A., Putri, I. A. P., & Prasojo, A. P. S. (2022). +37\. Dalimunthe, S. A., Putri, I. A. P., & Prasojo, A. P. S. (2022). Depicting mangrove’s potential as blue carbon champion in indonesia. In R. Dasgupta, S. Hashimoto, & O. Saito (Eds.), *Assessing, mapping and modelling of mangrove ecosystem services in the asia-pacific region* @@ -327,7 +364,7 @@ modelling of mangrove ecosystem services in the asia-pacific region*
-34\. Dı́az, A. O., & Gutiérrez, E. C. (2018). Competing actors in the +38\. Dı́az, A. O., & Gutiérrez, E. C. (2018). Competing actors in the climate change arena in Mexico: A network analysis. *Journal of Environmental Management*, *215*, 239–247. @@ -336,7 +373,7 @@ Environmental Management*, *215*, 239–247.
-35\. Drozdzynski, F. A. (2022). *The common agricultural policy post +39\. Drozdzynski, F. A. (2022). *The common agricultural policy post 2020: An analysis of the beliefs of selected key stakeholders* \[Master's Thesis, University of Twente, Faculty of Behavioural, Management; Social Sciences\]. @@ -345,7 +382,7 @@ Management; Social Sciences\].
-36\. Duygan, M. (2018). *An actor-based analysis of political context +40\. Duygan, M. (2018). *An actor-based analysis of political context for supporting sustainability transitions of socio-technical systems: A study of Swiss waste management* \[Doctoral Dissertation, ETH Zürich, Department of Environmental Systems Science\]. @@ -355,7 +392,7 @@ Department of Environmental Systems Science\].
-37\. Duygan, M., Stauffacher, M., & Meylan, G. (2019). A heuristic for +41\. Duygan, M., Stauffacher, M., & Meylan, G. (2019). A heuristic for conceptualizing and uncovering the determinants of agency in socio-technical transitions. *Environmental Innovation and Societal Transitions*, *33*, 13–29. @@ -364,7 +401,7 @@ Transitions*, *33*, 13–29.
-38\. Duygan, M., Stauffacher, M., & Meylan, G. (2018). Discourse +42\. Duygan, M., Stauffacher, M., & Meylan, G. (2018). Discourse coalitions in Swiss waste management: Gridlock or winds of change? *Waste Management*, *72*, 25–44. @@ -373,7 +410,7 @@ coalitions in Swiss waste management: Gridlock or winds of change?
-39\. Duygan, M., Stauffacher, M., & Meylan, G. (2021). What constitutes +43\. Duygan, M., Stauffacher, M., & Meylan, G. (2021). What constitutes agency? Determinants of actors’ influence on formal institutions in Swiss waste management. *Technological Forecasting and Social Change*, *162*, 120413. @@ -382,7 +419,7 @@ Swiss waste management. *Technological Forecasting and Social Change*,
-40\. Eberlein, B., & Rinscheid, A. (2020). Building bridges: How +44\. Eberlein, B., & Rinscheid, A. (2020). Building bridges: How discourse network analysis (DNA) can help CSR research to investigate the “new” political role of corporations. In M. Nagel, P. Kenis, P. Leifeld, & H.-J. Schmedes (Eds.), *Politische komplexität, governance @@ -393,14 +430,14 @@ von innovationen und policy-netzwerke* (pp. 139–146). Springer VS.
-41\. Eder, F. (2015). *Der irakkrieg 2003*. Innsbruck University Press. +45\. Eder, F. (2015). *Der irakkrieg 2003*. Innsbruck University Press.
-42\. Eder, F. (2023). Discourse network analysis. In P. A. Mello & F. +46\. Eder, F. (2023). Discourse network analysis. In P. A. Mello & F. Ostermann (Eds.), *Routledge handbook of foreign policy analysis methods* (pp. 516–535). Taylor & Francis. @@ -409,7 +446,7 @@ methods* (pp. 516–535). Taylor & Francis.
-43\. Eder, F. (2019). Making concurrence-seeking visible: Groupthink, +47\. Eder, F. (2019). Making concurrence-seeking visible: Groupthink, discourse networks, and the 2003 Iraq war. *Foreign Policy Analysis*, *15*(1), 21–42. @@ -417,7 +454,7 @@ discourse networks, and the 2003 Iraq war. *Foreign Policy Analysis*,
-44\. Eder, F., Libiseller, C., & Schneider, B. (2021). Contesting +48\. Eder, F., Libiseller, C., & Schneider, B. (2021). Contesting counter-terrorism: Discourse networks and the politicisation of counter-terrorism in Austria. *Journal of International Relations and Development*, *24*(1), 171–195. @@ -427,17 +464,25 @@ Development*, *24*(1), 171–195.
-45\. Edvra, P. A., & Ahmad, N. (2023). Tipologi jaringan wacana dan +49\. Edvra, P. A., & Ahmad, N. (2023). Tipologi jaringan wacana dan komunikator publik dalam berita omicron baru di media online. *Jurnal Riset Komunikasi*, *6*(1), 58–79.
+
+ +50\. Elislah, N. (2023). Discourse network analysis on delaying +elections in President Joko Widodo’s era. *Jurnal Aspikom*, *8*(2), +225–240. + +
+
-46\. Eriyanto, & Ali, D. J. (2020). Discourse network of a public issue -debate: A study on covid-19 cases in Indonesia. *Jurnal Komunikasi: +51\. Eriyanto, & Ali, D. J. (2020). Discourse network of a public issue +debate: A study on Covid-19 cases in Indonesia. *Jurnal Komunikasi: Malaysian Journal of Communication*, *36*(3), 209–227. @@ -445,7 +490,7 @@ Malaysian Journal of Communication*, *36*(3), 209–227.
-47\. Fergie, G., Leifeld, P., Hawkins, B., & Hilton, S. (2019). Mapping +52\. Fergie, G., Leifeld, P., Hawkins, B., & Hilton, S. (2019). Mapping discourse coalitions in the minimum unit pricing for alcohol debate: A discourse network analysis of UK newspaper coverage. *Addiction*, *114*(4), 741–753. @@ -454,7 +499,7 @@ discourse network analysis of UK newspaper coverage. *Addiction*,
-48\. Ferrare, J., Carter-Stone, L., & Galey-Horn, S. (2021). Ideological +53\. Ferrare, J., Carter-Stone, L., & Galey-Horn, S. (2021). Ideological tensions in education policy networks: An analysis of the policy innovators in education network in the United States. *Foro de Educacion*, *19*(1), 11–28. @@ -463,7 +508,7 @@ Educacion*, *19*(1), 11–28.
-49\. Filippini, R., Mazzocchi, C., & Corsi, S. (2015). Trends in urban +54\. Filippini, R., Mazzocchi, C., & Corsi, S. (2015). Trends in urban food strategies. In C. Tornaghi (Ed.), *Re-imagining sustainable food planning, building resourcefulness: Food movements, insurgent planning and heterodox economics: Proceedings of the 8th annual conference AESOP @@ -474,7 +519,7 @@ https://doi.org/ -50\. Fisher, D. R., & Leifeld, P. (2019). The polycentricity of climate +55\. Fisher, D. R., & Leifeld, P. (2019). The polycentricity of climate policy blockage. *Climatic Change*, *155*(4), 469–487. @@ -482,7 +527,7 @@ policy blockage. *Climatic Change*, *155*(4), 469–487.
-51\. Fisher, D. R., Leifeld, P., & Iwaki, Y. (2013). Mapping the +56\. Fisher, D. R., Leifeld, P., & Iwaki, Y. (2013). Mapping the ideological networks of American climate politics. *Climatic Change*, *116*(3), 523–545. @@ -490,7 +535,7 @@ ideological networks of American climate politics. *Climatic Change*,
-52\. Fisher, D. R., Waggle, J., & Leifeld, P. (2013). Where does +57\. Fisher, D. R., Waggle, J., & Leifeld, P. (2013). Where does political polarization come from? Locating polarization within the US climate change debate. *American Behavioral Scientist*, *57*(1), 70–92. @@ -499,7 +544,7 @@ climate change debate. *American Behavioral Scientist*, *57*(1), 70–92.
-53\. Friis, G. (2020). *Populist radical right parties into parliament: +58\. Friis, G. (2020). *Populist radical right parties into parliament: Changes in mainstream parties’ political positions in parliamentary debates on immigration and refugees* \[Master’s thesis, Uppsala University, Disciplinary Domain of Humanities; Social Sciences, Faculty @@ -510,7 +555,7 @@ of Social Sciences, Department of Government\].
-54\. Galey-Horn, S., & Ferrare, J. J. (2020). Using policy network +59\. Galey-Horn, S., & Ferrare, J. J. (2020). Using policy network analysis to understand ideological convergence and change in educational subsystems. *Education Policy Analysis Archives*, *28*(118). @@ -519,7 +564,7 @@ subsystems. *Education Policy Analysis Archives*, *28*(118).
-55\. Galey-Horn, S., Reckhow, S., Ferrare, J. J., & Jasny, L. (2020). +60\. Galey-Horn, S., Reckhow, S., Ferrare, J. J., & Jasny, L. (2020). Building consensus: Idea brokerage in teacher policy networks. *American Educational Research Journal*, *57*(2), 872–905. @@ -528,7 +573,7 @@ Educational Research Journal*, *57*(2), 872–905.
-56\. Galli Robertson, A. M. (2021). Privileged accounts in the debate +61\. Galli Robertson, A. M. (2021). Privileged accounts in the debate over coal-fired power in the United States. *Society & Natural Resources*, *34*(2), 188–207. @@ -537,7 +582,7 @@ Resources*, *34*(2), 188–207.
-57\. Gallmann, M. R. (2021). *Depicting climate change in a vulnerable +62\. Gallmann, M. R. (2021). *Depicting climate change in a vulnerable country: Agenda-setting and a discourse network approach on Philippine broadsheet media* \[Master's Thesis, University of Bern, Faculty of Science, Oeschger Centre for Climate Change Research\]. @@ -547,7 +592,7 @@ Science, Oeschger Centre for Climate Change Research\].
-58\. Geddes, A., Schmid, N., Schmidt, T. S., & Steffen, B. (2020). The +63\. Geddes, A., Schmid, N., Schmidt, T. S., & Steffen, B. (2020). The politics of climate finance: Consensus and partisanship in designing green state investment banks in the United Kingdom and Australia. *Energy Research & Social Science*, *69*, 101583. @@ -557,15 +602,16 @@ green state investment banks in the United Kingdom and Australia.
-59\. Ghinoi, S., De Vita, R., & Silvestri, F. (2021). Local +64\. Ghinoi, S., De Vita, R., & Silvestri, F. (2023). Local policymakers’ attitudes towards climate change: A multi-method case -study. *Social Networks*. +study. *Social Networks*, *25*, 197–209. +
-60\. Ghinoi, S., Junior, V. J. W., & Piras, S. (2018). Political debates +65\. Ghinoi, S., Junior, V. J. W., & Piras, S. (2018). Political debates and agricultural policies: Discourse coalitions behind the creation of Brazil’s Pronaf. *Land Use Policy*, *76*, 68–80. @@ -574,7 +620,7 @@ Brazil’s Pronaf. *Land Use Policy*, *76*, 68–80.
-61\. Ghinoi, S., & Omori, M. (2023). Expert knowledge and social +66\. Ghinoi, S., & Omori, M. (2023). Expert knowledge and social innovation: Analysing policy debates in Japan. *Journal of Social Entrepreneurship*. @@ -582,7 +628,7 @@ Entrepreneurship*.
-62\. Ghinoi, S., & Steiner, B. (2020). The political debate on climate +67\. Ghinoi, S., & Steiner, B. (2020). The political debate on climate change in Italy: A discourse network analysis. *Politics and Governance*, *8*(2), 215–228. @@ -590,7 +636,7 @@ Governance*, *8*(2), 215–228.
-63\. Gielens, E., Roosma, F., & Achterberg, P. (2023). Between left and +68\. Gielens, E., Roosma, F., & Achterberg, P. (2023). Between left and right: A discourse network analysis of universal basic income on Dutch Twitter. *Journal of Social Policy*. @@ -599,15 +645,24 @@ Twitter. *Journal of Social Policy*.
-64\. Gkiouzepas, G., & Botetzagias, I. (2017). Climate change coverage +69\. Gkiouzepas, G., & Botetzagias, I. (2017). Climate change coverage in Greek newspapers: 2001–2008. *Environmental Communication*, *11*(4), 490–514.
+
+ +70\. Grünwald, L. (2023). *Roadblocks of polarization: Mechanisms of +cultural resistance to a speed limit on German highways* \[Master’s +thesis, Universiteit Utrecht, Copernicus Institute of Sustainable +Development\]. + +
+
-65\. Gupta, K., Ripberger, J., Fox, A., Jenkins-Smith, H. C., & Silva, +71\. Gupta, K., Ripberger, J., Fox, A., Jenkins-Smith, H. C., & Silva, C. (2022). Discourse network analysis of nuclear narratives. In M. D. Jones, M. K. McBeth, & E. Shanahan (Eds.), *Narratives and the policy process: Applications of the narrative policy framework* (pp. 13–38). @@ -617,7 +672,7 @@ Montana State University Library.
-66\. Gutiérrez Meave, R. (2022). Framing and decisions: The punctuations +72\. Gutiérrez Meave, R. (2022). Framing and decisions: The punctuations of the mexican power generation policy subsystem. In A.-M. Bercu, I. Bilan, & C.-M. Apostoaie (Eds.), *European administrative area: Integration and resilience dynamics. Proceedings of the international @@ -629,16 +684,25 @@ Ioan Cuza” din Iai.
-67\. Gutiérrez Meave, R. (2022). *Redes de discurso, coaliciones y +73\. Gutiérrez Meave, R. (2022). *Redes de discurso, coaliciones y decisiones: La política de generación eléctrica en méxico 1994–2018* \[PhD thesis, Centro de Investigación y Docencia Económicas (CIDE), Doctorado en Políticas Públicas\].
+
+ +74\. Hamanduna, A. O. L., & Widjanarko, P. (2023). Discourse network on +the revision of Indonesian information and electronic transaction law. +*Jurnal Studi Komunikasi*, *7*(2), 519–538. + + +
+
-68\. Hanschmann, R. (2019). *Stalling the engine? EU climate politics +75\. Hanschmann, R. (2019). *Stalling the engine? EU climate politics after the “great recession.” Investigating the impact of economic shocks on EU climate policy-making in three case studies* \[Doctoral Dissertation, University of Potsdam, Faculty of Economics; Social @@ -648,7 +712,7 @@ Sciences\].
-69\. Hanschmann, R. (2017). Polarized business interests. EU climate +76\. Hanschmann, R. (2017). Polarized business interests. EU climate policy-making during the “great recession.” In D. K. Jesuit & R. A. Williams (Eds.), *Public policy, governance and polarization. Making governance work* (1st ed., pp. 126–156). Routledge. @@ -658,7 +722,7 @@ governance work* (1st ed., pp. 126–156). Routledge.
-70\. Hasselbalch, J. (2017). *The contentious politics of disruptive +77\. Hasselbalch, J. (2017). *The contentious politics of disruptive innovation: Vaping and fracking in the European Union* \[PhD thesis, University of Warwick, Department of Politics; International Studies; Université Libre de Bruxelles, Département de Sciences Politiques\]. @@ -668,7 +732,7 @@ Université Libre de Bruxelles, Département de Sciences Politiques\].
-71\. Hasselbalch, J. A. (2019). Framing brain drain: Between solidarity +78\. Hasselbalch, J. A. (2019). Framing brain drain: Between solidarity and skills in European labor mobility. *Review of International Political Economy*, *26*(6), 1333–1360. @@ -677,7 +741,7 @@ Political Economy*, *26*(6), 1333–1360.
-72\. Haunss, S. (2017). (De-)legitimating discourse networks: Smoke +79\. Haunss, S. (2017). (De-)legitimating discourse networks: Smoke without fire? In S. Schneider, H. Schmidtke, S. Haunss, & J. Gronau (Eds.), *Capitalism and its legitimacy in times of crisis* (pp. 191–220). Palgrave Macmillan. @@ -687,7 +751,7 @@ without fire? In S. Schneider, H. Schmidtke, S. Haunss, & J. Gronau
-73\. Haunss, S., Dietz, M., & Nullmeier, F. (2013). Der Ausstieg aus der +80\. Haunss, S., Dietz, M., & Nullmeier, F. (2013). Der Ausstieg aus der Atomenergie: Diskursnetzwerkanalyse als Beitrag zur Erklärung einer radikalen Politikwende. *Zeitschrift für Diskursforschung / Journal for Discourse Studies*, *1*(3), 288–316. @@ -697,16 +761,16 @@ Discourse Studies*, *1*(3), 288–316.
-74\. Haunss, S., & Hollway, J. (2022). Multimodal mechanisms of +81\. Haunss, S., & Hollway, J. (2023). Multimodal mechanisms of political discourse dynamics and the case of Germany’s nuclear energy -phase-out. *Network Science*, 1–19. +phase-out. *Network Science*, *11*(2), 205–223.
-75\. Haunss, S., Kuhn, J., Padó, S., Blessing, A., Blokker, N., Dayanik, +82\. Haunss, S., Kuhn, J., Padó, S., Blessing, A., Blokker, N., Dayanik, E., & Lapesa, G. (2020). Integrating manual and automatic annotation for the creation of discourse network data sets. *Politics and Governance*, *8*(2), 326–339. @@ -715,7 +779,7 @@ the creation of discourse network data sets. *Politics and Governance*,
-76\. Haunss, S., Lenke, F., Schmidtke, H., & Schneider, S. (2015). +83\. Haunss, S., Lenke, F., Schmidtke, H., & Schneider, S. (2015). Finanzkrise ohne Legitimationskrise? Kapitalismuskritik in der deutschen Qualitätspresse. In M. Dammayr, D. Grass, & B. Rothmüller (Eds.), *Legitimität. Gesellschaftliche, politische und wissenschaftliche @@ -726,7 +790,7 @@ Bruchlinien in der Rechtfertigung* (pp. 73–94). Transcript.
-77\. Hayward, B. A., McKay-Brown, L., & Poed, S. (2023). Restrictive +84\. Hayward, B. A., McKay-Brown, L., & Poed, S. (2023). Restrictive practices and the “need” for positive behaviour support (PBS): A critical discourse examination of disability policy beliefs. *Journal of Intellectual Disabilities*, *27*(1), 170–189. @@ -736,7 +800,7 @@ Intellectual Disabilities*, *27*(1), 170–189.
-78\. Heiberg, J. (2022). *The geography of configurations that work* +85\. Heiberg, J. (2022). *The geography of configurations that work* \[Doctoral Dissertation, Universiteit Utrecht, Copernicus Institute of Sustainable Development\]. @@ -744,7 +808,7 @@ Sustainable Development\].
-79\. Heiberg, J., Binz, C., & Truffer, B. (2020). The geography of +86\. Heiberg, J., Binz, C., & Truffer, B. (2020). The geography of technology legitimation: How multiscalar institutional dynamics matter for path creation in emerging industries. *Economic Geography*, *96*(5), 470–498. @@ -753,7 +817,7 @@ for path creation in emerging industries. *Economic Geography*, *96*(5),
-80\. Heiberg, J., Truffer, B., & Binz, C. (2022). Assessing transitions +87\. Heiberg, J., Truffer, B., & Binz, C. (2022). Assessing transitions through socio-technical configuration analysis – a methodological framework and a case study in the water sector. *Research Policy*, *51*(1), 104363. @@ -762,7 +826,7 @@ framework and a case study in the water sector. *Research Policy*,
-81\. Henrichsen, T. (2020). *Party competition as interdependent process +88\. Henrichsen, T. (2020). *Party competition as interdependent process – assessing the contagion effect of Eurosceptic parties in Italy* \[PhD thesis, Sant’Anna School of Advanced Studies Pisa, Joint PhD in Political Science, European Politics; International Relations\]. @@ -772,7 +836,7 @@ Political Science, European Politics; International Relations\].
-82\. Herranz-Surrallés, A. (2020). “Authority shifts” in global +89\. Herranz-Surrallés, A. (2020). “Authority shifts” in global governance: Intersecting politicizations and the reform of investor–state arbitration. *Politics and Governance*, *8*(1), 336–347. @@ -781,7 +845,7 @@ investor–state arbitration. *Politics and Governance*, *8*(1), 336–347.
-83\. Hertwig, M., & Witzak, P. (2022). Hybride Interessenvertretung in +90\. Hertwig, M., & Witzak, P. (2022). Hybride Interessenvertretung in der Plattformökonomie. Herausforderungen des “coalition building” bei der Kooperation zwischen IG Metall und YouTubers Union. *Zeitschrift für Soziologie*, *51*(2), 174–192. @@ -790,7 +854,7 @@ Soziologie*, *51*(2), 174–192.
-84\. Hilton, S., Buckton, C. H., Henrichsen, T., Fergie, G., & Leifeld, +91\. Hilton, S., Buckton, C. H., Henrichsen, T., Fergie, G., & Leifeld, P. (2020). Policy congruence and advocacy strategies in the discourse networks of minimum unit pricing for alcohol and the soft drinks industry levy. *Addiction*, *115*(12), 2303–2314. @@ -800,7 +864,7 @@ industry levy. *Addiction*, *115*(12), 2303–2314.
-85\. Hodge, E. M., Benko, S. L., & Salloum, S. J. (2020). Tracing +92\. Hodge, E. M., Benko, S. L., & Salloum, S. J. (2020). Tracing states’ messages about common core instruction: An analysis of English/language arts and close reading resources. *Teachers College Record*, *122*(3), 1–42. @@ -809,7 +873,7 @@ Record*, *122*(3), 1–42.
-86\. Holma, K. (2021). *Suomesta yritysvastuun edelläkävijä?: +93\. Holma, K. (2021). *Suomesta yritysvastuun edelläkävijä?: Diskurssiverkostoanalyysi suomalaisesta yritysvastuukeskustelusta* \[Master's Thesis, University of Helsinki, Faculty of Social Sciences\]. @@ -818,7 +882,7 @@ Diskurssiverkostoanalyysi suomalaisesta yritysvastuukeskustelusta*
-87\. Hopkins, V. (2020). *Lobbying for democracy: Interest groups in +94\. Hopkins, V. (2020). *Lobbying for democracy: Interest groups in Canada’s parliamentary system* \[PhD thesis, Simon Fraser University, Department of Political Science\]. @@ -826,7 +890,7 @@ Department of Political Science\].
-88\. Horning, D. G. (2017). *Understanding structure and character in +95\. Horning, D. G. (2017). *Understanding structure and character in rural water governance networks* \[PhD thesis, University of British Columbia, College of Graduate Studies\]. @@ -834,7 +898,7 @@ Columbia, College of Graduate Studies\].
-89\. Hornung, J., Schröder, I., & Bandelow, N. C. (2023). +96\. Hornung, J., Schröder, I., & Bandelow, N. C. (2023). Programmatisches Handeln in der deutschen Verkehrspolitik. Gemeinsame Identitäten von Akteuren im Umfeld des Deutschlandtakts. In D. Sack, H. Straßheim, & K. Zimmermann (Eds.), *Renaissance der Verkehrspolitik. @@ -845,7 +909,7 @@ Springer VS.
-90\. Howe, A. C. (2022). *Network processes related to political +97\. Howe, A. C. (2022). *Network processes related to political discourse and policy positions: The case of climate change policy networks in Canada* \[PhD thesis, University of British Columbia, Department of Sociology\]. @@ -854,16 +918,25 @@ Department of Sociology\].
-91\. Howe, A. C., Stoddart, M. C. J., & Tindall, D. B. (2020). Media +98\. Howe, A. C., Stoddart, M. C. J., & Tindall, D. B. (2020). Media coverage and perceived policy influence of environmental actors: Good strategy or pyrrhic victory? *Politics and Governance*, *8*(2), 298–310.
+
+ +99\. Hullmann, C. (2023). *Case study on the German discourse of +industry decarbonization* \[Master's Thesis, Radboud University +Nijmegen, Nijmegen School of Management\]. + + +
+
-92\. Hurka, S., & Nebel, K. (2013). Framing and policy change after +100\. Hurka, S., & Nebel, K. (2013). Framing and policy change after shooting rampages: A comparative analysis of discourse networks. *Journal of European Public Policy*, *20*(3), 390–406. @@ -872,7 +945,7 @@ shooting rampages: A comparative analysis of discourse networks.
-93\. Imbert, I. (2017). *An inquiry into the material and ideational +101\. Imbert, I. (2017). *An inquiry into the material and ideational dimensions of policymaking: A case study of fuel poverty in Germany* \[Doctoral Dissertation, University of Konstanz, Department of Politics; Public Administration\]. @@ -882,7 +955,7 @@ Public Administration\].
-94\. Jalasmäki, H. (2020). *Taistelu asiantuntijuudesta: Uskomukset ja +102\. Jalasmäki, H. (2020). *Taistelu asiantuntijuudesta: Uskomukset ja kannatuskoalitiot varhaiskasvatuksen diskurssiverkostossa* \[Master's Thesis, University of Helsinki, Faculty of Social Sciences\]. @@ -891,7 +964,7 @@ Thesis, University of Helsinki, Faculty of Social Sciences\].
-95\. Janning, F., Leifeld, P., Malang, T., & Schneider, V. (2009). +103\. Janning, F., Leifeld, P., Malang, T., & Schneider, V. (2009). Diskursnetzwerkanalyse. Überlegungen zur Theoriebildung und Methodik. In V. Schneider, F. Janning, P. Leifeld, & T. Malang (Eds.), *Politiknetzwerke. Modelle, Anwendungen und Visualisierungen* (pp. @@ -901,7 +974,7 @@ V. Schneider, F. Janning, P. Leifeld, & T. Malang (Eds.),
-96\. Jeong, M. (2017). *National renewable energy policy in a global +104\. Jeong, M. (2017). *National renewable energy policy in a global world* \[PhD thesis, University of Maryland, College Park, School of Public Policy\]. @@ -910,7 +983,7 @@ Public Policy\].
-97\. Jin, Y., Schaub, S., Tosun, J., & Wesseler, J. (2022). Does China +105\. Jin, Y., Schaub, S., Tosun, J., & Wesseler, J. (2022). Does China have a public debate on genetically modified organisms? A discourse network analysis of public debate on Weibo. *Public Understanding of Science*, *31*(6), 732–750. @@ -919,16 +992,16 @@ Science*, *31*(6), 732–750.
-98\. Joshi, B., & Swarnakar, P. (2022). How fair is our air? The +106\. Joshi, B., & Swarnakar, P. (2023). How fair is our air? The injustice of procedure, distribution, and recognition within the discourse of air pollution in Delhi, India. *Environmental Sociology*, -1–14. +*9*(2), 176–189.
-99\. Joshi, B., & Swarnakar, P. (2021). Staying away, staying alive: +107\. Joshi, B., & Swarnakar, P. (2021). Staying away, staying alive: Exploring risk and stigma of COVID-19 in the context of beliefs, actors and hierarchies in India. *Current Sociology*, *69*(4), 492–511. @@ -937,7 +1010,7 @@ and hierarchies in India. *Current Sociology*, *69*(4), 492–511.
-100\. Kammerer, M. (2017). *Climate politics at the intersection between +108\. Kammerer, M. (2017). *Climate politics at the intersection between international dynamics and national decision-making: A policy network approach* \[Doctoral Thesis, University of Zurich, Faculty of Arts; Social Sciences\]. @@ -946,7 +1019,7 @@ Social Sciences\].
-101\. Kammerer, M., Crameri, F., & Ingold, K. (2019). Das Klima und die +109\. Kammerer, M., Crameri, F., & Ingold, K. (2019). Das Klima und die EU: Eine Diskursperspektive auf die deutsche und schweizerische Klimapolitik. In R. Careja, P. Emmenegger, & N. Giger (Eds.), *The european social model under pressure. Liber amicorum in honour of klaus @@ -957,25 +1030,45 @@ armingeon* (pp. 599–623). Springer VS.
-102\. Kammerer, M., & Ingold, K. (2021). Actors and issues in climate +110\. Kammerer, M., & Ingold, K. (2023). Actors and issues in climate change policy: The maturation of a policy discourse in the national and -international context. *Social Networks*. +international context. *Social Networks*, *75*, 65–77.
-103\. Kasih, P. C. (2023). Pertarungan wacana Kereta Cepat +111\. Kasih, P. C. (2023). Pertarungan wacana Kereta Cepat Jakarta-Bandung dalam media online. *Jurnal Ilmu Komunikasi UHO: Jurnal Penelitian Kajian Ilmu Komunikasi Dan Informasi*, *8*(1), 19–34.
+
+ +112\. Keller, S. (2023). *Analysis of the media discourse about meat and +meat substitutes in U.S. Media between 2016 and 2021* \[Master's Thesis, +University of Bern, Faculty of Science, Oeschger Centre for Climate +Change Research\]. + + +
+ +
+ +113\. Kenis, P., & Schneider, V. (2019). Analyzing policy-making II: +Policy network analysis. In H. Van den Bulck, M. Puppis, K. Donders, & +L. Van Audenhove (Eds.), *The Palgrave handbook of methods for media +policy research* (pp. 471–491). Palgrave Macmillan. + + +
+
-104\. Khatami, M. I. (2022). Discourse network analysis (DNA): Aktivisme +114\. Khatami, M. I. (2022). Discourse network analysis (DNA): Aktivisme digital dalam perdebatan isu “presiden tiga periode” di Twitter. *Jurnal Audience: Jurnal Ilmu Komunikasi*, *5*(1), 80–94. @@ -984,7 +1077,7 @@ Audience: Jurnal Ilmu Komunikasi*, *5*(1), 80–94.
-105\. Khubbeeva, P. (2022). *Vom Bitcoin zur Blockchain? +115\. Khubbeeva, P. (2022). *Vom Bitcoin zur Blockchain? Distributed-Ledger-Technologien im politischen Diskurs. Leitbilder, Ideen und Diskursnetzwerke im deutschen Bundestag der 19. Legislaturperiode* \[Master's Thesis, FU Berlin, Otto-Suhr-Institut für @@ -994,7 +1087,7 @@ Politikwissenschaft\].
-106\. Koďousková, H., & Lehotskỳ, L. (2021). Energy poverty in the Czech +116\. Koďousková, H., & Lehotskỳ, L. (2021). Energy poverty in the Czech Republic: Individual responsibility or structural issue? *Energy Research & Social Science*, *72*, 101877. @@ -1003,7 +1096,7 @@ Research & Social Science*, *72*, 101877.
-107\. Koebele, E. A., Bultema, S., & Weible, C. (2020). Modeling +117\. Koebele, E. A., Bultema, S., & Weible, C. (2020). Modeling environmental governance in the Lake Tahoe basin: A multiplex network approach. In M. Fischer & K. Ingold (Eds.), *Networks in water governance* (pp. 173–202). Palgrave Macmillan. @@ -1013,16 +1106,25 @@ governance* (pp. 173–202). Palgrave Macmillan.
-108\. Kooistra, M. N. (2022). *Space security and orbital +118\. Kooistra, M. N. (2022). *Space security and orbital sustainability. An institutional logics approach* \[Master's Thesis, Universiteit Utrecht, Copernicus Institute of Sustainable Development\].
+
+ +119\. Koop-Monteiro, Y., Stoddart, M. C. J., & Tindall, D. B. (2023). +Animals and climate change: A visual and discourse network analysis of +Instagram posts. *Environmental Sociology*, *9*(4), 409–426. + + +
+
-109\. Kukkonen, A., Stoddart, M. C. J., & Ylä-Anttila, T. (2021). Actors +120\. Kukkonen, A., Stoddart, M. C. J., & Ylä-Anttila, T. (2021). Actors and justifications in media debates on Arctic climate change in Finland and Canada: A network approach. *Acta Sociologica*, *64*(1), 103–117. @@ -1031,7 +1133,7 @@ and Canada: A network approach. *Acta Sociologica*, *64*(1), 103–117.
-110\. Kukkonen, A. (2018). *Discourse networks and justifications of +121\. Kukkonen, A. (2018). *Discourse networks and justifications of climate change policy: News media debates in Canada, the United States, Finland, France, Brazil and India* \[Doctoral Dissertation, University of Helsinki, Faculty of Social Sciences\]. @@ -1039,9 +1141,19 @@ of Helsinki, Faculty of Social Sciences\].
+
+ +122\. Kukkonen, A., & Malkamäki, A. (2023). A cultural approach to +politicization of science: How the forestry coalition challenged the +scientific consensus in the Finnish news media debate on increased +logging. *Society & Natural Resources*. + + +
+
-111\. Kukkonen, A., & Ylä-Anttila, T. (2020). The science–policy +123\. Kukkonen, A., & Ylä-Anttila, T. (2020). The science–policy interface as a discourse network: Finland’s climate change policy 2002–2015. *Politics and Governance*, *8*(2), 200. @@ -1050,7 +1162,7 @@ interface as a discourse network: Finland’s climate change policy
-112\. Kukkonen, A., Ylä-Anttila, T., & Broadbent, J. (2017). Advocacy +124\. Kukkonen, A., Ylä-Anttila, T., & Broadbent, J. (2017). Advocacy coalitions, beliefs and climate change policy in the United States. *Public Administration*, *95*(3), 713–729. @@ -1059,7 +1171,7 @@ coalitions, beliefs and climate change policy in the United States.
-113\. Kukkonen, A., Ylä-Anttila, T., Swarnakar, P., Broadbent, J., +125\. Kukkonen, A., Ylä-Anttila, T., Swarnakar, P., Broadbent, J., Lahsen, M., & Stoddart, M. C. J. (2018). International organizations, advocacy coalitions, and domestication of global norms: Debates on climate change in Canada, the US, Brazil, and India. *Environmental @@ -1070,7 +1182,7 @@ Science & Policy*, *81*, 54–62.
-114\. Lapesa, G., Blessing, A., Blokker, N., Dayanık, E., Haunss, S., +126\. Lapesa, G., Blessing, A., Blokker, N., Dayanık, E., Haunss, S., Kuhn, J., & Padó, S. (2020). DEbateNet-mig15: Tracing the 2015 immigration debate in Germany over time. *Proceedings of the Twelfth Language Resources and Evaluation Conference*, 919–927. @@ -1080,7 +1192,7 @@ Language Resources and Evaluation Conference*, 919–927.
-115\. Laurer, M., & Seidl, T. (2021). Regulating the European +127\. Laurer, M., & Seidl, T. (2021). Regulating the European data-driven economy: A case study on the general data protection regulation. *Policy & Internet*, *13*(2), 257–277. @@ -1089,7 +1201,7 @@ regulation. *Policy & Internet*, *13*(2), 257–277.
-116\. Leifeld, P. (2009). Die Untersuchung von Diskursnetzwerken mit dem +128\. Leifeld, P. (2009). Die Untersuchung von Diskursnetzwerken mit dem Discourse Network Analyzer (DNA). In V. Schneider, F. Janning, P. Leifeld, & T. Malang (Eds.), *Politiknetzwerke. Modelle, Anwendungen und Visualisierungen* (pp. 391–404). Springer VS. @@ -1099,7 +1211,7 @@ Visualisierungen* (pp. 391–404). Springer VS.
-117\. Leifeld, P. (2013). Reconceptualizing major policy change in the +129\. Leifeld, P. (2013). Reconceptualizing major policy change in the advocacy coalition framework: A discourse network analysis of German pension politics. *Policy Studies Journal*, *41*(1), 169–198. @@ -1108,7 +1220,7 @@ pension politics. *Policy Studies Journal*, *41*(1), 169–198.
-118\. Leifeld, P. (2014). Polarization of coalitions in an agent-based +130\. Leifeld, P. (2014). Polarization of coalitions in an agent-based model of political discourse. *Computational Social Networks*, *1*(1), 1–22. @@ -1116,7 +1228,7 @@ model of political discourse. *Computational Social Networks*, *1*(1),
-119\. Leifeld, P. (2016). *Policy debates as dynamic networks: German +131\. Leifeld, P. (2016). *Policy debates as dynamic networks: German pension politics and privatization discourse*. Campus. @@ -1124,7 +1236,7 @@ pension politics and privatization discourse*. Campus.
-120\. Leifeld, P. (2017). Discourse network analysis: Policy debates as +132\. Leifeld, P. (2017). Discourse network analysis: Policy debates as dynamic networks. In J. N. Victor, A. H. Montgomery, & M. N. Lubell (Eds.), *The Oxford handbook of political networks* (pp. 301–325). Oxford University Press. @@ -1134,7 +1246,7 @@ Oxford University Press.
-121\. Leifeld, P. (2020). Policy debates and discourse network analysis: +133\. Leifeld, P. (2020). Policy debates and discourse network analysis: A research agenda. *Politics and Governance*, *8*(2), 180–183. @@ -1142,7 +1254,7 @@ A research agenda. *Politics and Governance*, *8*(2), 180–183.
-122\. Leifeld, P., & Brandenberger, L. (2019). *Endogenous coalition +134\. Leifeld, P., & Brandenberger, L. (2019). *Endogenous coalition formation in policy debates*. arXiv Preprint. @@ -1150,7 +1262,7 @@ formation in policy debates*. arXiv Preprint.
-123\. Leifeld, P., & Haunss, S. (2012). Political discourse networks and +135\. Leifeld, P., & Haunss, S. (2012). Political discourse networks and the conflict over software patents in Europe. *European Journal of Political Research*, *51*(3), 382–409. @@ -1159,7 +1271,7 @@ Political Research*, *51*(3), 382–409.
-124\. Leifeld, P., Henrichsen, T., Buckton, C., Fergie, G., & Hilton, S. +136\. Leifeld, P., Henrichsen, T., Buckton, C., Fergie, G., & Hilton, S. (2021). Belief system alignment and cross-sectoral advocacy efforts in policy debates. *Journal of European Public Policy*, 1–24. @@ -1168,7 +1280,7 @@ policy debates. *Journal of European Public Policy*, 1–24.
-125\. Leipold, A. C. (2016). *Ökonomische Ungleichheit und der Einfluss +137\. Leipold, A. C. (2016). *Ökonomische Ungleichheit und der Einfluss von Diskurskoalitionen auf Vermögensbesteuerung in Deutschland, 1995–2015: Eine Diskursnetzwerkanalyse von Policy-Wandel in der Steuerpolitik* \[Master's Thesis, FernUniversität Hagen, Fakultät für @@ -1179,7 +1291,7 @@ Kultur- und Sozialwissenschaften, Institut für Politikwissenschaft\].
-126\. Lemke, N., Trein, P., & Varone, F. (2023). *Defining artificial +138\. Lemke, N., Trein, P., & Varone, F. (2023). *Defining artificial intelligence as a political problem: A discourse network analysis from Germany*. @@ -1188,7 +1300,7 @@ Germany*.
-127\. Lestrelin, G., Augusseau, X., David, D., Bourgoin, J., +139\. Lestrelin, G., Augusseau, X., David, D., Bourgoin, J., Lagabrielle, E., Seen, D. L., & Degenne, P. (2017). Collaborative landscape research in Reunion Island: Using spatial modelling and simulation to support territorial foresight and urban planning. *Applied @@ -1198,7 +1310,7 @@ Geography*, *78*, 66–77.
-128\. Li, Z., Tan, X., & Liu, B. (2023). Policy changes in china’s +140\. Li, Z., Tan, X., & Liu, B. (2023). Policy changes in china’s family planning: Perspectives of advocacy coalitions. *International Journal of Environmental Research and Public Health*, *20*(6), 5204. @@ -1207,7 +1319,7 @@ Journal of Environmental Research and Public Health*, *20*(6), 5204.
-129\. Ličková, V. (2023). *Coal framing in the Indian political +141\. Ličková, V. (2023). *Coal framing in the Indian political discourse* \[Master's Thesis, Masaryk University, Faculty of Social Studies\]. @@ -1215,7 +1327,7 @@ Studies\].
-130\. Lindberg, M. B., & Kammermann, L. (2021). Advocacy coalitions in +142\. Lindberg, M. B., & Kammermann, L. (2021). Advocacy coalitions in the acceleration phase of the European energy transition. *Environmental Innovation and Societal Transitions*, *40*, 262–282. @@ -1224,7 +1336,7 @@ Innovation and Societal Transitions*, *40*, 262–282.
-131\. Lockhart, C. (2014). *Discourse network analysis of the Northern +143\. Lockhart, C. (2014). *Discourse network analysis of the Northern Gateway Pipeline project: Assessing environmental governance in the joint review panel process* \[Master's Thesis, Universiteit Utrecht, Copernicus Institute of Sustainable Development, Environmental @@ -1234,7 +1346,7 @@ Governance Section\].
-132\. Malkamäki, A., Ylä-Anttila, T., Brockhaus, M., Toppinen, A., & +144\. Malkamäki, A., Ylä-Anttila, T., Brockhaus, M., Toppinen, A., & Wagner, P. M. (2021). Unity in diversity? When advocacy coalitions and policy beliefs grow trees in South Africa. *Land Use Policy*, *102*, 105283. @@ -1243,16 +1355,25 @@ policy beliefs grow trees in South Africa. *Land Use Policy*, *102*,
-133\. Malkamäki, A. (2019). *On the human impacts and governance of +145\. Malkamäki, A. (2019). *On the human impacts and governance of large-scale tree plantations* \[Doctoral Dissertation, University of Helsinki, Faculty of Agriculture; Forestry\].
+
+ +146\. Malkamäki, A., Chen, T. H. Y., Gronow, A., Kivelä, M., Vesa, J., & +Ylä-Anttila, T. (2023). *Complex coalitions: Political alliances across +relational contexts*. arXiv:2308.14422. + + +
+
-134\. Malkamäki, A., Wagner, P. M., Brockhaus, M., Toppinen, A., & +147\. Malkamäki, A., Wagner, P. M., Brockhaus, M., Toppinen, A., & Ylä-Anttila, T. (2021). On the acoustics of policy learning: Can co-participation in policy forums break up echo chambers? *Policy Studies Journal*, *49*(2), 431–456. @@ -1261,7 +1382,7 @@ Studies Journal*, *49*(2), 431–456.
-135\. Mardiah, A. N. R. (2018). *Interface between disaster and +148\. Mardiah, A. N. R. (2018). *Interface between disaster and development: Local economic revival through collaborative post-disaster recovery governance and network in Indonesia* \[PhD thesis, University of Leeds, School of Geography\]. @@ -1271,7 +1392,7 @@ of Leeds, School of Geography\].
-136\. Mardiah, A. N., Lovett, J. C., & Evanty, N. (2017). Toward +149\. Mardiah, A. N., Lovett, J. C., & Evanty, N. (2017). Toward integrated and inclusive disaster risk reduction in Indonesia: Review of regulatory frameworks and institutional networks. In R. Djalante, M. Garschagen, F. Thomalla, & R. Shaw (Eds.), *Disaster risk reduction in @@ -1282,7 +1403,7 @@ Indonesia* (pp. 57–84). Springer.
-137\. Marino, F., Crowley, S. L., Williams Foley, N. A., McDonald, R. +150\. Marino, F., Crowley, S. L., Williams Foley, N. A., McDonald, R. A., & Hodgson, D. J. (2023). Stakeholder discourse coalitions and polarisation in the hen harrier conservation debate in news media. *People and Nature*, *5*(2), 668–683. @@ -1292,7 +1413,7 @@ polarisation in the hen harrier conservation debate in news media.
-138\. Mariño, D., & Rozenblat, C. (2022). Stakeholders’ power in the +151\. Mariño, D., & Rozenblat, C. (2022). Stakeholders’ power in the networking structuration processes of the urban resilience concept in Habitat III agenda (2012–2016). *Geography and Sustainability*, *3*(1), 46–57. @@ -1301,7 +1422,7 @@ Habitat III agenda (2012–2016). *Geography and Sustainability*, *3*(1),
-139\. Markard, J., Rinscheid, A., & Widdel, L. (2021). Analyzing +152\. Markard, J., Rinscheid, A., & Widdel, L. (2021). Analyzing transitions through the lens of discourse networks: Coal phase-out in Germany. *Environmental Innovation and Societal Transitions*, *40*, 315–331. @@ -1310,7 +1431,7 @@ Germany. *Environmental Innovation and Societal Transitions*, *40*,
-140\. Mayer, C. D. (2022). *New west tension and threatened species +153\. Mayer, C. D. (2022). *New west tension and threatened species protection: The western Joshua tree conservation debate in the Morongo Basin, california* \[Master's Thesis, California State University, Long Beach, Department of Geography\]. @@ -1320,16 +1441,26 @@ Beach, Department of Geography\].
-141\. McDonald, E. (2019). *Energy security in the age of +154\. McDonald, E. (2019). *Energy security in the age of interconnection: Cyber-threat framing in British political discourse* \[Master's Thesis, Masaryk University, Faculty of Social Studies\].
+
+ +155\. Mijailoff, J. D., & Burns, S. L. (2023). Fixing the meaning of +floating signifier: Discourses and network analysis in the bioeconomy +policy processes in Argentina and Uruguay. *Forest Policy and +Economics*, *154*, 103039. + + +
+
-142\. Miles, A. (2020). *Changes in social networks and narratives +156\. Miles, A. (2020). *Changes in social networks and narratives associated with Lake Erie water quality management after the 2014 Toledo water crisis* \[Master's Thesis, The Ohio State University, Graduate Program in Environment; Natural Resources\]. @@ -1339,7 +1470,7 @@ Program in Environment; Natural Resources\].
-143\. Minin, N. (2020). *Post-Fukushima discourse regarding nuclear +157\. Minin, N. (2020). *Post-Fukushima discourse regarding nuclear energy in the European Union and its implications* \[Doctoral Dissertation, Masaryk University, Department of International Relations; European Studies\]. @@ -1348,7 +1479,7 @@ European Studies\].
-144\. Miörner, J., Heiberg, J., & Binz, C. (2022). How global regimes +158\. Miörner, J., Heiberg, J., & Binz, C. (2022). How global regimes diffuse in space – explaining a missed transition in San Diego’s water sector. *Environmental Innovation and Societal Transitions*, *44*, 29–47. @@ -1357,7 +1488,7 @@ sector. *Environmental Innovation and Societal Transitions*, *44*,
-145\. Mišić, M., & Obydenkova, A. (2021). Environmental conflict, +159\. Mišić, M., & Obydenkova, A. (2021). Environmental conflict, renewable energy, or both? Public opinion on small hydropower plants in Serbia. *Post-Communist Economies*, *34*(5), 684–713. @@ -1366,16 +1497,16 @@ Serbia. *Post-Communist Economies*, *34*(5), 684–713.
-146\. Möck, M., Vogeler, C. S., Bandelow, N. C., & Hornung, J. (2022). +160\. Möck, M., Vogeler, C. S., Bandelow, N. C., & Hornung, J. (2023). Relational coupling of multiple streams: The case of COVID-19 infections -in German abattoirs. *Policy Studies Journal*. +in German abattoirs. *Policy Studies Journal*, *51*(2), 351–374.
-147\. Morton, S. E., Muchiri, J., & Swiss, L. (2020). Which feminism(s)? +161\. Morton, S. E., Muchiri, J., & Swiss, L. (2020). Which feminism(s)? For whom? Intersectionality in Canada’s feminist international assistance policy. *International Journal*, *75*(3), 329–348. @@ -1384,7 +1515,7 @@ assistance policy. *International Journal*, *75*(3), 329–348.
-148\. Muller, A. (2014). Het meten van discourscoalities met +162\. Muller, A. (2014). Het meten van discourscoalities met discoursnetwerkanalyse: Naar een formele analyse van het politieke vertoog. *Res Publica*, *56*(3), 337–364. @@ -1393,7 +1524,7 @@ vertoog. *Res Publica*, *56*(3), 337–364.
-149\. Muller, A. (2015). Using discourse network analysis to measure +163\. Muller, A. (2015). Using discourse network analysis to measure discourse coalitions: Towards a formal analysis of political discourse. *World Political Science*, *11*(2), 377–404. @@ -1402,7 +1533,7 @@ discourse coalitions: Towards a formal analysis of political discourse.
-150\. Murti, D. C. W., & Nur Ratriyana, I. (2022). The playground of big +164\. Murti, D. C. W., & Nur Ratriyana, I. (2022). The playground of big tobacco? Discourse network analysis of the cigarette advertising debate and policy in Indonesia. *Journal of Communication Inquiry*. @@ -1411,7 +1542,7 @@ and policy in Indonesia. *Journal of Communication Inquiry*.
-151\. Nagel, M. (2015). *Polarisierung im politischen Diskurs: Eine +165\. Nagel, M. (2015). *Polarisierung im politischen Diskurs: Eine Netzwerkanalyse zum Konflikt um “Stuttgart 21”*. Springer VS. @@ -1419,7 +1550,7 @@ Netzwerkanalyse zum Konflikt um “Stuttgart 21”*. Springer VS.
-152\. Nagel, M., & Bravo-Laguna, C. (2022). Analyzing multi-level +166\. Nagel, M., & Bravo-Laguna, C. (2022). Analyzing multi-level governance dynamics from a discourse network perspective: The debate over air pollution regulation in Germany. *Environmental Sciences Europe*, *34*(62), 1–18. @@ -1428,7 +1559,7 @@ Europe*, *34*(62), 1–18.
-153\. Nagel, M., & Satoh, K. (2019). Protesting iconic megaprojects. A +167\. Nagel, M., & Satoh, K. (2019). Protesting iconic megaprojects. A discourse network analysis of the evolution of the conflict over Stuttgart 21. *Urban Studies*, *56*(8), 1681–1700. @@ -1437,7 +1568,7 @@ Stuttgart 21. *Urban Studies*, *56*(8), 1681–1700.
-154\. Nagel, M., & Schäfer, M. (2023). Powerful stories of local climate +168\. Nagel, M., & Schäfer, M. (2023). Powerful stories of local climate action: Comparing the evolution of narratives using the “narrative rate” index. *Review of Policy Research*. @@ -1445,7 +1576,7 @@ index. *Review of Policy Research*.
-155\. Nägler, R. (2019). *Steuermannskunst im Hochschulmanagement. Die +169\. Nägler, R. (2019). *Steuermannskunst im Hochschulmanagement. Die Wirkungskraft von Ideen und Diskursen auf die Universität*. Springer VS. @@ -1453,7 +1584,7 @@ Wirkungskraft von Ideen und Diskursen auf die Universität*. Springer VS.
-156\. Nam, A., & Weible, C. M. (2023). Examining experts’ discourse in +170\. Nam, A., & Weible, C. M. (2023). Examining experts’ discourse in South Korea’s nuclear power policy making: An advocacy coalition framework approach to policy knowledge. *Politics & Policy*, *51*(2), 201–221. @@ -1462,7 +1593,7 @@ framework approach to policy knowledge. *Politics & Policy*, *51*(2),
-157\. Nam, A., Weible, C. M., & Park, K. (2022). Polarization and frames +171\. Nam, A., Weible, C. M., & Park, K. (2022). Polarization and frames of advocacy coalitions in South Korea’s nuclear energy policy. *Review of Policy Research*, *39*(4), 387–410. @@ -1471,7 +1602,7 @@ of Policy Research*, *39*(4), 387–410.
-158\. Nebel, K. (2016). Religion im moralpolitischen diskurs. Position +172\. Nebel, K. (2016). Religion im moralpolitischen diskurs. Position und Einfluss der Kirchen in der deutschen Debatte um die embryonale Stammzellenforschung. In A. Liedhegener & G. Pickel (Eds.), *Religionspolitik und Politik der Religionen in Deutschland* (pp. @@ -1481,7 +1612,7 @@ Stammzellenforschung. In A. Liedhegener & G. Pickel (Eds.),
-159\. Nolte, I. M., Polzer, T., & Seiwald, J. (2021). Gender budgeting +173\. Nolte, I. M., Polzer, T., & Seiwald, J. (2021). Gender budgeting in emerging economies – a systematic literature review and research agenda. *Journal of Accounting in Emerging Economies*, *11*(5), 799–820. @@ -1490,7 +1621,7 @@ agenda. *Journal of Accounting in Emerging Economies*, *11*(5), 799–820.
-160\. Ocelík, P. (2015). *Analỳza diskursivních sítí: Případ lokální +174\. Ocelík, P. (2015). *Analỳza diskursivních sítí: Případ lokální opozice vůči hlubinnému úložišti radioaktivních odpadů v České republice* \[PhD thesis, Masarykova univerzita, Fakulta sociálních studií\]. @@ -1499,7 +1630,7 @@ univerzita, Fakulta sociálních studií\].
-161\. Ocelı́k, P. (2022). Climate change scepticism in front-page Czech +175\. Ocelı́k, P. (2022). Climate change scepticism in front-page Czech newspaper coverage: A one man show. In D. Tindall, M. C. J. Stoddart, & R. E. Dunlap (Eds.), *Handbook of anti-environmentalism* (pp. 84–106). Edward Elgar Publishing. @@ -1508,7 +1639,7 @@ Edward Elgar Publishing.
-162\. Ohlendorf, N. (2022). *The political economy of energy +176\. Ohlendorf, N. (2022). *The political economy of energy transitions* \[Doctoral Thesis, Technical University of Berlin, Fakultät VI - Planen Bauen Umwelt, FG Ökonomie des Klimawandels\]. @@ -1517,7 +1648,7 @@ VI - Planen Bauen Umwelt, FG Ökonomie des Klimawandels\].
-163\. Ohlendorf, N., Löhr, M., & Markard, J. (2023). Actors in +177\. Ohlendorf, N., Löhr, M., & Markard, J. (2023). Actors in multi-sector transitions – discourse analysis on hydrogen in Germany. *Environmental Innovation and Societal Transitions*, *47*, 100692. @@ -1526,7 +1657,7 @@ multi-sector transitions – discourse analysis on hydrogen in Germany.
-164\. Ohno, T. (2022). Advocacy coalition framework in environmental +178\. Ohno, T. (2022). Advocacy coalition framework in environmental governance studies: Explaining major policy change for a large dam removal in Japan. *International Review of Public Policy*, *4*(1). @@ -1535,7 +1666,7 @@ removal in Japan. *International Review of Public Policy*, *4*(1).
-165\. Osei-Kojo, A. (2021). *An advocacy coalition framework analysis of +179\. Osei-Kojo, A. (2021). *An advocacy coalition framework analysis of oil and gas governance in Ghana* \[PhD thesis, University of Colorado Denver, School of Public Affairs\]. @@ -1544,7 +1675,7 @@ Denver, School of Public Affairs\].
-166\. Osei-Kojo, A. (2023). Analysing the stability of advocacy +180\. Osei-Kojo, A. (2023). Analysing the stability of advocacy coalitions and policy frames in Ghana’s oil and gas governance. *Policy & Politics*, *51*(1), 71--90. @@ -1553,7 +1684,7 @@ coalitions and policy frames in Ghana’s oil and gas governance. *Policy
-167\. Osička, J., Lehotskỳ, L., Zapletalová, V., Černoch, F., & Dančák, +181\. Osička, J., Lehotskỳ, L., Zapletalová, V., Černoch, F., & Dančák, B. (2018). Natural gas market integration in the Visegrad 4 region: An example to follow or to avoid? *Energy Policy*, *112*, 184–197. @@ -1562,7 +1693,7 @@ example to follow or to avoid? *Energy Policy*, *112*, 184–197.
-168\. Padó, S., Blessing, A., Blokker, N., Dayanık, E., Haunss, S., & +182\. Padó, S., Blessing, A., Blokker, N., Dayanık, E., Haunss, S., & Kuhn, J. (2019). Who sides with whom? Towards computational construction of discourse networks for political debates. *Proceedings of the 57th Annual Meeting of the Association for Computational Linguistics*, @@ -1572,7 +1703,7 @@ Annual Meeting of the Association for Computational Linguistics*,
-169\. Palladino, N. (2021). The role of epistemic communities in the +183\. Palladino, N. (2021). The role of epistemic communities in the “constitutionalization” of internet governance: The example of the European Commission high-level expert group on artificial intelligence. *Telecommunications Policy*, *45*(6), 102149. @@ -1582,7 +1713,7 @@ European Commission high-level expert group on artificial intelligence.
-170\. Pham-Truffert, M., Metz, F., Fischer, M., Rueff, H., & Messerli, +184\. Pham-Truffert, M., Metz, F., Fischer, M., Rueff, H., & Messerli, P. (2020). Interactions among sustainable development goals: Knowledge for identifying multipliers and virtuous cycles. *Sustainable Development*, *28*(5), 1236–1250. @@ -1591,7 +1722,7 @@ Development*, *28*(5), 1236–1250.
-171\. Pic, P. (2022). *Une sécurité arctique? Analyse des +185\. Pic, P. (2022). *Une sécurité arctique? Analyse des échelles de la sécurité dans une région à géométrie variable* \[PhD thesis, Université Laval, Qu’ebec, Graduate School of International Studies\]. @@ -1601,7 +1732,7 @@ Université Laval, Qu’ebec, Graduate School of International Studies\].
-172\. Piereder, J., Janzwood, S., & Homer-Dixon, T. (2023). Ideology and +186\. Piereder, J., Janzwood, S., & Homer-Dixon, T. (2023). Ideology and climate change. A complex reflexive systems approach to energy transition discourse networks. In J. Leader Maynard & M. L. Haas (Eds.), *The routledge handbook of ideology and international relations* (pp. @@ -1609,9 +1740,18 @@ transition discourse networks. In J. Leader Maynard & M. L. Haas (Eds.),
+
+ +187\. Pop, I., Gielens, E., & Kottmann, H. (2023). Microdosing +psychedelics: The segregation of spiritual and scientific narratives +within the r/microdosing online community. *Journal of Psychedelic +Studies*, *7*(2), 119–128. + +
+
-173\. Pospı́šilová, T. (2022). *Liberty, equality, hydrogen? Discourse +188\. Pospı́šilová, T. (2022). *Liberty, equality, hydrogen? Discourse network analysis of French hydrogen politics* \[Master's Thesis, Masaryk University, Faculty of Social Studies\]. @@ -1619,7 +1759,7 @@ University, Faculty of Social Studies\].
-174\. Pratama, B. I., & Illahi Ulfa, A. A. (n.d.). Discourse networking +189\. Pratama, B. I., & Illahi Ulfa, A. A. (n.d.). Discourse networking analysis as alternative research method in communication science studies – discourse networking analysis sebagai metode penelitian alternatif dalam kajian ilmu komunikasi. *Jurnal Penelitian Komunikasi Dan Opini @@ -1627,9 +1767,18 @@ Publik*, *21*(2), 223278.
+
+ +190\. Pratiwi, M., Murtiningsih, B. S. E., & Juliadi, R. (2023). +Discourse network analysis pada stakeholder dan integrated value +creation dalam CSR Bank Mandiri. *Jurnal Komunikasi Profesional*, +*7*(2), 256–274. + +
+
-175\. Rantala, S., & Di Gregorio, M. (2014). Multistakeholder +191\. Rantala, S., & Di Gregorio, M. (2014). Multistakeholder environmental governance in action: REDD+ discourse coalitions in Tanzania. *Ecology and Society*, *19*(2), 66–76. @@ -1638,7 +1787,7 @@ Tanzania. *Ecology and Society*, *19*(2), 66–76.
-176\. Reckhow, S., & Tompkins-Stange, M. (2018). Financing the education +192\. Reckhow, S., & Tompkins-Stange, M. (2018). Financing the education policy discourse: Philanthropic funders as entrepreneurs in policy networks. *Interest Groups & Advocacy*, *7*(3), 258–288. @@ -1647,7 +1796,7 @@ networks. *Interest Groups & Advocacy*, *7*(3), 258–288.
-177\. Reckhow, S., Tompkins-Stange, M., & Galey-Horn, S. (2021). How the +193\. Reckhow, S., Tompkins-Stange, M., & Galey-Horn, S. (2021). How the political economy of knowledge production shapes education policy: The case of teacher evaluation in federal policy discourse. *Educational Evaluation and Policy Analysis*, *43*(3), 472–494. @@ -1657,7 +1806,7 @@ Evaluation and Policy Analysis*, *43*(3), 472–494.
-178\. Rennkamp, B. (2019). Power, coalitions and institutional change in +194\. Rennkamp, B. (2019). Power, coalitions and institutional change in South African climate policy. *Climate Policy*, *19*(6), 756–770. @@ -1665,7 +1814,7 @@ South African climate policy. *Climate Policy*, *19*(6), 756–770.
-179\. Rennkamp, B., Haunss, S., Wongsa, K., Ortega, A., & Casamadrid, E. +195\. Rennkamp, B., Haunss, S., Wongsa, K., Ortega, A., & Casamadrid, E. (2017). Competing coalitions: The politics of renewable energy and fossil fuels in Mexico, South Africa and Thailand. *Energy Research & Social Science*, *34*, 214–223. @@ -1675,7 +1824,7 @@ Social Science*, *34*, 214–223.
-180\. Rinscheid, A. (2018). *Behavioral and institutionalist +196\. Rinscheid, A. (2018). *Behavioral and institutionalist perspectives on preference formation in a contested political context: The case of divesting from nuclear power* \[Doctoral Dissertation, University of St. Gallen, School of Management, Economics, Law, Social @@ -1686,7 +1835,7 @@ Sciences; International Affairs\].
-181\. Rinscheid, A. (2015). Crisis, policy discourse, and major policy +197\. Rinscheid, A. (2015). Crisis, policy discourse, and major policy change: Exploring the role of subsystem polarization in nuclear energy policymaking. *European Policy Analysis*, *1*(2), 34–70. @@ -1695,7 +1844,7 @@ policymaking. *European Policy Analysis*, *1*(2), 34–70.
-182\. Rinscheid, A. (2020). Business power in noisy politics: An +198\. Rinscheid, A. (2020). Business power in noisy politics: An exploration based on discourse network analysis and survey data. *Politics and Governance*, *8*(2), 286–297. @@ -1704,7 +1853,7 @@ exploration based on discourse network analysis and survey data.
-183\. Rinscheid, A., Eberlein, B., Emmenegger, P., & Schneider, V. +199\. Rinscheid, A., Eberlein, B., Emmenegger, P., & Schneider, V. (2020). Why do junctures become critical? Political discourse, agency, and joint belief shifts in comparative perspective. *Regulation & Governance*, *14*(4), 653–673. @@ -1713,7 +1862,7 @@ Governance*, *14*(4), 653–673.
-184\. Rone, J. (2018). *“Don’t worry, we are from the internet.” The +200\. Rone, J. (2018). *“Don’t worry, we are from the internet.” The diffusion of protest against the anti-counterfeiting trade agreement in the age of austerity* \[PhD thesis, European University Institute, Department of Political; Social Sciences\]. @@ -1723,15 +1872,16 @@ Department of Political; Social Sciences\].
-185\. Rone, J. (2022). Instrumentalising sovereignty claims in British +201\. Rone, J. (2023). Instrumentalising sovereignty claims in British pro- and anti-Brexit mobilisations. *The British Journal of Politics and -International Relations*. +International Relations*, *25*(3), 444–461. +
-186\. Rosalia, F. (2023). Discourse battle on Borobudur Temple tariff +202\. Rosalia, F. (2023). Discourse battle on Borobudur Temple tariff increase policy in discourse analysis network. *Jurnal Komunikasi*, *17*(1), 62–75. @@ -1739,7 +1889,7 @@ increase policy in discourse analysis network. *Jurnal Komunikasi*,
-187\. Rychlik, J., Hornung, J., & Bandelow, N. C. (2021). Come together, +203\. Rychlik, J., Hornung, J., & Bandelow, N. C. (2021). Come together, right now: Storylines and social identities in coalition building in a local policy subsystem. *Politics & Policy*, *49*(5), 1216–1247. @@ -1748,7 +1898,7 @@ local policy subsystem. *Politics & Policy*, *49*(5), 1216–1247.
-188\. Schaub, S. (2021). *The politics of water protection* \[Doctoral +204\. Schaub, S. (2021). *The politics of water protection* \[Doctoral Dissertation, University of Heidelberg, Faculty of Economics; Social Studies, Institute of Political Science\]. @@ -1757,7 +1907,7 @@ Studies, Institute of Political Science\].
-189\. Schaub, S. (2021). Public contestation over agricultural +205\. Schaub, S. (2021). Public contestation over agricultural pollution: A discourse network analysis on narrative strategies in the policy process. *Policy Sciences*, *54*(4), 783–821. @@ -1766,7 +1916,7 @@ policy process. *Policy Sciences*, *54*(4), 783–821.
-190\. Schaub, S., & Braunbeck, T. (2020). Transition towards sustainable +206\. Schaub, S., & Braunbeck, T. (2020). Transition towards sustainable pharmacy? The influence of public debates on policy responses to pharmaceutical contaminants in water. *Environmental Sciences Europe*, *32*(1), 1–19. @@ -1775,7 +1925,7 @@ pharmaceutical contaminants in water. *Environmental Sciences Europe*,
-191\. Schaub, S., & Metz, F. A. (2020). Comparing discourse and policy +207\. Schaub, S., & Metz, F. A. (2020). Comparing discourse and policy network approaches: Evidence from water policy on micropollutants. *Politics and Governance*, *8*(2), 184–199. @@ -1784,7 +1934,7 @@ network approaches: Evidence from water policy on micropollutants.
-192\. Schmid, N. (2020). *The politics of technological change – case +208\. Schmid, N. (2020). *The politics of technological change – case studies from the energy sector* \[Doctoral Thesis, ETH Zürich, Department of Humanities, Social; Political Sciences, Energy; Technology Policy Group\]. @@ -1793,7 +1943,7 @@ Policy Group\].
-193\. Schmid, N., Sewerin, S., & Schmidt, T. S. (2020). Explaining +209\. Schmid, N., Sewerin, S., & Schmidt, T. S. (2020). Explaining advocacy coalition change with policy feedback. *Policy Studies Journal*, *48*(4), 1109–1134. @@ -1801,7 +1951,7 @@ Journal*, *48*(4), 1109–1134.
-194\. Schmidt, S. (2017). *Terrorist attacks as “policy windows”: A +210\. Schmidt, S. (2017). *Terrorist attacks as “policy windows”: A discourse network analysis of German parliamentary debates* \[Diploma Thesis, Charles University, Department of Security Studies\]. @@ -1810,7 +1960,7 @@ Thesis, Charles University, Department of Security Studies\].
-195\. Schmidt, T. S., Schmid, N., & Sewerin, S. (2019). Policy goals, +211\. Schmidt, T. S., Schmid, N., & Sewerin, S. (2019). Policy goals, partisanship and paradigmatic change in energy policy – analyzing parliamentary discourse in Germany over 30 years. *Climate Policy*, *19*(6), 771–786. @@ -1819,7 +1969,7 @@ parliamentary discourse in Germany over 30 years. *Climate Policy*,
-196\. Schmitz, L. (2018). *From coherence to coheritization. Explaining +212\. Schmitz, L. (2018). *From coherence to coheritization. Explaining the rise of policy coherence in EU external policy* \[Master's Thesis, Radboud University Nijmegen, Faculteit der Managementwetenschappen\]. @@ -1828,7 +1978,7 @@ Radboud University Nijmegen, Faculteit der Managementwetenschappen\].
-197\. Schmitz, L., & Eimer, T. R. (2020). From coherence to +213\. Schmitz, L., & Eimer, T. R. (2020). From coherence to coheritization: Explaining the rise of policy coherence in EU external policy. *Globalizations*, *17*(4), 629–647. @@ -1837,7 +1987,7 @@ policy. *Globalizations*, *17*(4), 629–647.
-198\. Schmitz, L., & Seidl, T. (2022). As open as possible, as +214\. Schmitz, L., & Seidl, T. (2022). As open as possible, as autonomous as necessary: Understanding the rise of open strategic autonomy in EU trade policy. *JCMS: Journal of Common Market Studies*, *61*(3), 834–852. @@ -1846,7 +1996,7 @@ autonomy in EU trade policy. *JCMS: Journal of Common Market Studies*,
-199\. Schneider, V., & Ollmann, J. K. (2013). Punctuations and +215\. Schneider, V., & Ollmann, J. K. (2013). Punctuations and displacements in policy discourse: The climate change issue in Germany 2007–2010. In S. Silvern & S. Young (Eds.), *Environmental change and sustainability* (pp. 157–184). Intech. @@ -1855,7 +2005,7 @@ sustainability* (pp. 157–184). Intech.
-200\. Schulz, C. (2020). Forest conservation through markets? A +216\. Schulz, C. (2020). Forest conservation through markets? A discourse network analysis of the debate on funding mechanisms for REDD+ in Brazil. *Environmental Communication*, *14*(2), 202–218. @@ -1864,16 +2014,16 @@ in Brazil. *Environmental Communication*, *14*(2), 202–218.
-201\. Sconfienza, U., & Durand, F. (2023). Discourse network analysis of +217\. Sconfienza, U., & Durand, F. (2023). Discourse network analysis of Twitter and newspapers: Lessons learned from the nuclear debate in the -2022 French presidential campaign. *French Politics*. +2022 French presidential campaign. *French Politics*, *21*(2), 195–221.
-202\. Seidl, T. (2021). *Ideas, politics, and technological change: +218\. Seidl, T. (2021). *Ideas, politics, and technological change: Essays on the comparative political economy of digital capitalism* \[PhD thesis, European University Institute, Department of Political; Social Sciences\]. @@ -1882,7 +2032,7 @@ Sciences\].
-203\. Seidl, T. (2022). The politics of platform capitalism: A case +219\. Seidl, T. (2022). The politics of platform capitalism: A case study on the regulation of Uber in New York. *Regulation & Governance*, *16*(2), 357–374. @@ -1890,7 +2040,7 @@ study on the regulation of Uber in New York. *Regulation & Governance*,
-204\. Selle, L. (2017). What multi-level parliamentary system? +220\. Selle, L. (2017). What multi-level parliamentary system? Parliamentary discourses in EU budgetary negotiations (MFF 2014–2020). In S. Becker, M. W. Bauer, & A. De Feo (Eds.), *The new politics of the European Union budget* (pp. 149–172). Nomos. @@ -1900,7 +2050,7 @@ European Union budget* (pp. 149–172). Nomos.
-205\. Ševčík, M. (2021). *Analỳza vyřazenı́ jaderné energie z +221\. Ševčík, M. (2021). *Analỳza vyřazenı́ jaderné energie z energetického mixu Německa po roce 2011* \[Diploma Thesis\]. @@ -1908,7 +2058,7 @@ energetického mixu Německa po roce 2011* \[Diploma Thesis\].
-206\. Shukla, R., & Swarnakar, P. (2022). Energy justice in post-Paris +222\. Shukla, R., & Swarnakar, P. (2022). Energy justice in post-Paris India: Unpacking consensus and conflict through storylines and discourse coalitions. *Energy Research & Social Science*, *91*, 102687. @@ -1917,7 +2067,7 @@ coalitions. *Energy Research & Social Science*, *91*, 102687.
-207\. Shukla, R., & Swarnakar, P. (2022). Energy transition and +223\. Shukla, R., & Swarnakar, P. (2022). Energy transition and dialectics: Tracing discursive resistance to coal through discourse coalition in India. *Globalizations*. @@ -1926,16 +2076,58 @@ coalition in India. *Globalizations*.
-208\. Siagian, T. H. (2020). Mencari kelompok berisiko tinggi terinfeksi +224\. Siagian, T. H. (2020). Mencari kelompok berisiko tinggi terinfeksi virus corona dengan discourse network analysis. *Jurnal Kebijakan Kesehatan Indonesia: JKKI*, *9*(2), 98–106.
+
+ +225\. Sick, H. (2023). From rhetoric to regulation: Inferring lobbying +influence on EU efforts to regulate CO2 emissions of cars using network +analysis. *Interest Groups & Advocacy*. + + +
+ +
+ +226\. Silalahi, E. (2023). Analisis jaringan wacana pada pembentukan +UUTPKS di media daring. *Jurnal Riset Komunikasi (JURKOM)*, *6*(2), +34–49. + +
+ +
+ +227\. Sofura, A. M. (2023). Discourse network analysis: Studi kasus pada +kebijakan kenaikan harga Bahan Bakar Minyak (BBM) pertamina. +*Kommunikatif*, *12*(1), 37–50. + +
+ +
+ +228\. Sohn, C. (2023). The impact of rebordering on cross-border +cooperation actors’ discourses in the Öresund region. A semantic network +approach. *Geografiska Annaler: Series B, Human Geography*. + + +
+ +
+ +229\. Soraya, R. (2023). Jaringan wacana isu publik: Studi DNA pada isu +ASN terpapar radikalisme. *Jurnal Interaksi: Jurnal Ilmu Komunikasi*, +*7*(2), 130–145. + +
+
-209\. Stancioff, C. E. (2016). Locality and landscape change: Cultural +230\. Stancioff, C. E. (2016). Locality and landscape change: Cultural values and social-ecological resiliency in the Kalinago territory. In T. Collins, G. Kindermann, C. Newman, & N. Cronin (Eds.), *Landscape values: Place and praxis. Conference, galway, 29th june–2nd july, 2016* @@ -1946,7 +2138,7 @@ values: Place and praxis. Conference, galway, 29th june–2nd july, 2016*
-210\. Starke, J. (2016). *Generating policy change in situations of +231\. Starke, J. (2016). *Generating policy change in situations of equilibrium: Shifting discourse networks in the case of wild circus animals in Germany* \[Master's Thesis, Universiteit Utrecht, Copernicus Institute of Sustainable Development, Environmental Governance @@ -1954,9 +2146,19 @@ Section\].
+
+ +232\. Starke, J. R., Metze, T. A. P., Candel, J. J. L., Dewulf, A. R. P. +J., & Termeer, K. J. A. M. (2023). “Green future” versus “planetary +boundaries”? Evolving online discourse coalitions in European bioeconomy +conflicts. *Journal of Cleaner Production*, *425*, 139058. + + +
+
-211\. Steinfeld, N. (2016). The F-campaign: A discourse network analysis +233\. Steinfeld, N. (2016). The F-campaign: A discourse network analysis of party leaders’ campaign statements on Facebook. *Israel Affairs*, *22*(3–4), 743–759. @@ -1964,7 +2166,7 @@ of party leaders’ campaign statements on Facebook. *Israel Affairs*,
-212\. St-Jacques, B. (2019). *Us and them. Mapping discourse coalitions +234\. St-Jacques, B. (2019). *Us and them. Mapping discourse coalitions in the EU copyright directive debate* \[Master's Thesis, Hertie School of Governance, Master of Public Policy\]. @@ -1973,7 +2175,7 @@ of Governance, Master of Public Policy\].
-213\. Stoddart, M. C. J., Mattoni, A., & McLevey, J. (2020). *Industrial +235\. Stoddart, M. C. J., Mattoni, A., & McLevey, J. (2020). *Industrial development and eco-tourisms. Can oil extraction and nature conservation co-exist?* Palgrave Macmillan. @@ -1982,7 +2184,7 @@ co-exist?* Palgrave Macmillan.
-214\. Stoddart, M. C. J., McCurdy, P., Slawinski, N., & Collins, C. G. +236\. Stoddart, M. C. J., McCurdy, P., Slawinski, N., & Collins, C. G. (2020). Envisioning energy futures in the North Atlantic oil industry: Avoidance, persistence, and transformation as responses to climate change. *Energy Research & Social Science*, *69*, 101662. @@ -1992,7 +2194,7 @@ change. *Energy Research & Social Science*, *69*, 101662.
-215\. Stoddart, M. C. J., & Nezhadhossein, E. (2016). Is nature-oriented +237\. Stoddart, M. C. J., & Nezhadhossein, E. (2016). Is nature-oriented tourism a pro-environmental practice? Examining tourism–environmentalism alignments through discourse networks and intersectoral relationships. *The Sociological Quarterly*, *57*(3), 544–568. @@ -2002,7 +2204,7 @@ alignments through discourse networks and intersectoral relationships.
-216\. Stoddart, M. C. J., Ramos, H., Foster, K., & Ylä-Anttila, T. +238\. Stoddart, M. C. J., Ramos, H., Foster, K., & Ylä-Anttila, T. (2023). Competing crises? Media coverage and framing of climate change during the COVID-19 pandemic. *Environmental Communication*, *17*(3), 276–292. @@ -2011,7 +2213,7 @@ during the COVID-19 pandemic. *Environmental Communication*, *17*(3),
-217\. Stoddart, M. C. J., & Smith, J. (2016). The endangered arctic, the +239\. Stoddart, M. C. J., & Smith, J. (2016). The endangered arctic, the arctic as resource frontier: Canadian news media narratives of climate change and the north. *Canadian Review of Sociology/Revue Canadienne de Sociologie*, *53*(3), 316–336. @@ -2020,7 +2222,7 @@ Sociologie*, *53*(3), 316–336.
-218\. Stoddart, M. C. J., & Tindall, D. B. (2015). Canadian news media +240\. Stoddart, M. C. J., & Tindall, D. B. (2015). Canadian news media and the cultural dynamics of multilevel climate governance. *Environmental Politics*, *24*(3), 401–422. @@ -2029,7 +2231,7 @@ and the cultural dynamics of multilevel climate governance.
-219\. Stoddart, M. C. J., Tindall, D. B., Smith, J., & Haluza-Delay, R. +241\. Stoddart, M. C. J., Tindall, D. B., Smith, J., & Haluza-Delay, R. (2017). Media access and political efficacy in the eco-politics of climate change: Canadian national news and mediated policy networks. *Environmental Communication*, *11*(3), 386–400. @@ -2039,16 +2241,25 @@ climate change: Canadian national news and mediated policy networks.
-220\. Stoddart, M. C. J., & Yang, Y. (2022). What are the roles of +242\. Stoddart, M. C. J., & Yang, Y. (2022). What are the roles of regional and local climate governance discourse and actors? Mediated climate change policy networks in Atlantic Canada. *Review of Policy Research*.
+
+ +243\. Sumirat, P. A., & Eriyanto, E. (2023). Koalisi wacana dalam debat +pemekaran Papua: Analisis jaringan wacana debat pemekaran tiga provinsi +Baru di Papua. *Jurnal Riset Komunikasi (JURKOM)*, *6*(2), 1–16. + + +
+
-221\. Swacha, P., Karaczun, Z. M., & Murawska, D. (2022). The +244\. Swacha, P., Karaczun, Z. M., & Murawska, D. (2022). The Europeanization of Polish climate policy. *Ekonomia i Środowisko – Economics and Environment*, *83*(4), 62–75. @@ -2057,7 +2268,7 @@ Economics and Environment*, *83*(4), 62–75.
-222\. Swarnakar, P., Shukla, R., & Broadbent, J. (2022). Beliefs and +245\. Swarnakar, P., Shukla, R., & Broadbent, J. (2022). Beliefs and networks: Mapping the Indian climate policy discourse surrounding the Paris climate change conference in 2015. *Environmental Communication*, *16*(2), 145–162. @@ -2066,7 +2277,7 @@ Paris climate change conference in 2015. *Environmental Communication*,
-223\. Swedenmark, S. Ö. (2018). *“Vi ska sträcka oss lite längre +246\. Swedenmark, S. Ö. (2018). *“Vi ska sträcka oss lite längre än vi behöver.” En fallstudie om diskursen kring mellanorganisatorisk samverkan inom Stockholmsregionen* \[Magisteruppsats i statsvetenskap, Mittuniversitetet\]. @@ -2076,7 +2287,7 @@ kring mellanorganisatorisk samverkan inom Stockholmsregionen*
-224\. Swinkels, E. M. (2021). *The role of EU leaders and ideas in +247\. Swinkels, E. M. (2021). *The role of EU leaders and ideas in managing the Eurozone crisis: Navigating uncharted territory* \[PhD thesis, Utrecht University\]. @@ -2084,16 +2295,25 @@ thesis, Utrecht University\].
-225\. Swinkels, M., & Esch, F. van. (2022). Deciding upon the banking +248\. Swinkels, M., & Esch, F. van. (2022). Deciding upon the banking union: How a joint belief shift instigated deep institutional change in Eurozone governance. *European Policy Analysis*, *8*(1), 9–32.
+
+ +249\. Syafrudin, M., Sarwono, Hakim, A., & Solimun. (2023). Examine the +elements that impact food security. *Proceedings of the Fifth Annual +International Conference on Business and Public Administration (AICoBPA +2022)*, 563–581. + +
+
-226\. Taranger, K. K. (2020). *The institutionalisation of climate +250\. Taranger, K. K. (2020). *The institutionalisation of climate justice in the global governance architecture* \[Master's Thesis, Universiteit Utrecht, Copernicus Institute of Sustainable Development, Environmental Governance Section\]. @@ -2102,7 +2322,7 @@ Environmental Governance Section\].
-227\. Tobin, P., Schmidt, N. M., Tosun, J., & Burns, C. (2018). Mapping +251\. Tobin, P., Schmidt, N. M., Tosun, J., & Burns, C. (2018). Mapping states’ Paris climate pledges: Analysing targets and groups at COP 21. *Global Environmental Change*, *48*, 11–21. @@ -2111,7 +2331,7 @@ states’ Paris climate pledges: Analysing targets and groups at COP 21.
-228\. Tolstukha, E. (2022). *Stalemate in the democratic reform debate +252\. Tolstukha, E. (2022). *Stalemate in the democratic reform debate of the European Union? A dynamic discourse network analysis of actors and their commitment to reform options* \[PhD thesis, University of Glasgow, School of Social; Political Sciences\]. @@ -2121,7 +2341,7 @@ Glasgow, School of Social; Political Sciences\].
-229\. Tosun, J., & Lang, A. (2016). The politics of hydraulic fracturing +253\. Tosun, J., & Lang, A. (2016). The politics of hydraulic fracturing in Germany: Party competition at different levels of government. In C. M. Weible, T. Heikkila, K. Ingold, & M. Fischer (Eds.), *Policy debates on hydraulic fracturing. Comparing coalition politics in north america @@ -2132,7 +2352,7 @@ and europe* (pp. 177–200). Palgrave Macmillan.
-230\. Tosun, J., & Schaub, S. (2017). Mobilization in the European +254\. Tosun, J., & Schaub, S. (2017). Mobilization in the European public sphere: The struggle over genetically modified organisms. *Review of Policy Research*, *34*(3), 310–330. @@ -2141,7 +2361,7 @@ of Policy Research*, *34*(3), 310–330.
-231\. Tribulová, Z. (2019). *Postoj českej republiky k energetickej +255\. Tribulová, Z. (2019). *Postoj českej republiky k energetickej tranzícii v kontexte energiewende –- analýza politického diskurzu* \[Master's Thesis, Masaryk University, Faculty of Social Studies\]. @@ -2150,7 +2370,7 @@ tranzícii v kontexte energiewende –- analýza politického diskurzu*
-232\. Tuinenburg, J. (2019). *The effect of discourse networks on the +256\. Tuinenburg, J. (2019). *The effect of discourse networks on the leading support schemes for renewable electricity* \[Master's Thesis, Universiteit Utrecht, Sustainable Development, Earth System Governance\]. @@ -2159,7 +2379,7 @@ Governance\].
-233\. Umansky Casapa, N. (2022). *Securitization and social media +257\. Umansky Casapa, N. (2022). *Securitization and social media networks: Who tweets security?* \[Doctoral Thesis, University College Dublin, School of Politics; International Relations\]. @@ -2168,7 +2388,7 @@ Dublin, School of Politics; International Relations\].
-234\. Vanková, L. (2019). *Ťažba hnedého uhlia na hornej nitre: Analỳza +258\. Vanková, L. (2019). *Ťažba hnedého uhlia na hornej nitre: Analỳza diskurzívnych sietí* \[Master's Thesis, Masaryk University, Faculty of Social Studies\]. @@ -2176,7 +2396,7 @@ Social Studies\].
-235\. Vaughan, M. (2020). Talking about tax: The discursive distance +259\. Vaughan, M. (2020). Talking about tax: The discursive distance between 38 Degrees and GetUp. *Journal of Information Technology & Politics*, *17*(2), 114–129. @@ -2185,7 +2405,7 @@ Politics*, *17*(2), 114–129.
-236\. Vedres, B. (2022). Multivocality and robust action dynamics in +260\. Vedres, B. (2022). Multivocality and robust action dynamics in political discourse. *Poetics*, *90*, 101576. @@ -2193,7 +2413,7 @@ political discourse. *Poetics*, *90*, 101576.
-237\. Vesa, J., Gronow, A., & Ylä-Anttila, T. (2020). The quiet +261\. Vesa, J., Gronow, A., & Ylä-Anttila, T. (2020). The quiet opposition: How the pro-economy lobby influences climate policy. *Global Environmental Change*, *63*, 102117. @@ -2202,7 +2422,7 @@ Environmental Change*, *63*, 102117.
-238\. Vogeler, C. S. (2022). The integration of environmental objectives +262\. Vogeler, C. S. (2022). The integration of environmental objectives in the common agricultural policy—partisan politics in the European Parliament. *Zeitschrift für Vergleichende Politikwissenschaft*, *15*(4), 551–569. @@ -2211,7 +2431,7 @@ Parliament. *Zeitschrift für Vergleichende Politikwissenschaft*,
-239\. Vogeler, C. S., Möck, M., & Bandelow, N. C. (2021). Shifting +263\. Vogeler, C. S., Möck, M., & Bandelow, N. C. (2021). Shifting governance cooperatively – coordination by public discourses in the German water–food nexus. *Journal of Environmental Management*, *286*, 112266. @@ -2220,7 +2440,7 @@ German water–food nexus. *Journal of Environmental Management*, *286*,
-240\. Vogeler, C. S., Schwindenhammer, S., Gonglach, D., & Bandelow, N. +264\. Vogeler, C. S., Schwindenhammer, S., Gonglach, D., & Bandelow, N. C. (2021). Agri-food technology politics: Exploring policy narratives in the European Parliament. *European Policy Analysis*, *7*, 324–343. @@ -2229,7 +2449,7 @@ the European Parliament. *European Policy Analysis*, *7*, 324–343.
-241\. Steinsdorff, S. von, Gottmann, L., Hüggelmeyer, M., Jeske, I.-M., +265\. Steinsdorff, S. von, Gottmann, L., Hüggelmeyer, M., Jeske, I.-M., Onkelbach, C., & Siebeking, J. (2021). Plenardebatten als Spiegel sich wandelnder Diskurskoalitionen: Die Positionierung der Bundestagsfraktionen zum Verhältnis von Ökologie und Ökonomie seit 1977. @@ -2240,7 +2460,7 @@ Bundestagsfraktionen zum Verhältnis von Ökologie und Ökonomie seit 1977.
-242\. Wagner, P., & Payne, D. (2017). Trends, frames and discourse +266\. Wagner, P., & Payne, D. (2017). Trends, frames and discourse networks: Analysing the coverage of climate change in Irish newspapers. *Irish Journal of Sociology*, *25*(1), 5–28. @@ -2249,7 +2469,7 @@ networks: Analysing the coverage of climate change in Irish newspapers.
-243\. Wallaschek, S. (2019). The discursive appeal to solidarity and +267\. Wallaschek, S. (2019). The discursive appeal to solidarity and partisan journalism in Europe’s migration crisis. *Social Inclusion*, *7*(2), 187–197. @@ -2257,7 +2477,7 @@ partisan journalism in Europe’s migration crisis. *Social Inclusion*,
-244\. Wallaschek, S. (2019). *Mapping solidarity in Europe. Discourse +268\. Wallaschek, S. (2019). *Mapping solidarity in Europe. Discourse networks in the Euro crisis and Europe’s migration crisis* \[Doctoral Dissertation, University of Bremen, Bremen International Graduate School of Social Sciences (BIGSSS), Department of Social Sciences\]. @@ -2267,7 +2487,7 @@ of Social Sciences (BIGSSS), Department of Social Sciences\].
-245\. Wallaschek, S. (2020). Analyzing the European parliamentary +269\. Wallaschek, S. (2020). Analyzing the European parliamentary elections in 2019: Actor visibility and issue-framing in transnational media. In M. Kaeding, M. Müller, & J. Schmälter (Eds.), *Die Europwahl 2019. Ringen um die Zukunft Europas* (pp. 219–230). Springer VS. @@ -2277,7 +2497,7 @@ media. In M. Kaeding, M. Müller, & J. Schmälter (Eds.), *Die Europwahl
-246\. Wallaschek, S. (2020). Contested solidarity in the Euro crisis and +270\. Wallaschek, S. (2020). Contested solidarity in the Euro crisis and Europe’s migration crisis: A discourse network analysis. *Journal of European Public Policy*, *27*(7), 1034–1053. @@ -2286,7 +2506,7 @@ European Public Policy*, *27*(7), 1034–1053.
-247\. Wallaschek, S. (2020). Framing solidarity in the Euro crisis: A +271\. Wallaschek, S. (2020). Framing solidarity in the Euro crisis: A comparison of the German and Irish media discourse. *New Political Economy*, *25*(2), 231–247. @@ -2295,7 +2515,7 @@ Economy*, *25*(2), 231–247.
-248\. Wallaschek, S. (2020). The discursive construction of solidarity: +272\. Wallaschek, S. (2020). The discursive construction of solidarity: Analysing public claims in Europe’s migration crisis. *Political Studies*, *68*(1), 74–92. @@ -2303,7 +2523,7 @@ Studies*, *68*(1), 74–92.
-249\. Wallaschek, S. (2017). Notions of solidarity in Europe’s migration +273\. Wallaschek, S. (2017). Notions of solidarity in Europe’s migration crisis: The case of Germany’s media discourse. *EuropeNow Journal*, *11*. @@ -2311,7 +2531,7 @@ crisis: The case of Germany’s media discourse. *EuropeNow Journal*,
-250\. Wallaschek, S., Kaushik, K., Verbalyte, M., Sojka, A., Sorci, G., +274\. Wallaschek, S., Kaushik, K., Verbalyte, M., Sojka, A., Sorci, G., Trenz, H.-J., & Eigmüller, M. (2022). Same same but different? Gender politics and (trans-) national value contestation in Europe on Twitter. *Politics and Governance*, *10*(1), 146–160. @@ -2321,7 +2541,7 @@ politics and (trans-) national value contestation in Europe on Twitter.
-251\. Wallaschek, S., Starke, C., & Brüning, C. (2020). Solidarity in +275\. Wallaschek, S., Starke, C., & Brüning, C. (2020). Solidarity in the public sphere: A discourse network analysis of German newspapers (2008–2017). *Politics and Governance*, *8*(2). @@ -2330,7 +2550,7 @@ the public sphere: A discourse network analysis of German newspapers
-252\. Wang, S. (2018). *Dynamic constructed climate change discourses +276\. Wang, S. (2018). *Dynamic constructed climate change discourses and discourse networks across newspapers in China around three critical policy moments: A comparative study of People’s Daily, China Daily, and Southern Weekend* \[PhD thesis, University of Exeter, Department of @@ -2340,7 +2560,7 @@ Politics\].
-253\. Wang, C., & Wang, L. (2017). Unfolding policies for innovation +277\. Wang, C., & Wang, L. (2017). Unfolding policies for innovation intermediaries in China: A discourse network analysis. *Science and Public Policy*, *44*(3), 354–368. @@ -2349,7 +2569,7 @@ Public Policy*, *44*(3), 354–368.
-254\. Wang, Y. (2021). Examining the actor coalitions and discourse +278\. Wang, Y. (2021). Examining the actor coalitions and discourse coalitions of the opt-out movement in New York: A discourse network analysis. *Teachers College Record*, *123*(5), 1–26. @@ -2358,7 +2578,7 @@ analysis. *Teachers College Record*, *123*(5), 1–26.
-255\. Wang, Y. (2017). The social networks and paradoxes of the opt-out +279\. Wang, Y. (2017). The social networks and paradoxes of the opt-out movement amid the common core state standards implementation. *Education Policy Analysis Archives*, *25*(34), 1–27. @@ -2367,16 +2587,26 @@ Policy Analysis Archives*, *25*(34), 1–27.
-256\. Wang, Y. (2020). Understanding Congressional coalitions: A +280\. Wang, Y. (2020). Understanding Congressional coalitions: A discourse network analysis of Congressional hearings for the Every Student Succeeds act. *Education Policy Analysis Archives*, *28*(119), 1–30.
+
+ +281\. Wesche, J. P., Negro, S. O., Brugger, H. I., Eichhammer, W., & +Hekkert, M. P. (2023). The influence of visions on cooperation among +interest organizations in fragmented socio-technical systems. +*Environmental Policy and Governance*. + + +
+
-257\. Westenberger, G.-J., & Schneider, V. (2022). Söders Ökofeuerwerk +282\. Westenberger, G.-J., & Schneider, V. (2022). Söders Ökofeuerwerk und die grünfärbung der CSU: Diskursnetzwerke im bayrischen Themenwettbewerb. *Zeitschrift für Vergleichende Politikwissenschaft*, *15*(4), 641–665. @@ -2385,7 +2615,7 @@ Themenwettbewerb. *Zeitschrift für Vergleichende Politikwissenschaft*,
-258\. Wibisono, H., Lovett, J. C., & Anindito, D. B. (2023). The +283\. Wibisono, H., Lovett, J. C., & Anindito, D. B. (2023). The contestation of ideas behind Indonesia’s rural electrification policies: The influence of global and national institutional dynamics. *Development Policy Review*, *41*(1), e12650. @@ -2395,7 +2625,7 @@ The influence of global and national institutional dynamics.
-259\. Wu, J., & Liu, Y. (2020). Deception detection methods +284\. Wu, J., & Liu, Y. (2020). Deception detection methods incorporating discourse network metrics in synchronous computer-mediated communication. *Journal of Information Science*, *46*(1), 64–81. @@ -2404,7 +2634,7 @@ communication. *Journal of Information Science*, *46*(1), 64–81.
-260\. Wu, J., & Zhou, L. (2015). DOBNet: Exploiting the discourse of +285\. Wu, J., & Zhou, L. (2015). DOBNet: Exploiting the discourse of deception behaviour to uncover online deception strategies. *Behaviour & Information Technology*, *34*(9), 936–948. @@ -2413,7 +2643,7 @@ Information Technology*, *34*(9), 936–948.
-261\. Yan, K., Wu, H., Bu, K., & Wu, L. (2023). The college admission +286\. Yan, K., Wu, H., Bu, K., & Wu, L. (2023). The college admission policy evolution from 2003 to 2020 in China – a social network analysis. *Higher Education Policy*. @@ -2421,7 +2651,7 @@ policy evolution from 2003 to 2020 in China – a social network analysis.
-262\. Yap, X.-S., Heiberg, J., Truffer, B., David, E., & Kneib, J.-P. +287\. Yap, X.-S., Heiberg, J., Truffer, B., David, E., & Kneib, J.-P. (2023). Emerging global socio-technical regimes for tackling space debris: A discourse network analysis. *Acta Astronautica*, *207*, 445–454. @@ -2430,7 +2660,7 @@ debris: A discourse network analysis. *Acta Astronautica*, *207*,
-263\. You, J., Weible, C. M., & Heikkila, T. (2021). Exploring +288\. You, J., Weible, C. M., & Heikkila, T. (2021). Exploring instigator and defender policy scenarios in the siting of energy infrastructure. *Politics & Policy*, *50*(1), 8–32. @@ -2439,7 +2669,7 @@ infrastructure. *Politics & Policy*, *50*(1), 8–32.
-264\. Yordy, J., Durnová, A., & Weible, C. M. (2023). Exploring +289\. Yordy, J., Durnová, A., & Weible, C. M. (2023). Exploring emotional discourses: The case of COVID-19 protests in the US media. *Administrative Theory & Praxis*. @@ -2448,7 +2678,7 @@ emotional discourses: The case of COVID-19 protests in the US media.
-265\. Yordy, J., You, J., Park, K., Weible, C. M., & Heikkila, T. +290\. Yordy, J., You, J., Park, K., Weible, C. M., & Heikkila, T. (2019). Framing contests and policy conflicts over gas pipelines. *Review of Policy Research*, *36*(6), 736–756. @@ -2457,7 +2687,7 @@ emotional discourses: The case of COVID-19 protests in the US media.
-266\. You, J., Yordy, J., Park, K., Heikkila, T., & Weible, C. M. +291\. You, J., Yordy, J., Park, K., Heikkila, T., & Weible, C. M. (2020). Policy conflicts in the siting of natural gas pipelines. *Journal of Environmental Policy & Planning*, *22*(4), 501–517. @@ -2466,7 +2696,7 @@ emotional discourses: The case of COVID-19 protests in the US media.
-267\. You, J., Yordy, J., Weible, C. M., Park, K., Heikkila, T., & +292\. You, J., Yordy, J., Weible, C. M., Park, K., Heikkila, T., & Gilchrist, D. (2021). Comparing policy conflict on electricity transmission line sitings. *Public Policy and Administration*, *38*(1), 107--129. @@ -2475,7 +2705,7 @@ transmission line sitings. *Public Policy and Administration*, *38*(1),
-268\. Žaková, K. (2023). *Expertization of the Czech climate policy +293\. Žaková, K. (2023). *Expertization of the Czech climate policy network* \[Master’s thesis, Masaryk University, Faculty of Social Studies\]. diff --git a/dna/src/main/java/dna/Dna.java b/dna/src/main/java/dna/Dna.java index 8bf626dd..4caac62a 100644 --- a/dna/src/main/java/dna/Dna.java +++ b/dna/src/main/java/dna/Dna.java @@ -3,19 +3,9 @@ import java.io.*; import java.net.URISyntaxException; -import gui.NewDatabaseDialog; -import org.jasypt.exceptions.EncryptionOperationNotPossibleException; -import org.jasypt.util.text.AES256TextEncryptor; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonIOException; -import com.google.gson.JsonSyntaxException; - -import gui.MainWindow; +import gui.Gui; import logger.LogEvent; import logger.Logger; -import sql.ConnectionProfile; import sql.Sql; /** @@ -27,11 +17,11 @@ public class Dna { public static Dna dna; public static Logger logger; public static Sql sql; - public static final String date = "2023-03-01"; - public static final String version = "3.0.10"; + public static final String date = "2023-10-21"; + public static final String version = "3.0.11"; public static final String operatingSystem = System.getProperty("os.name"); public static File workingDirectory = null; - public MainWindow mainWindow; + public static Gui gui; public HeadlessDna headlessDna; /** @@ -50,7 +40,7 @@ public Dna(String[] args) { // determine JAR working directory as starting directory for file dialogs String currentDir = "~/"; try { - currentDir = new File(NewDatabaseDialog.class.getProtectionDomain().getCodeSource().getLocation().toURI()).getPath(); + currentDir = new File(HeadlessDna.class.getProtectionDomain().getCodeSource().getLocation().toURI()).getPath(); } catch (URISyntaxException ex) { LogEvent le = new LogEvent(Logger.WARNING, "Current JAR working directory cannot be detected.", @@ -58,11 +48,10 @@ public Dna(String[] args) { ex); logger.log(le); } - this.workingDirectory = new File(currentDir); + workingDirectory = new File(currentDir); // start GUI or headless DNA if (args != null && args.length > 0 && args[0].equals("headless")) { - mainWindow = new MainWindow(); headlessDna = new HeadlessDna(); Dna.logger.addListener(headlessDna); @@ -71,8 +60,7 @@ public Dna(String[] args) { "DNA started in headless mode to work with rDNA."); Dna.logger.log(l2); } else { - mainWindow = new MainWindow(); - mainWindow.setVisible(true); + gui = new Gui(); } } @@ -100,70 +88,4 @@ public void uncaughtException(Thread t, Throwable e) { logger.log(le); } } - - /** - * Read in a saved connection profile from a JSON file, decrypt the - * credentials, and return the connection profile. - * - * @param file The file name including path of the JSON connection profile - * @param key The key/password of the coder to decrypt the credentials - * @return Decrypted connection profile - */ - public static ConnectionProfile readConnectionProfile(String file, String key) throws EncryptionOperationNotPossibleException { - // read connection profile JSON file in, in String format but with encrypted credentials - ConnectionProfile cp = null; - Gson gson = new Gson(); - try (BufferedReader br = new BufferedReader(new FileReader(file))) { - cp = gson.fromJson(br, ConnectionProfile.class); - } catch (JsonSyntaxException | JsonIOException | IOException e) { - LogEvent l = new LogEvent(Logger.ERROR, - "Failed to read connection profile.", - "Tried to read a connection profile from a JSON file and failed. File: " + file + ".", - e); - Dna.logger.log(l); - } - - // decrypt the URL, user name, and SQL connection password inside the profile - AES256TextEncryptor textEncryptor = new AES256TextEncryptor(); - textEncryptor.setPassword(key); - cp.setUrl(textEncryptor.decrypt(cp.getUrl())); - cp.setUser(textEncryptor.decrypt(cp.getUser())); - cp.setPassword(textEncryptor.decrypt(cp.getPassword())); - - return cp; - } - - /** - * Take a decrypted connection profile, encrypt the credentials, and write - * it to a JSON file on disk. - * - * @param file The file name including full path as a String - * @param cp The connection profile to be encrypted and saved - * @param key The key/password of the coder to encrypt the credentials - */ - public static void writeConnectionProfile(String file, ConnectionProfile cp, String key) { - // encrypt URL, user, and password using Jasypt - AES256TextEncryptor textEncryptor = new AES256TextEncryptor(); - textEncryptor.setPassword(key); - cp.setUrl(textEncryptor.encrypt(cp.getUrl())); - cp.setUser(textEncryptor.encrypt(cp.getUser())); - cp.setPassword(textEncryptor.encrypt(cp.getPassword())); - - // serialize Connection object to JSON file and save to disk - try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) { - Gson prettyGson = new GsonBuilder() - .setPrettyPrinting() - .serializeNulls() - .disableHtmlEscaping() - .create(); - String g = prettyGson.toJson(cp); - writer.write(g); - } catch (IOException e) { - LogEvent l = new LogEvent(Logger.ERROR, - "Failed to write connection profile.", - "Tried to write a connection profile to a JSON file and failed. File: " + file + ".", - e); - Dna.logger.log(l); - } - } } \ No newline at end of file diff --git a/dna/src/main/java/dna/HeadlessDna.java b/dna/src/main/java/dna/HeadlessDna.java index 9a2c9308..b45fde74 100644 --- a/dna/src/main/java/dna/HeadlessDna.java +++ b/dna/src/main/java/dna/HeadlessDna.java @@ -12,6 +12,7 @@ import export.*; import me.tongfei.progressbar.ProgressBar; +import model.Value; import org.jasypt.exceptions.EncryptionOperationNotPossibleException; import org.rosuda.JRI.RConsoleOutputStream; import org.rosuda.JRI.Rengine; @@ -138,7 +139,7 @@ public boolean openDatabase(int coderId, String coderPassword, String type, Stri public boolean openConnectionProfile(String fileName, String clearCoderPassword) { ConnectionProfile cp = null; try { - cp = Dna.readConnectionProfile(fileName, clearCoderPassword); + cp = new ConnectionProfile(fileName, clearCoderPassword); } catch (EncryptionOperationNotPossibleException e2) { cp = null; LogEvent l = new LogEvent(Logger.ERROR, @@ -203,7 +204,7 @@ public boolean saveConnectionProfile(String fileName, String clearCoderPassword) boolean authenticated = Dna.sql.authenticate(-1, clearCoderPassword); if (authenticated) { // write the connection profile to disk, with an encrypted version of the password - Dna.writeConnectionProfile(fileName, new ConnectionProfile(Dna.sql.getConnectionProfile()), clearCoderPassword); + ConnectionProfile.writeConnectionProfile(fileName, new ConnectionProfile(Dna.sql.getConnectionProfile()), clearCoderPassword); LogEvent l = new LogEvent(Logger.MESSAGE, "Connection profile saved to file.", "A connection profile was successfully saved to the following file: \"" + fileName + "\"."); @@ -655,8 +656,10 @@ public double[][] rSaturation(int[] codedDocumentIds, int numSamples, int maxNum /** * Compute backbone and set of redundant entities on the second mode in a discourse network. * - * @param p Penalty parameter, for example {@code 7.5}. - * @param T Number of iterations, for example {@code 50000}. + * @param method Backbone algorithm (can be {@code "nested"}, {@code "fixed"}, or {@code "penalty"}). + * @param backboneSize The number of elements in the backbone set, as a fixed parameter. Only used when {@code method = "size"}. + * @param p Penalty parameter, for example {@code 7.5}. Only used when {@code method = "penalty"}. + * @param T Number of iterations, for example {@code 50000}. Only used when {@code method = "penalty"}. * @param statementType Statement type as a {@link String}. * @param variable1 First variable for export, provided as a {@link String}. * @param variable1Document boolean indicating if the first variable is at the document level. @@ -684,9 +687,9 @@ public double[][] rSaturation(int[] codedDocumentIds, int numSamples, int maxNum * @param invertTypes boolean indicating whether the document-level type values should be included (= {@code true}) rather than excluded. * @param outfile {@link String} with a file name under which the resulting network should be saved. * @param fileFormat {@link String} with the file format. Valid values are {@code "xml"}, {@code "json"}, and {@code null} (for no file export). - * @return A {@link BackboneResult} object containing the results. + * @return A {@link SimulatedAnnealingBackboneResult} object containing the results. */ - public void rBackbone(double p, int T, String statementType, String variable1, boolean variable1Document, String variable2, + public void rBackbone(String method, int backboneSize, double p, int T, String statementType, String variable1, boolean variable1Document, String variable2, boolean variable2Document, String qualifier, boolean qualifierDocument, String qualifierAggregation, String normalization, String duplicates, String startDate, String stopDate, String startTime, String stopTime, String[] excludeVariables, String[] excludeValues, String[] excludeAuthors, String[] excludeSources, String[] excludeSections, @@ -791,43 +794,234 @@ public void rBackbone(double p, int T, String statementType, String variable1, b } // step 3: compute results - this.exporter.backbone(p, T); // initial results - try (ProgressBar pb = new ProgressBar("Simulated annealing...", T)) { - while (exporter.getCurrentT() <= T) { // run up to upper bound of iterations T, provided by the user - pb.stepTo(exporter.getCurrentT()); - exporter.iterateBackbone(); + if (method.equals("nested")) { + this.exporter.initializeNestedBackbone(); + int iterations = 0; + for (int i = 0; i < exporter.getFullSize(); i++) { + iterations = iterations + (exporter.getFullSize() - i); } - exporter.saveBackboneResult(); - - // step 4: save to file - if (fileFormat != null && outfile != null) { - if (fileFormat.equals("json") && !outfile.toLowerCase().endsWith(".json")) { - outfile = outfile + ".json"; - LogEvent le = new LogEvent(Logger.WARNING, - "Appended \".json\" to file name.", - "The outfile for the backbone export did not end with \".json\" although the \"json\" file format was chosen. Appending \".json\" to the file name."); - Dna.logger.log(le); + try (ProgressBar pb = new ProgressBar("Nested backbone...", iterations)) { + while (exporter.getBackboneSize() > 0) { // run up to the point where all concepts have been moved from the backbone set to the redundant set + int it = 1; + for (int i = 0; i < exporter.getFullSize() - exporter.getBackboneSize(); i++) { + it = it + (exporter.getFullSize() - i); + } + pb.stepTo(it); // go from empty backbone set to full backbone set + exporter.iterateNestedBackbone(); } - if (fileFormat.equals("xml") && !outfile.toLowerCase().endsWith(".xml")) { - outfile = outfile + ".xml"; - LogEvent le = new LogEvent(Logger.WARNING, - "Appended \".xml\" to file name.", - "The outfile for the backbone export did not end with \".xml\" although the \"xml\" file format was chosen. Appending \".xml\" to the file name."); - Dna.logger.log(le); + exporter.saveNestedBackboneResult(); + + // step 4: save to file + saveJsonXml(fileFormat, outfile); + pb.stepTo(iterations); + } + } else if (method.equals("fixed") || method.equals("penalty")) { + this.exporter.initializeSimulatedAnnealingBackbone(method.equals("penalty"), p, T, backboneSize); // initialize algorithm + try (ProgressBar pb = new ProgressBar("Simulated annealing...", T)) { + while (exporter.getCurrentT() <= T) { // run up to upper bound of iterations T, provided by the user + pb.stepTo(exporter.getCurrentT()); + exporter.iterateSimulatedAnnealingBackbone(method.equals("penalty")); } - if (!fileFormat.equals("xml") && !fileFormat.equals("json")) { - fileFormat = null; - LogEvent le = new LogEvent(Logger.WARNING, - "File format for backbone export not recognized.", - "The file format for saving a backbone and redundant set to disk was not recognized. Valid file formats are \"json\" and \"xml\". The file format you provided was \"" + fileFormat + "\". Not saving the file to disk because the file format is unknown."); - Dna.logger.log(le); - } else { - this.exporter.writeBackboneToFile(outfile); + exporter.saveSimulatedAnnealingBackboneResult(method.equals("penalty")); + + // step 4: save to file + saveJsonXml(fileFormat, outfile); + pb.stepTo(T); + } + } + } + + /** + * Compute the spectral loss for a given backbone set relative to the full network. + * + * @param backboneEntities An array of entities (e.g., concepts) for which the spectral loss should be computed relative to the full network. + * @param p The penalty parameter. Can be \code{0} to switch off the penalty. + * @param statementType Statement type as a {@link String}. + * @param variable1 First variable for export, provided as a {@link String}. + * @param variable1Document boolean indicating if the first variable is at the document level. + * @param variable2 Second variable for export, provided as a {@link String}. + * @param variable2Document boolean indicating if the second variable is at the document level. + * @param qualifier Qualifier variable as a {@link String}. + * @param qualifierDocument boolean indicating if the qualifier variable is at the document level. + * @param qualifierAggregation Aggregation rule for the qualifier variable (can be {@code "ignore"}, {@code "combine"}, {@code "subtract"}, {@code "congruence"}, or {@code "conflict"}). Note that negative values in the {@code "subtract"} case are replaced by {@code 0}. + * @param normalization Normalization setting as a {@link String}, as provided by rDNA (can be {@code "no"}, {@code "activity"}, {@code "prominence"}, {@code "average"}, {@code "jaccard"}, or {@code "cosine"}). + * @param duplicates An input {@link String} from rDNA that can be {@code "include"}, {@code "document"}, {@code "week"}, {@code "month"}, {@code "year"}, or {@code "acrossrange"}. + * @param startDate Start date for the export, provided as a {@link String} with format {@code "dd.MM.yyyy"}. + * @param stopDate Stop date for the export, provided as a {@link String} with format {@code "dd.MM.yyyy"}. + * @param startTime Start time for the export, provided as a {@link String} with format {@code "HH:mm:ss"}. + * @param stopTime Stop time for the export, provided as a {@link String} with format {@code "HH:mm:ss"}. + * @param excludeVariables A {@link String} array with n elements, indicating the variable of the n'th value. + * @param excludeValues A {@link String} array with n elements, indicating the value pertaining to the n'th variable {@link String}. + * @param excludeAuthors A {@link String} array of values to exclude in the {@code author} variable at the document level. + * @param excludeSources A {@link String} array of values to exclude in the {@code source} variable at the document level. + * @param excludeSections A {@link String} array of values to exclude in the {@code section} variable at the document level. + * @param excludeTypes A {@link String} array of values to exclude in the {@code "type"} variable at the document level. + * @param invertValues boolean indicating whether the statement-level exclude values should be included (= {@code true}) rather than excluded. + * @param invertAuthors boolean indicating whether the document-level author values should be included (= {@code true}) rather than excluded. + * @param invertSources boolean indicating whether the document-level source values should be included (= {@code true}) rather than excluded. + * @param invertSections boolean indicating whether the document-level section values should be included (= {@code true}) rather than excluded. + * @param invertTypes boolean indicating whether the document-level type values should be included (= {@code true}) rather than excluded. + * @return A double array with the loss for the backbone and redundant set. + */ + public double[] rEvaluateBackboneSolution(String[] backboneEntities, int p, String statementType, String variable1, boolean variable1Document, String variable2, + boolean variable2Document, String qualifier, boolean qualifierDocument, String qualifierAggregation, String normalization, + String duplicates, String startDate, String stopDate, String startTime, String stopTime, + String[] excludeVariables, String[] excludeValues, String[] excludeAuthors, String[] excludeSources, String[] excludeSections, + String[] excludeTypes, boolean invertValues, boolean invertAuthors, boolean invertSources, boolean invertSections, + boolean invertTypes) { + + // step 1: preprocess arguments + StatementType st = Dna.sql.getStatementType(statementType); // format statement type + + // format dates and times with input formats "dd.MM.yyyy" and "HH:mm:ss" + DateTimeFormatter dtf = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss"); + LocalDateTime ldtStart, ldtStop; + LocalDateTime[] dateRange = Dna.sql.getDateTimeRange(); + if (startTime == null || startTime.equals("")) { + startTime = "00:00:00"; + } + if (startDate == null || startDate.equals("") || startDate.equals("01.01.1900")) { + ldtStart = dateRange[0]; + } else { + String startString = startDate + " " + startTime; + ldtStart = LocalDateTime.parse(startString, dtf); + if (!startString.equals(dtf.format(ldtStart))) { + ldtStart = dateRange[0]; + LogEvent le = new LogEvent(Logger.WARNING, + "Start date or time is invalid.", + "When computing the backbone and redundant set of the network, the start date or time (" + startString + ") did not conform to the format dd.MM.yyyy HH:mm:ss and could not be interpreted. Assuming earliest date and time in the dataset: " + ldtStart.format(dtf) + "."); + Dna.logger.log(le); + } + } + if (stopTime == null || stopTime.equals("")) { + stopTime = "23:59:59"; + } + if (stopDate == null || stopDate.equals("") || stopDate.equals("31.12.2099")) { + ldtStop = dateRange[1]; + } else { + String stopString = stopDate + " " + stopTime; + ldtStop = LocalDateTime.parse(stopString, dtf); + if (!stopString.equals(dtf.format(ldtStop))) { + ldtStop = dateRange[1]; + LogEvent le = new LogEvent(Logger.WARNING, + "End date or time is invalid.", + "When computing the spectral loss of a backbone set, the end date or time (" + stopString + ") did not conform to the format dd.MM.yyyy HH:mm:ss and could not be interpreted. Assuming latest date and time in the dataset: " + ldtStop.format(dtf) + "."); + Dna.logger.log(le); + } + } + + // process exclude variables: create HashMap with variable:value pairs + HashMap> map = new HashMap>(); + if (excludeVariables.length > 0) { + for (int i = 0; i < excludeVariables.length; i++) { + ArrayList values = map.get(excludeVariables[i]); + if (values == null) { + values = new ArrayList(); } + if (!values.contains(excludeValues[i])) { + values.add(excludeValues[i]); + } + Collections.sort(values); + map.put(excludeVariables[i], values); } + } - pb.stepTo(T); + // initialize Exporter class + this.exporter = new Exporter( + "onemode", + st, + variable1, + variable1Document, + variable2, + variable2Document, + qualifier, + qualifierDocument, + qualifierAggregation, + normalization, + true, + duplicates, + ldtStart, + ldtStop, + "no", + 1, + map, + Stream.of(excludeAuthors).collect(Collectors.toCollection(ArrayList::new)), + Stream.of(excludeSources).collect(Collectors.toCollection(ArrayList::new)), + Stream.of(excludeSections).collect(Collectors.toCollection(ArrayList::new)), + Stream.of(excludeTypes).collect(Collectors.toCollection(ArrayList::new)), + invertValues, + invertAuthors, + invertSources, + invertSections, + invertTypes, + null, + null); + + // step 2: filter + this.exporter.loadData(); + this.exporter.filterStatements(); + if (exporter.getFilteredStatements().size() == 0) { + LogEvent le = new LogEvent(Logger.ERROR, + "No statements left after filtering.", + "Attempted to filter the statements by date and other criteria before finding backbone. But no statements were left after applying the filters. Perhaps the time period was mis-specified?"); + Dna.logger.log(le); } + + // step 3: compute and return results + return this.exporter.evaluateBackboneSolution(backboneEntities, p); + } + + private void saveJsonXml(String fileFormat, String outfile) { + if (fileFormat != null && outfile != null) { + if (fileFormat.equals("json") && !outfile.toLowerCase().endsWith(".json")) { + outfile = outfile + ".json"; + LogEvent le = new LogEvent(Logger.WARNING, + "Appended \".json\" to file name.", + "The outfile for the backbone export did not end with \".json\" although the \"json\" file format was chosen. Appending \".json\" to the file name."); + Dna.logger.log(le); + } + if (fileFormat.equals("xml") && !outfile.toLowerCase().endsWith(".xml")) { + outfile = outfile + ".xml"; + LogEvent le = new LogEvent(Logger.WARNING, + "Appended \".xml\" to file name.", + "The outfile for the backbone export did not end with \".xml\" although the \"xml\" file format was chosen. Appending \".xml\" to the file name."); + Dna.logger.log(le); + } + if (!fileFormat.equals("xml") && !fileFormat.equals("json")) { + fileFormat = null; + LogEvent le = new LogEvent(Logger.WARNING, + "File format for backbone export not recognized.", + "The file format for saving a backbone and redundant set to disk was not recognized. Valid file formats are \"json\" and \"xml\". The file format you provided was \"" + fileFormat + "\". Not saving the file to disk because the file format is unknown."); + Dna.logger.log(le); + } else { + this.exporter.writeBackboneToFile(outfile); + } + } + } + + /* ================================================================================================================= + * Functions for managing variables + * ================================================================================================================= + */ + + /** + * Retrieve variables and data type definitions for a given statement type (via label). + * + * @param statementTypeLabel Label of the statement type for which variables should be retrieved. + * @return Array list of {@link Value} objects representing variables. + */ + public ArrayList getVariables(String statementTypeLabel) { + return Dna.sql.getStatementType(statementTypeLabel).getVariables(); + } + + /** + * Retrieve variables and data type definitions for a given statement type (via ID). + * + * @param statementTypeId ID of the statement type for which variables should be retrieved. + * @return Array list of {@link Value} objects representing variables. + */ + public ArrayList getVariables(int statementTypeId) { + return Dna.sql.getStatementType(statementTypeId).getVariables(); } /* ================================================================================================================= diff --git a/dna/src/main/java/export/Exporter.java b/dna/src/main/java/export/Exporter.java index 9a59c156..7a91d370 100644 --- a/dna/src/main/java/export/Exporter.java +++ b/dna/src/main/java/export/Exporter.java @@ -22,7 +22,6 @@ import org.ojalgo.matrix.Primitive64Matrix; import org.ojalgo.matrix.decomposition.Eigenvalue; -import java.awt.*; import java.io.*; import java.time.LocalDateTime; import java.time.ZoneOffset; @@ -75,23 +74,34 @@ public class Exporter { * Holds the resulting matrices. Can have size 1. */ private ArrayList matrixResults; - /** - * Holds the resulting backbone result. - */ - private BackboneResult backboneResult = null; - // objects for backbone algorithm + // common backbone algorithm objects + private String[] fullConcepts; + private Matrix fullMatrix; + private ArrayList currentBackboneList, currentRedundantList; + private double[] eigenvaluesFull; + + // objects for nested backbone algorithm + private int counter; + private int[] iteration, numStatements; + private String[] entity; + private double[] backboneLoss, redundantLoss; + ArrayList backboneMatrices = new ArrayList<>(); + ArrayList redundantMatrices = new ArrayList<>(); + private NestedBackboneResult nestedBackboneResult = null; + + // objects for simulated annealing backbone algorithm private ArrayList temperatureLog, acceptanceProbabilityLog, penalizedBackboneLossLog, acceptanceRatioLastHundredIterationsLog; private ArrayList acceptedLog, proposedBackboneSizeLog, acceptedBackboneSizeLog, finalBackboneSizeLog; - private String[] fullConcepts; private String selectedAction; - private ArrayList actionList, currentBackboneList, currentRedundantList, candidateBackboneList, candidateRedundantList, finalBackboneList, finalRedundantList; + private ArrayList actionList, candidateBackboneList, candidateRedundantList, finalBackboneList, finalRedundantList; private ArrayList currentStatementList, candidateStatementList, finalStatementList; // declare candidate statement list at t - private Matrix fullMatrix, currentMatrix, candidateMatrix, finalMatrix; // candidate matrix at the respective t, Y^{B^*_t} + private Matrix currentMatrix, candidateMatrix, finalMatrix; // candidate matrix at the respective t, Y^{B^*_t} private boolean accept; private double p, temperature, acceptance, r, oldLoss, newLoss, finalLoss, log; - private double[] eigenvaluesFull, eigenvaluesCurrent, eigenvaluesCandidate, eigenvaluesFinal; - private int T, t; + private double[] eigenvaluesCurrent, eigenvaluesCandidate, eigenvaluesFinal; + private int T, t, backboneSize; + private SimulatedAnnealingBackboneResult simulatedAnnealingBackboneResult = null; /** *

Create a new Exporter class instance, holding an array list of export @@ -419,7 +429,7 @@ public Exporter( this.qualifierAggregation = qualifierAggregation.toLowerCase(); this.qualifier = qualifier; ArrayList variables = Stream.of(this.statementType.getVariablesList(false, true, true, true)).collect(Collectors.toCollection(ArrayList::new)); - if (this.qualifier != null && this.qualifierDocument == false && (!variables.contains(this.qualifier) || this.qualifier.equals(this.variable1) || this.qualifier.equals(this.variable2))) { + if (this.qualifier != null && !this.qualifierDocument && (!variables.contains(this.qualifier) || this.qualifier.equals(this.variable1) || this.qualifier.equals(this.variable2))) { this.qualifier = null; if (!this.qualifierAggregation.equals("ignore")) { this.qualifierAggregation = "ignore"; @@ -1825,7 +1835,7 @@ public void computeTimeWindowMatrices() { currentWindowStatements.add(this.filteredStatements.get(i)); } } - if (currentWindowStatements.size() > 0) { + // if (currentWindowStatements.size() > 0) { Matrix m; if (this.networkType.equals("twomode")) { m = computeTwoModeMatrix(currentWindowStatements, windowStart, windowStop); @@ -1835,7 +1845,7 @@ public void computeTimeWindowMatrices() { m.setDateTime(matrixTime); m.setNumStatements(currentWindowStatements.size()); timeWindowMatrices.add(m); - } + // } } percent = 100 * (currentTime.toEpochSecond(ZoneOffset.UTC) - startCalendar.toEpochSecond(ZoneOffset.UTC)) / (stopCalendar.toEpochSecond(ZoneOffset.UTC) - startCalendar.toEpochSecond(ZoneOffset.UTC)); pb.stepTo(percent); @@ -2590,6 +2600,347 @@ public BarplotResult generateBarplotData() { return new BarplotResult(this.variable1, values, counts, attributes, intScale, attributeVariables); } + /** + * Get the current iteration {@code t} of the simulated annealing algorithm. + * + * @return Current iteration {@code t}. + */ + public int getCurrentT() { + return this.t; + } + + /** + * Set the current iteration {@code t} of the simulated annealing algorithm. + * + * @return Current iteration {@code t}. + */ + public void setCurrentT(int t) { + this.t = t; + } + + /** + * Reduce the dimensions of a candidate matrix with all isolate nodes to the dimensions of the full matrix, which + * does not contain isolate nodes. + * + * @param candidateMatrix The candidate matrix with isolates (to be reduced to smaller dimensions). + * @param fullLabels The node labels of the full matrix without isolates. + * @return A reduced candidate matrix with the same dimensions as the full matrix and the same node order. + */ + private Matrix reduceCandidateMatrix(Matrix candidateMatrix, String[] fullLabels) { + HashMap map = new HashMap(); + for (int i = 0; i < fullLabels.length; i++) { + for (int j = 0; j < candidateMatrix.getRowNames().length; j++) { + if (fullLabels[i].equals(candidateMatrix.getRowNames()[j])) { + map.put(i, j); + } + } + } + double[][] mat = new double[fullLabels.length][fullLabels.length]; + for (int i = 0; i < fullLabels.length; i++) { + for (int j = 0; j < fullLabels.length; j++) { + mat[i][j] = candidateMatrix.getMatrix()[map.get(i)][map.get(j)]; + } + } + candidateMatrix.setMatrix(mat); + candidateMatrix.setRowNames(fullLabels); + candidateMatrix.setColumnNames(fullLabels); + return candidateMatrix; + } + + /** + * Compute matrix after final backbone iteration, collect results, and save in class. + */ + public void saveSimulatedAnnealingBackboneResult(boolean penalty) { + Collections.sort(finalBackboneList); + Collections.sort(finalRedundantList); + + // create redundant matrix + ArrayList redundantStatementList = this.filteredStatements + .stream() + .filter(s -> currentRedundantList.contains(((Entity) s.get(this.variable2)).getValue())) + .collect(Collectors.toCollection(ArrayList::new)); + Matrix redundantMatrix = this.computeOneModeMatrix(redundantStatementList, this.qualifierAggregation, this.startDateTime, this.stopDateTime); + + String method = "penalty"; + if (!penalty) { + method = "fixed"; + p = 0; + } + this.simulatedAnnealingBackboneResult = new SimulatedAnnealingBackboneResult(method, + finalBackboneList.toArray(String[]::new), + finalRedundantList.toArray(String[]::new), + spectralLoss(eigenvaluesFull, eigenvaluesCurrent), + spectralLoss(eigenvaluesFull, computeNormalizedEigenvalues(redundantMatrix.getMatrix())), + p, + T, + temperatureLog.stream().mapToDouble(v -> v.doubleValue()).toArray(), + acceptanceProbabilityLog.stream().mapToDouble(v -> v.doubleValue()).toArray(), + acceptedLog.stream().mapToInt(v -> v.intValue()).toArray(), + penalizedBackboneLossLog.stream().mapToDouble(v -> v.doubleValue()).toArray(), + proposedBackboneSizeLog.stream().mapToInt(v -> v.intValue()).toArray(), + acceptedBackboneSizeLog.stream().mapToInt(v -> v.intValue()).toArray(), + finalBackboneSizeLog.stream().mapToInt(v -> v.intValue()).toArray(), + acceptanceRatioLastHundredIterationsLog.stream().mapToDouble(v -> v.doubleValue()).toArray(), + fullMatrix.getMatrix(), + currentMatrix.getMatrix(), + redundantMatrix.getMatrix(), + fullMatrix.getRowNames(), + fullMatrix.getStart().toEpochSecond(ZoneOffset.UTC), + fullMatrix.getStop().toEpochSecond(ZoneOffset.UTC), + fullMatrix.getNumStatements()); + this.nestedBackboneResult = null; + } + + /** + * Get the penalty backbone result that is saved in the class. + * + * @return The penalty backbone result (can be null if backbone function has not been executed). + */ + public SimulatedAnnealingBackboneResult getSimulatedAnnealingBackboneResult() { + return this.simulatedAnnealingBackboneResult; + } + + /** + * Use tools from the {@code ojalgo} library to compute eigenvalues of a symmetric matrix. + * + * @param matrix The matrix as a two-dimensional double array. + * @return One-dimensional double array of eigenvalues. + */ + private double[] computeNormalizedEigenvalues(double[][] matrix) { + for (int i = 0; i < matrix.length; i++) { + for (int j = 0; j < matrix[0].length; j++) { + if (matrix[i][j] < 0) { + matrix[i][j] = 0.0; + } + } + } + Primitive64Matrix matrixPrimitive = Primitive64Matrix.FACTORY.rows(matrix); // create matrix + DenseArray rowSums = Primitive64Array.FACTORY.make(matrix.length); // container for row sums + matrixPrimitive.reduceRows(Aggregator.SUM, rowSums); // populate row sums into rowSums + Primitive64Matrix.SparseReceiver sr = Primitive64Matrix.FACTORY.makeSparse(matrix.length, matrix.length); // container for degree matrix + sr.fillDiagonal(rowSums); // put row sums onto diagonal + Primitive64Matrix laplacian = sr.get(); // put row sum container into a new degree matrix (the future Laplacian matrix) + laplacian.subtract(matrixPrimitive); // subtract adjacency matrix from degree matrix to create Laplacian matrix + Eigenvalue eig = Eigenvalue.PRIMITIVE.make(laplacian); // eigenvalues + eig.decompose(laplacian); // decomposition + double[] eigenvalues = eig.getEigenvalues().toRawCopy1D(); // extract eigenvalues and convert to double[] + double eigenvaluesSum = Arrays.stream(eigenvalues).sum(); // compute sum of eigenvalues + if (eigenvaluesSum > 0.0) { + eigenvalues = DoubleStream.of(eigenvalues).map(v -> v / eigenvaluesSum).toArray(); // normalise/scale to one + } + + return eigenvalues; + } + + /** + * Compute penalized Euclidean spectral distance. + * + * @param eigenvalues1 Normalized eigenvalues of the full matrix. + * @param eigenvalues2 Normalized eigenvalues of the current or candidate matrix. + * @param p The penalty parameter. Typical values could be {@code 5.5}, {@code 7.5}, or {@code 12}, for example. + * @param candidateBackboneSize The number of entities in the current or candidate backbone. + * @param numEntitiesTotal The number of second-mode entities (e.g., concepts) in total. + * @return Penalized loss. + */ + private double penalizedLoss(double[] eigenvalues1, double[] eigenvalues2, double p, int candidateBackboneSize, int numEntitiesTotal) { + double distance = 0.0; // Euclidean spectral distance + for (int i = 0; i < eigenvalues1.length; i++) { + distance = distance + Math.sqrt((eigenvalues1[i] - eigenvalues2[i]) * (eigenvalues1[i] - eigenvalues2[i])); + } + double penalty = Math.exp(-p * (((double) (numEntitiesTotal - candidateBackboneSize)) / ((double) numEntitiesTotal))); // compute penalty factor + return distance * penalty; // return penalised distance + } + + /** + * Write the backbone results to a JSON or XML file + * + * @param filename File name with absolute path as a string. + */ + public void writeBackboneToFile(String filename) { + File file = new File(filename); + String s = ""; + + if (filename.toLowerCase().endsWith(".xml")) { + XStream xstream = new XStream(new StaxDriver()); + xstream.processAnnotations(SimulatedAnnealingBackboneResult.class); + StringWriter stringWriter = new StringWriter(); + if (this.nestedBackboneResult != null) { + xstream.marshal(this.nestedBackboneResult, new PrettyPrintWriter(stringWriter)); + } else if (this.simulatedAnnealingBackboneResult != null) { + xstream.marshal(this.simulatedAnnealingBackboneResult, new PrettyPrintWriter(stringWriter)); + } + s = stringWriter.toString(); + } else if (filename.toLowerCase().endsWith(".json")) { + Gson prettyGson = new GsonBuilder() + .setPrettyPrinting() + .serializeNulls() + .disableHtmlEscaping() + .create(); + if (this.nestedBackboneResult != null) { + s = prettyGson.toJson(this.nestedBackboneResult); + } else if (this.simulatedAnnealingBackboneResult != null) { + s = prettyGson.toJson(this.simulatedAnnealingBackboneResult); + } + } + try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) { + writer.write(s); + LogEvent l = new LogEvent(Logger.MESSAGE, + "Backbone result was saved to disk.", + "Backbone result was saved to file: " + filename + "."); + Dna.logger.log(l); + } catch (IOException exception) { + LogEvent l = new LogEvent(Logger.ERROR, + "Backbone result could not be saved to disk.", + "Attempted to save backbone results to file: \"" + filename + "\". The file saving operation did not work, possibly because the file could not be written to disk or because the results could not be converted to the final data format.", + exception); + Dna.logger.log(l); + } + } + + /** + * Compute penalized Euclidean spectral distance. + * + * @param eigenvalues1 Normalized eigenvalues of the full matrix. + * @param eigenvalues2 Normalized eigenvalues of the current or candidate matrix. + * @return Spectral loss. + */ + private double spectralLoss(double[] eigenvalues1, double[] eigenvalues2) { + double distance = 0.0; // Euclidean spectral distance + for (int i = 0; i < eigenvalues1.length; i++) { + distance = distance + Math.sqrt((eigenvalues1[i] - eigenvalues2[i]) * (eigenvalues1[i] - eigenvalues2[i])); + } + return distance; + } + + /** + * Get the size of the current backbone list. + * + * @return Backbone size at current iteration. + */ + public int getBackboneSize() { + return this.currentBackboneList.size(); + } + + /** + * Get the number of variable 2 entities used for computing the full matrix (i.e., after filtering). + * + * @return Number of entities. + */ + public int getFullSize() { + return this.extractLabels(this.filteredStatements, this.variable2, this.variable2Document).length; + } + + /** + * Initialize the nested backbone algorithm by setting up the data structures. + */ + public void initializeNestedBackbone() { + this.isolates = false; // no isolates initially for full matrix; will be set to true after full matrix has been computed + + // initial values before iterations start + this.originalStatements = this.filteredStatements; // to ensure not all isolates are included later + + // full set of concepts C + fullConcepts = this.extractLabels(this.filteredStatements, this.variable2, this.variable2Document); + + // full network matrix Y against which we compare in every iteration + fullMatrix = this.computeOneModeMatrix(this.filteredStatements, this.qualifierAggregation, this.startDateTime, this.stopDateTime); + this.isolates = true; // include isolates in the iterations but not in the full matrix; will be adjusted to smaller full matrix dimensions without isolates manually each time in the iterations; necessary because some actors may be deleted in the backbone matrix otherwise after deleting their concepts + + // compute normalised eigenvalues for the full matrix; no need to recompute every time as they do not change + eigenvaluesFull = computeNormalizedEigenvalues(fullMatrix.getMatrix()); + iteration = new int[fullConcepts.length]; + backboneLoss = new double[fullConcepts.length]; + redundantLoss = new double[fullConcepts.length]; + entity = new String[fullConcepts.length]; + ArrayList allConcepts = new ArrayList<>(); // convert fullConcepts to array to populate backbone concepts + for (int i = 0; i < fullConcepts.length; i++) { + allConcepts.add(fullConcepts[i]); + } + currentBackboneList = new ArrayList<>(allConcepts); + currentRedundantList = new ArrayList<>(); + backboneMatrices = new ArrayList<>(); + redundantMatrices = new ArrayList<>(); + numStatements = new int[fullConcepts.length]; + counter = 0; + } + + /** + * One iteration in the nested backbone algorithm. Needs to be called in a while loop until the backbone set is empty ({@code while (currentBackboneSet.size() > 0)}). + */ + public void iterateNestedBackbone() { + ArrayList candidateMatrices = new ArrayList<>(); + double[] currentLosses = new double[currentBackboneList.size()]; + int[] numStatementsCandidates = new int[currentBackboneList.size()]; + for (int i = 0; i < currentBackboneList.size(); i++) { + ArrayList candidate = new ArrayList<>(currentBackboneList); + candidate.remove(i); + final ArrayList finalCandidate = new ArrayList(candidate); // make it final, so it can be used in a stream + candidateStatementList = this.filteredStatements + .stream() + .filter(s -> finalCandidate.contains(((Entity) s.get(this.variable2)).getValue())) + .collect(Collectors.toCollection(ArrayList::new)); + numStatementsCandidates[i] = candidateStatementList.size(); + candidateMatrix = this.computeOneModeMatrix(candidateStatementList, this.qualifierAggregation, this.startDateTime, this.stopDateTime); + candidateMatrix = this.reduceCandidateMatrix(candidateMatrix, fullMatrix.getRowNames()); // ensure it has the right dimensions by purging isolates relative to the full matrix + candidateMatrices.add(candidateMatrix); + eigenvaluesCandidate = computeNormalizedEigenvalues(candidateMatrix.getMatrix()); // normalised eigenvalues for the candidate matrix + currentLosses[i] = spectralLoss(eigenvaluesFull, eigenvaluesCandidate); + } + double smallestLoss = 0.0; + if (currentBackboneList.size() > 0) { + smallestLoss = Arrays.stream(currentLosses).min().getAsDouble(); + } + for (int i = currentBackboneList.size() - 1; i >= 0; i--) { + if (currentLosses[i] == smallestLoss) { + iteration[counter] = counter + 1; + entity[counter] = currentBackboneList.get(i); + backboneLoss[counter] = smallestLoss; + currentRedundantList.add(currentBackboneList.get(i)); + currentBackboneList.remove(i); + backboneMatrices.add(candidateMatrices.get(i)); + + // compute redundant matrix and loss at this level + final ArrayList finalRedundantCandidate = new ArrayList(currentRedundantList); + candidateStatementList = this.filteredStatements + .stream() + .filter(s -> finalRedundantCandidate.contains(((Entity) s.get(this.variable2)).getValue())) + .collect(Collectors.toCollection(ArrayList::new)); + Matrix redundantMatrix = this.computeOneModeMatrix(candidateStatementList, this.qualifierAggregation, this.startDateTime, this.stopDateTime); + redundantMatrix = this.reduceCandidateMatrix(redundantMatrix, fullMatrix.getRowNames()); + redundantMatrices.add(redundantMatrix); + eigenvaluesCandidate = computeNormalizedEigenvalues(redundantMatrix.getMatrix()); + redundantLoss[counter] = spectralLoss(eigenvaluesFull, eigenvaluesCandidate); + numStatements[counter] = numStatementsCandidates[i]; + counter++; + } + } + } + + /** + * Get the nested backbone result that is saved in the class. + * + * @return The nested backbone result (can be null if backbone function has not been executed). + */ + public NestedBackboneResult getNestedBackboneResult() { + return this.nestedBackboneResult; + } + + /** + * Compute matrix after final backbone iteration, collect results, and save in class. + */ + public void saveNestedBackboneResult() { + Exporter.this.nestedBackboneResult = new NestedBackboneResult("nested", + iteration, + entity, + backboneLoss, + redundantLoss, + numStatements, + this.filteredStatements.size(), + fullMatrix.getStart().toEpochSecond(ZoneOffset.UTC), + fullMatrix.getStop().toEpochSecond(ZoneOffset.UTC)); + Exporter.this.simulatedAnnealingBackboneResult = null; + } + /** * For a vector of document IDs (the already coded set of documents), generate a number of sequences of cumulative * additions to the network according to normalized Euclidean network distances between consecutively sampled @@ -2756,12 +3107,15 @@ private double normalizedEuclideanNetworkDistance(double[][] matrix1, double[][] * Partition the discourse network into a backbone and redundant set of second-mode entities using penalised * spectral distances and simulated annealing. This method prepares the data before the algorithm starts. * - * @param p Penalty parameter. + * @param penalty Use penalty parameter? False if fixed backbone set. + * @param p Penalty parameter. Only used if penalty parameter is true. * @param T Number of iterations. + * @param size The (fixed) size of the backbone set. Only used if no penalty. */ - public void backbone(double p, int T) { + public void initializeSimulatedAnnealingBackbone(boolean penalty, double p, int T, int size) { this.p = p; this.T = T; + this.backboneSize = size; this.isolates = false; // no isolates initially for full matrix; will be set to true after full matrix has been computed // initial values before iterations start @@ -2777,20 +3131,44 @@ public void backbone(double p, int T) { // compute normalised eigenvalues for the full matrix; no need to recompute every time as they do not change eigenvaluesFull = computeNormalizedEigenvalues(fullMatrix.getMatrix()); - // pick a random concept c_j from C and save its index - int randomConceptIndex = ThreadLocalRandom.current().nextInt(0, fullConcepts.length); + if (penalty) { // simulated annealing with penalty: initially one randomly chosen entity in the backbone set + // pick a random concept c_j from C and save its index + int randomConceptIndex = ThreadLocalRandom.current().nextInt(0, fullConcepts.length); - // final backbone list B, which contains only one random concept initially but will contain the final backbone set in the end - finalBackboneList = new ArrayList(); + // final backbone list B, which contains only one random concept initially but will contain the final backbone set in the end + finalBackboneList = new ArrayList(); - // add the one uniformly sampled concept c_j to the backbone as the initial solution at t = 0: B <- {c_j} - finalBackboneList.add(fullConcepts[randomConceptIndex]); + // add the one uniformly sampled concept c_j to the backbone as the initial solution at t = 0: B <- {c_j} + finalBackboneList.add(fullConcepts[randomConceptIndex]); - // final redundant set R, which is initially C without c_j - finalRedundantList = Arrays - .stream(fullConcepts) - .filter(c -> !c.equals(fullConcepts[randomConceptIndex])) - .collect(Collectors.toCollection(ArrayList::new)); + // final redundant set R, which is initially C without c_j + finalRedundantList = Arrays + .stream(fullConcepts) + .filter(c -> !c.equals(fullConcepts[randomConceptIndex])) + .collect(Collectors.toCollection(ArrayList::new)); + } else { // simulated annealing without penalty and fixed backbone set size: randomly sample as many initial entities as needed + // sample initial backbone set randomly + if (this.backboneSize > fullConcepts.length) { + LogEvent l = new LogEvent(Logger.ERROR, + "Backbone size parameter too large", + "The backbone size parameter of " + this.backboneSize + " is larger than the number of entities on the second mode, " + fullConcepts.length + ". It is impossible to choose a backbone set of that size. Please choose a smaller backbone size."); + Dna.logger.log(l); + } else if (this.backboneSize < 1) { + LogEvent l = new LogEvent(Logger.ERROR, + "Backbone size parameter too small", + "The backbone size parameter of " + size + " is smaller than 1. It is impossible to choose a backbone set of that size. Please choose a larger backbone size."); + Dna.logger.log(l); + } + finalBackboneList = new ArrayList<>(); + while (finalBackboneList.size() < this.backboneSize) { + int randomConceptIndex = ThreadLocalRandom.current().nextInt(0, fullConcepts.length); + String entity = fullConcepts[randomConceptIndex]; + if (!finalBackboneList.contains(entity)) { + finalBackboneList.add(entity); + } + } + finalRedundantList = Stream.of(fullConcepts).filter(c -> !finalBackboneList.contains(c)).collect(Collectors.toCollection(ArrayList::new)); + } // final statement list: filter the statement list by only retaining those statements that are in the final backbone set B finalStatementList = this.filteredStatements @@ -2800,16 +3178,16 @@ public void backbone(double p, int T) { // final matrix based on the initial final backbone set, Y^B, which is initially identical to the previous matrix finalMatrix = this.computeOneModeMatrix(finalStatementList, this.qualifierAggregation, this.startDateTime, this.stopDateTime); + finalMatrix = this.reduceCandidateMatrix(finalMatrix, fullMatrix.getRowNames()); // ensure it has the right dimensions by purging isolates relative to the full matrix + + // eigenvalues for final matrix + eigenvaluesFinal = computeNormalizedEigenvalues(finalMatrix.getMatrix()); // normalised eigenvalues for the candidate matrix // create an initial current backbone set B_0, also with the one c_j concept like in B: B_0 <- {c_j} - currentBackboneList = new ArrayList(); - currentBackboneList.add(fullConcepts[randomConceptIndex]); + currentBackboneList = new ArrayList(finalBackboneList); // create an initial current redundant set R_t, which is C without c_j - currentRedundantList = Arrays - .stream(fullConcepts) - .filter(c -> !c.equals(fullConcepts[randomConceptIndex])) - .collect(Collectors.toCollection(ArrayList::new)); + currentRedundantList = new ArrayList(finalRedundantList); // filter the statement list by only retaining those statements that are in the initial current backbone set B_0 currentStatementList = this.filteredStatements @@ -2818,7 +3196,10 @@ public void backbone(double p, int T) { .collect(Collectors.toCollection(ArrayList::new)); // create initial current matrix at t = 0 - currentMatrix = this.computeOneModeMatrix(currentStatementList, this.qualifierAggregation, this.startDateTime, this.stopDateTime); + currentMatrix = new Matrix(finalMatrix); + + // initial current eigenvalues + eigenvaluesCurrent = eigenvaluesFinal; // initialise (empty) action set S this.actionList = new ArrayList(); @@ -2833,16 +3214,20 @@ public void backbone(double p, int T) { candidateRedundantList = new ArrayList(); // declare loss comparison result variables - oldLoss = 0.0; + if (penalty) { + finalLoss = penalizedLoss(eigenvaluesFull, eigenvaluesFinal, p, currentBackboneList.size(), fullConcepts.length); // spectral distance between full and initial matrix + } else { + finalLoss = spectralLoss(eigenvaluesFull, eigenvaluesFinal); // spectral distance between full and initial matrix + } + oldLoss = finalLoss; newLoss = 0.0; - finalLoss = 0.0; accept = false; // reporting temperatureLog = new ArrayList(); acceptanceProbabilityLog = new ArrayList(); acceptedLog = new ArrayList(); - penalizedBackboneLossLog = new ArrayList(); + penalizedBackboneLossLog = new ArrayList(); // penalised or not penalised, depending on algorithm proposedBackboneSizeLog = new ArrayList(); acceptedBackboneSizeLog = new ArrayList(); finalBackboneSizeLog = new ArrayList(); @@ -2851,44 +3236,32 @@ public void backbone(double p, int T) { // matrix algebra declarations eigenvaluesCurrent = new double[0]; - // set to first iteration and start simulated annealing + // set to first iteration before starting simulated annealing t = 1; } - /** - * Get the current iteration {@code t} of the simulated annealing algorithm. - * - * @return Current iteration {@code t}. - */ - public int getCurrentT() { - return this.t; - } - - /** - * Set the current iteration {@code t} of the simulated annealing algorithm. - * - * @return Current iteration {@code t}. - */ - public void setCurrentT(int t) { - this.t = t; - } - /** * Execute the next iteration of the simulated annealing backbone algorithm. */ - public void iterateBackbone() { - // first step: make a random move by adding, removing, or swapping a concept and computing a new candidate + public void iterateSimulatedAnnealingBackbone(boolean penalty) { + // calculate temperature + temperature = 1 - (1 / (1 + Math.exp(-(-5 + (12.0 / T) * t)))); // temperature + temperatureLog.add(temperature); + + // make a random move by adding, removing, or swapping a concept and computing a new candidate actionList.clear(); // clear the set of possible actions and repopulate, depending on solution size - if (currentBackboneList.size() < 2) { // if there is only one concept, don't remove it because empty backbones do not work + if (currentBackboneList.size() < 2 && penalty) { // if there is only one concept, don't remove it because empty backbones do not work actionList.add("add"); actionList.add("swap"); - } else if (currentBackboneList.size() > fullConcepts.length - 2) { // do not create a backbone with all concepts because it would be useless + } else if (currentBackboneList.size() > fullConcepts.length - 2 && penalty) { // do not create a backbone with all concepts because it would be useless actionList.add("remove"); actionList.add("swap"); - } else { // everything in between one and |C| - 1 concepts: add all three possible moves to the action set + } else if (penalty) { // everything in between one and |C| - 1 concepts: add all three possible moves to the action set actionList.add("add"); actionList.add("remove"); actionList.add("swap"); + } else { // with fixed backbone set (i.e., no penalty), only allow horizontal swaps + actionList.add("swap"); } Collections.shuffle(actionList); // randomly re-order the action set... selectedAction = actionList.get(0); // and draw the first action (i.e., pick a random action) @@ -2913,29 +3286,27 @@ public void iterateBackbone() { candidateBackboneList.remove(0); // then remove it from the backbone list } proposedBackboneSizeLog.add(candidateBackboneList.size()); // log number of concepts in candidate backbone in the current iteration - // after executing the action, filter the statement list based on the candidate backbone set B^*_t in order to create the candidate matrix + + // after executing the action, filter the statement list based on the candidate backbone set B^*_t in order to create the candidate matrix, then compute eigenvalues and loss for the candidate candidateStatementList = this.filteredStatements .stream() .filter(s -> candidateBackboneList.contains(((Entity) s.get(this.variable2)).getValue())) .collect(Collectors.toCollection(ArrayList::new)); - // create candidate matrix after filtering the statements based on the action that was executed - candidateMatrix = this.computeOneModeMatrix(candidateStatementList, this.qualifierAggregation, this.startDateTime, this.stopDateTime); + candidateMatrix = this.computeOneModeMatrix(candidateStatementList, this.qualifierAggregation, this.startDateTime, this.stopDateTime); // create candidate matrix after filtering the statements based on the action that was executed candidateMatrix = this.reduceCandidateMatrix(candidateMatrix, fullMatrix.getRowNames()); // ensure it has the right dimensions by purging isolates relative to the full matrix - - // second step: compare loss between full and previous matrix to loss between full and candidate matrix and accept or reject candidate - temperature = 1 - (1 / (1 + Math.exp(-(-5 + (12.0 / T) * t)))); // temperature - temperatureLog.add(temperature); - eigenvaluesCurrent = computeNormalizedEigenvalues(currentMatrix.getMatrix()); // normalised eigenvalues for the current matrix eigenvaluesCandidate = computeNormalizedEigenvalues(candidateMatrix.getMatrix()); // normalised eigenvalues for the candidate matrix - oldLoss = penalizedLoss(eigenvaluesFull, eigenvaluesCurrent, p, currentBackboneList.size(), fullConcepts.length); // spectral distance between full and previous matrix - newLoss = penalizedLoss(eigenvaluesFull, eigenvaluesCandidate, p, candidateBackboneList.size(), fullConcepts.length); // spectral distance between full and candidate matrix + if (penalty) { + newLoss = penalizedLoss(eigenvaluesFull, eigenvaluesCandidate, p, candidateBackboneList.size(), fullConcepts.length); // spectral distance between full and candidate matrix + } else { + newLoss = spectralLoss(eigenvaluesFull, eigenvaluesCandidate); // spectral distance between full and candidate matrix + } penalizedBackboneLossLog.add(newLoss); // log the penalised spectral distance between full and candidate solution + + // compare loss between full and previous (current) matrix to loss between full and candidate matrix and accept or reject candidate accept = false; if (newLoss < oldLoss) { // if candidate is better than previous matrix, adopt it as current solution accept = true; // flag this solution for acceptance acceptanceProbabilityLog.add(-1.0); // log the acceptance probability as -1.0; technically it should be 1.0 because the solution was better and hence accepted, but it would be useless for plotting the acceptance probabilities as a diagnostic tool - eigenvaluesFinal = computeNormalizedEigenvalues(currentMatrix.getMatrix()); // normalised eigenvalues for the current matrix - finalLoss = penalizedLoss(eigenvaluesFull, eigenvaluesFinal, p, finalBackboneList.size(), fullConcepts.length); // test if also better than global optimum so far if (newLoss <= finalLoss) { // if better than the best solution, adopt candidate as new final backbone solution finalBackboneList.clear(); // clear the best solution list finalBackboneList.addAll(candidateBackboneList); // and populate it with the concepts from the candidate solution instead @@ -2944,6 +3315,8 @@ public void iterateBackbone() { finalStatementList.clear(); // same with the final list of statements finalStatementList.addAll(candidateStatementList); finalMatrix = new Matrix(candidateMatrix); // save the candidate matrix as best solution matrix + eigenvaluesFinal = eigenvaluesCandidate; + finalLoss = newLoss; // save the candidate loss as the globally optimal loss so far } } else { // if the solution is worse than the previous one, apply Hastings ratio and temperature and compare with random number r = Math.random(); // random double between 0 and 1 @@ -2961,6 +3334,8 @@ public void iterateBackbone() { currentStatementList.clear(); // save candidate statement list as new current statement list currentStatementList.addAll(candidateStatementList); currentMatrix = new Matrix(candidateMatrix); // save candidate matrix as new current matrix + eigenvaluesCurrent = eigenvaluesCandidate; + oldLoss = newLoss; // save the corresponding candidate loss as the current/old loss acceptedLog.add(1); // log the acceptance of the proposed candidate } else { acceptedLog.add(0); // log the non-acceptance of the proposed candidate @@ -2976,166 +3351,62 @@ public void iterateBackbone() { } /** - * Reduce the dimensions of a candidate matrix with all isolate nodes to the dimensions of the full matrix, which - * does not contain isolate nodes. + * Compute the spectral distance between the full network and the network based only on the backbone set and only the redundant set. The penalty parameter can be switched off by setting it to zero. * - * @param candidateMatrix The candidate matrix with isolates (to be reduced to smaller dimensions). - * @param fullLabels The node labels of the full matrix without isolates. - * @return A reduced candidate matrix with the same dimensions as the full matrix and the same node order. + * @param backboneEntities An array of entities (e.g., concepts) to construct a backbone set for computing the spectral distance. + * @param p The penalty parameter. Can be \code{0} to switch off the penalty parameter. + * @return A double array with the penalized loss for the backbone set and the redundant set. */ - private Matrix reduceCandidateMatrix(Matrix candidateMatrix, String[] fullLabels) { - HashMap map = new HashMap(); - for (int i = 0; i < fullLabels.length; i++) { - for (int j = 0; j < candidateMatrix.getRowNames().length; j++) { - if (fullLabels[i].equals(candidateMatrix.getRowNames()[j])) { - map.put(i, j); - } - } - } - double[][] mat = new double[fullLabels.length][fullLabels.length]; - for (int i = 0; i < fullLabels.length; i++) { - for (int j = 0; j < fullLabels.length; j++) { - mat[i][j] = candidateMatrix.getMatrix()[map.get(i)][map.get(j)]; - } - } - candidateMatrix.setMatrix(mat); - candidateMatrix.setRowNames(fullLabels); - candidateMatrix.setColumnNames(fullLabels); - return candidateMatrix; - } + public double[] evaluateBackboneSolution(String[] backboneEntities, int p) { + this.p = p; + double[] results = new double[2]; + this.isolates = false; // no isolates initially for full matrix; will be set to true after full matrix has been computed - /** - * Compute matrix after final backbone iteration, collect results, and save in class. - */ - public void saveBackboneResult() { - Collections.sort(finalBackboneList); - Collections.sort(finalRedundantList); + // initial values before iterations start + this.originalStatements = this.filteredStatements; // to ensure not all isolates are included later - // create redundant matrix - ArrayList redundantStatementList = this.filteredStatements - .stream() - .filter(s -> currentRedundantList.contains(((Entity) s.get(this.variable2)).getValue())) - .collect(Collectors.toCollection(ArrayList::new)); - Matrix redundantMatrix = this.computeOneModeMatrix(redundantStatementList, this.qualifierAggregation, this.startDateTime, this.stopDateTime); + // full set of concepts C + fullConcepts = this.extractLabels(this.filteredStatements, this.variable2, this.variable2Document); - this.backboneResult = new BackboneResult(finalBackboneList.toArray(String[]::new), - finalRedundantList.toArray(String[]::new), - penalizedLoss(eigenvaluesFull, eigenvaluesCurrent, 0, currentBackboneList.size(), fullConcepts.length), - penalizedLoss(eigenvaluesFull, computeNormalizedEigenvalues(redundantMatrix.getMatrix()), 0, currentBackboneList.size(), fullConcepts.length), - p, - T, - temperatureLog.stream().mapToDouble(v -> v.doubleValue()).toArray(), - acceptanceProbabilityLog.stream().mapToDouble(v -> v.doubleValue()).toArray(), - acceptedLog.stream().mapToInt(v -> v.intValue()).toArray(), - penalizedBackboneLossLog.stream().mapToDouble(v -> v.doubleValue()).toArray(), - proposedBackboneSizeLog.stream().mapToInt(v -> v.intValue()).toArray(), - acceptedBackboneSizeLog.stream().mapToInt(v -> v.intValue()).toArray(), - finalBackboneSizeLog.stream().mapToInt(v -> v.intValue()).toArray(), - acceptanceRatioLastHundredIterationsLog.stream().mapToDouble(v -> v.doubleValue()).toArray(), - fullMatrix.getMatrix(), - currentMatrix.getMatrix(), - redundantMatrix.getMatrix(), - fullMatrix.getRowNames(), - fullMatrix.getStart().toEpochSecond(ZoneOffset.UTC), - fullMatrix.getStop().toEpochSecond(ZoneOffset.UTC), - fullMatrix.getNumStatements()); - } + // full network matrix Y against which we compare in every iteration + fullMatrix = this.computeOneModeMatrix(this.filteredStatements, this.qualifierAggregation, this.startDateTime, this.stopDateTime); + this.isolates = true; // include isolates in the iterations; will be adjusted to full matrix without isolates manually each time - /** - * Get the backbone result that is saved in the class. - * - * @return The backbone result (can be null if backbone function has not been executed). - */ - public BackboneResult getBackboneResult() { - return this.backboneResult; - } + // compute normalised eigenvalues for the full matrix; no need to recompute every time as they do not change + eigenvaluesFull = computeNormalizedEigenvalues(fullMatrix.getMatrix()); - /** - * Use tools from the {@code ojalgo} library to compute eigenvalues of a symmetric matrix. - * - * @param matrix The matrix as a two-dimensional double array. - * @return One-dimensional double array of eigenvalues. - */ - private double[] computeNormalizedEigenvalues(double[][] matrix) { - for (int i = 0; i < matrix.length; i++) { - for (int j = 0; j < matrix[0].length; j++) { - if (matrix[i][j] < 0) { - matrix[i][j] = 0.0; - } + // create copy of filtered statements and remove redundant entities + ArrayList entityList = Stream.of(backboneEntities).collect(Collectors.toCollection(ArrayList::new)); + ArrayList backboneSet = new ArrayList<>(); + ArrayList redundantSet = new ArrayList<>(); + for (int i = 0; i < fullConcepts.length; i++) { + if (entityList.contains(fullConcepts[i])) { + backboneSet.add(fullConcepts[i]); + } else { + redundantSet.add(fullConcepts[i]); } } - Primitive64Matrix matrixPrimitive = Primitive64Matrix.FACTORY.rows(matrix); // create matrix - DenseArray rowSums = Primitive64Array.FACTORY.make(matrix.length); // container for row sums - matrixPrimitive.reduceRows(Aggregator.SUM, rowSums); // populate row sums into rowSums - Primitive64Matrix.SparseReceiver sr = Primitive64Matrix.FACTORY.makeSparse(matrix.length, matrix.length); // container for degree matrix - sr.fillDiagonal(rowSums); // put row sums onto diagonal - Primitive64Matrix laplacian = sr.get(); // put row sum container into a new degree matrix (the future Laplacian matrix) - laplacian.subtract(matrixPrimitive); // subtract adjacency matrix from degree matrix to create Laplacian matrix - Eigenvalue eig = Eigenvalue.PRIMITIVE.make(laplacian); // eigenvalues - eig.decompose(laplacian); // decomposition - double[] eigenvalues = eig.getEigenvalues().toRawCopy1D(); // extract eigenvalues and convert to double[] - double eigenvaluesSum = Arrays.stream(eigenvalues).sum(); // compute sum of eigenvalues - if (eigenvaluesSum > 0.0) { - eigenvalues = DoubleStream.of(eigenvalues).map(v -> v / eigenvaluesSum).toArray(); // normalise/scale to one - } - return eigenvalues; - } - - /** - * Compute penalized Euclidean spectral distance. - * - * @param eigenvalues1 Normalized eigenvalues of the full matrix. - * @param eigenvalues2 Normalized eigenvalues of the current or candidate matrix. - * @param p The penalty parameter. Typical values could be {@code 5.5}, {@code 7.5}, or {@code 12}, for example. Use {@code 0} to switch off penalty. - * @param candidateBackboneSize The number of entities in the current or candidate backbone. - * @param numEntitiesTotal The number of second-mode entities (e.g., concepts) in total. - * @return Penalized loss. - */ - private double penalizedLoss(double[] eigenvalues1, double[] eigenvalues2, double p, int candidateBackboneSize, int numEntitiesTotal) { - double distance = 0.0; // Euclidean spectral distance - for (int i = 0; i < eigenvalues1.length; i++) { - distance = distance + Math.sqrt((eigenvalues1[i] - eigenvalues2[i]) * (eigenvalues1[i] - eigenvalues2[i])); - } - double penalty = Math.exp(-p * (((double) (numEntitiesTotal - candidateBackboneSize)) / ((double) numEntitiesTotal))); // compute penalty factor - return distance * penalty; // return penalised distance - } + // spectral distance between full and backbone set + candidateStatementList = this.filteredStatements + .stream() + .filter(s -> backboneSet.contains(((Entity) s.get(this.variable2)).getValue())) + .collect(Collectors.toCollection(ArrayList::new)); + candidateMatrix = this.computeOneModeMatrix(candidateStatementList, this.qualifierAggregation, this.startDateTime, this.stopDateTime); // create candidate matrix after filtering the statements based on the action that was executed + candidateMatrix = this.reduceCandidateMatrix(candidateMatrix, fullMatrix.getRowNames()); // ensure it has the right dimensions by purging isolates relative to the full matrix + eigenvaluesCandidate = computeNormalizedEigenvalues(candidateMatrix.getMatrix()); // normalised eigenvalues for the candidate matrix + results[0] = penalizedLoss(eigenvaluesFull, eigenvaluesCandidate, p, backboneSet.size(), fullConcepts.length); // spectral distance between full and candidate matrix - /** - * Write the backbone results to a JSON or XML file - * - * @param filename File name with absolute path as a string. - */ - public void writeBackboneToFile(String filename) { - File file = new File(filename); - String s = ""; + // spectral distance between full and redundant set + candidateStatementList = this.filteredStatements + .stream() + .filter(s -> redundantSet.contains(((Entity) s.get(this.variable2)).getValue())) + .collect(Collectors.toCollection(ArrayList::new)); + candidateMatrix = this.computeOneModeMatrix(candidateStatementList, this.qualifierAggregation, this.startDateTime, this.stopDateTime); // create candidate matrix after filtering the statements based on the action that was executed + candidateMatrix = this.reduceCandidateMatrix(candidateMatrix, fullMatrix.getRowNames()); // ensure it has the right dimensions by purging isolates relative to the full matrix + eigenvaluesCandidate = computeNormalizedEigenvalues(candidateMatrix.getMatrix()); // normalised eigenvalues for the candidate matrix + results[1] = penalizedLoss(eigenvaluesFull, eigenvaluesCandidate, p, redundantSet.size(), fullConcepts.length); // spectral distance between full and candidate matrix - if (filename.toLowerCase().endsWith(".xml")) { - XStream xstream = new XStream(new StaxDriver()); - xstream.processAnnotations(BackboneResult.class); - StringWriter stringWriter = new StringWriter(); - xstream.marshal(this.backboneResult, new PrettyPrintWriter(stringWriter)); - s = stringWriter.toString(); - } else if (filename.toLowerCase().endsWith(".json")) { - Gson prettyGson = new GsonBuilder() - .setPrettyPrinting() - .serializeNulls() - .disableHtmlEscaping() - .create(); - s = prettyGson.toJson(this.backboneResult); - } - try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) { - writer.write(s); - LogEvent l = new LogEvent(Logger.MESSAGE, - "Backbone result was saved to disk.", - "Backbone result was saved to file: " + filename + "."); - Dna.logger.log(l); - } catch (IOException exception) { - LogEvent l = new LogEvent(Logger.ERROR, - "Backbone result could not be saved to disk.", - "Attempted to save backbone results to file: \"" + filename + "\". The file saving operation did not work, possibly because the file could not be written to disk or because the results could not be converted to the final data format.", - exception); - Dna.logger.log(l); - } + return results; } } \ No newline at end of file diff --git a/dna/src/main/java/export/NestedBackboneResult.java b/dna/src/main/java/export/NestedBackboneResult.java new file mode 100644 index 00000000..18e4b2ca --- /dev/null +++ b/dna/src/main/java/export/NestedBackboneResult.java @@ -0,0 +1,104 @@ +package export; + +import java.util.ArrayList; + +public class NestedBackboneResult { + + /** + * The algorithm used to compute the results. Can be {@code "nested"} (for a nested, agglomerative method), + * {@code "all"} (for simulated annealing with all backbone sizes and no penalty), {@code "size"} (for simulated + * annealing for a specific backbone size with no penalty), or {@code "penalty"} (for a penalized simulated + * annealing approach). + */ + private String method; + + /** + * Iteration from 1 to the final size of the redundant set. + */ + private int[] iteration; + + /** + * The entity for which the result holds. + */ + private String[] entities; + + /** + * Euclidean spectral distance between the full network and the backbone network at each backbone size, without + * penalty factor for the number of entities in the set, after optimization. + */ + private double[] backboneLoss; + + /** + * Euclidean spectral distance between the full network and the redundant network at each backbone size, without + * penalty factor for the number of entities in the set, after optimization. + */ + private double[] redundantLoss; + + /** + * Number of statements used to create the backbone networks. + */ + private int[] numStatements; + + /** + * Number of statements used to create the full network. + */ + private int numStatementsFull; + + /** + * Start date/time of the network in seconds since 1 January 1970. + */ + private long start; + + /** + * End date/time of the network in seconds since 1 January 1970. + */ + private long stop; + + public NestedBackboneResult(String method, int[] iteration, String[] entities, double[] backboneLoss, double[] redundantLoss, int[] numStatements, int numStatementsFull, long start, long stop) { + this.method = method; + this.iteration = iteration; + this.entities = entities; + this.backboneLoss = backboneLoss; + this.redundantLoss = redundantLoss; + this.numStatementsFull = numStatementsFull; + this.numStatements = numStatements; + this.start = start; + this.stop = stop; + } + + public String getMethod() { + return method; + } + + public int[] getIteration() { + return iteration; + } + + public String[] getEntities() { + return entities; + } + + public double[] getBackboneLoss() { + return backboneLoss; + } + + public double[] getRedundantLoss() { + return redundantLoss; + } + + public int getNumStatementsFull() { + return numStatementsFull; + } + + public int[] getNumStatements() { + return numStatements; + } + + public long getStart() { + return start; + } + + public long getStop() { + return stop; + } +} diff --git a/dna/src/main/java/export/BackboneResult.java b/dna/src/main/java/export/SimulatedAnnealingBackboneResult.java similarity index 83% rename from dna/src/main/java/export/BackboneResult.java rename to dna/src/main/java/export/SimulatedAnnealingBackboneResult.java index 79591081..9ff06d45 100644 --- a/dna/src/main/java/export/BackboneResult.java +++ b/dna/src/main/java/export/SimulatedAnnealingBackboneResult.java @@ -1,15 +1,21 @@ package export; import java.io.Serializable; -import java.time.LocalDateTime; -import java.util.ArrayList; /** * Class representing backbone results. */ -public class BackboneResult implements Serializable { +public class SimulatedAnnealingBackboneResult implements Serializable { private static final long serialVersionUID = -2275971337294798275L; + /** + * The algorithm used to compute the results. Can be {@code "nested"} (for a nested, agglomerative method), + * {@code "all"} (for simulated annealing with all backbone sizes and no penalty), {@code "size"} (for simulated + * annealing for a specific backbone size with no penalty), or {@code "penalty"} (for a penalized simulated + * annealing approach). + */ + private String method; + /** * The entities found to be in the backbone set, as an array of strings. */ @@ -124,6 +130,7 @@ public class BackboneResult implements Serializable { /** * Create a new backbone result. * + * @param method The backbone algorithm. * @param backboneEntities The entities found to be in the backbone set, as an array of strings. * @param redundantEntities The entities found to be in the redundant set, as an array of strings. * @param unpenalizedBackboneLoss Euclidean spectral distance between the full network and the backbone network @@ -155,28 +162,34 @@ public class BackboneResult implements Serializable { * array. * @param redundantNetwork The network matrix based only on the redundant concepts, after optimization as a 2D * double array. + * @param labels The network labels. + * @param start Start date and time. + * @param stop Stop date and time. + * @param numStatements The number of filtered statements contributing to the full network. */ - public BackboneResult(String[] backboneEntities, - String[] redundantEntities, - double unpenalizedBackboneLoss, - double unpenalizedRedundantLoss, - double penalty, - int iterations, - double[] temperature, - double[] acceptanceProbability, - int[] acceptance, - double[] penalizedBackboneLoss, - int[] proposedBackboneSize, - int[] currentBackboneSize, - int[] optimalBackboneSize, - double[] acceptanceRatioMovingAverage, - double[][] fullNetwork, - double[][] backboneNetwork, - double[][] redundantNetwork, - String[] labels, - long start, - long stop, - int numStatements) { + public SimulatedAnnealingBackboneResult(String method, + String[] backboneEntities, + String[] redundantEntities, + double unpenalizedBackboneLoss, + double unpenalizedRedundantLoss, + double penalty, + int iterations, + double[] temperature, + double[] acceptanceProbability, + int[] acceptance, + double[] penalizedBackboneLoss, + int[] proposedBackboneSize, + int[] currentBackboneSize, + int[] optimalBackboneSize, + double[] acceptanceRatioMovingAverage, + double[][] fullNetwork, + double[][] backboneNetwork, + double[][] redundantNetwork, + String[] labels, + long start, + long stop, + int numStatements) { + this.method = method; this.backboneEntities = backboneEntities; this.redundantEntities = redundantEntities; this.unpenalizedBackboneLoss = unpenalizedBackboneLoss; @@ -200,6 +213,10 @@ public BackboneResult(String[] backboneEntities, this.numStatements = numStatements; } + public String getMethod() { + return this.method; + } + public String[] getBackboneEntities() { return backboneEntities; } @@ -367,5 +384,4 @@ public int getNumStatements() { public void setNumStatements(int numStatements) { this.numStatements = numStatements; } - } \ No newline at end of file diff --git a/dna/src/main/java/gui/AttributeComboBoxRenderer.java b/dna/src/main/java/gui/AttributeComboBoxRenderer.java index 184ea0a9..80cfabee 100644 --- a/dna/src/main/java/gui/AttributeComboBoxRenderer.java +++ b/dna/src/main/java/gui/AttributeComboBoxRenderer.java @@ -22,7 +22,7 @@ class AttributeComboBoxRenderer implements ListCellRenderer { public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { Entity a = (Entity) value; JLabel label = new JLabel(a.getValue()); - label.setForeground(a.getColor()); + label.setForeground(a.getColor().toAWTColor()); // list background Color selectedColor = javax.swing.UIManager.getColor("List.selectionBackground"); diff --git a/dna/src/main/java/gui/AttributeManager.java b/dna/src/main/java/gui/AttributeManager.java index 1f263590..36a1ce18 100644 --- a/dna/src/main/java/gui/AttributeManager.java +++ b/dna/src/main/java/gui/AttributeManager.java @@ -219,7 +219,7 @@ public void actionPerformed(ActionEvent e) { map.put(model.getAttributeVariables().get(i), ""); } int variableId = ((Value) variableBox.getSelectedItem()).getVariableId(); - Entity entity = new Entity(-1, variableId, newField.getText(), Color.BLACK, -1, false, map); + Entity entity = new Entity(-1, variableId, newField.getText(), new model.Color(0, 0, 0), -1, false, map); Dna.sql.addEntity(entity); refreshTable(variableId); newField.setText(""); @@ -468,7 +468,7 @@ protected List doInBackground() { ResultSet r; // save entities in a list - Color color; + model.Color color; int entityId; s1.setInt(1, variableId); r = s1.executeQuery(); @@ -476,7 +476,7 @@ protected List doInBackground() { if (isCancelled() || isDone()) { return null; } - color = new Color(r.getInt("Red"), r.getInt("Green"), r.getInt("Blue")); + color = new model.Color(r.getInt("Red"), r.getInt("Green"), r.getInt("Blue")); entityId = r.getInt("ID"); indexMap.put(entityId, l.size()); l.add(new Entity(entityId, variableId, r.getString("Value"), color, r.getInt("ChildOf"), r.getInt("InDatabase") == 1, new HashMap())); @@ -635,8 +635,8 @@ public void setValueAt(Object aValue, int row, int col) { } } else if (col == 2) { // color try { - Dna.sql.setEntityColor(this.rows.get(row).getId(), (Color) aValue); - this.rows.get(row).setColor((Color) aValue); + Dna.sql.setEntityColor(this.rows.get(row).getId(), (model.Color) aValue); + this.rows.get(row).setColor((model.Color) aValue); } catch (SQLException ex) { LogEvent l = new LogEvent(Logger.ERROR, "[SQL] Entity color could not be updated in the database.", @@ -831,7 +831,7 @@ public Component getTableCellRendererComponent(JTable table, Object value, boole private static final long serialVersionUID = 1648028274961429514L; public void paintComponent(Graphics g) { super.paintComponent(g); - g.setColor(entity.getColor()); + g.setColor(entity.getColor().toAWTColor()); g.fillRect(0, 0, 30, 8); } }); diff --git a/dna/src/main/java/gui/BackboneExporter.java b/dna/src/main/java/gui/BackboneExporter.java index 48db72e1..a3f208dd 100644 --- a/dna/src/main/java/gui/BackboneExporter.java +++ b/dna/src/main/java/gui/BackboneExporter.java @@ -14,10 +14,11 @@ import javax.swing.*; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; -import javax.swing.filechooser.FileNameExtensionFilter; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; import java.io.File; import java.time.LocalDateTime; import java.util.ArrayList; @@ -30,7 +31,7 @@ * various kinds of networks. */ public class BackboneExporter extends JDialog { - private static final long serialVersionUID = 3774134211831948308L; + private static final long serialVersionUID = 3774134211831448308L; private LocalDateTime[] dateTimeRange = Dna.sql.getDateTimeRange(); private DateTimePicker startPicker, stopPicker; private JCheckBox helpBox; @@ -43,9 +44,12 @@ public class BackboneExporter extends JDialog { private ArrayList excludeAuthor, excludeSource, excludeSection, excludeType; private JTextArea excludePreviewArea; private Exporter exporter; + private GridBagLayout g; + private SpinnerNumberModel backboneSizeModel; + private JComboBox backboneMethodBox; /** - * Constructor for GUI. Opens an Exporter window, which displays the GUI for exporting network data. + * Constructor for GUI. Opens an Exporter window, which displays the GUI for exporting backbone data. */ public BackboneExporter(Frame parent) { super(parent, "Backbone finder", true); @@ -56,7 +60,7 @@ public BackboneExporter(Frame parent) { this.setLayout(new BorderLayout()); JPanel settingsPanel = new JPanel(); - GridBagLayout g = new GridBagLayout(); + g = new GridBagLayout(); settingsPanel.setLayout(g); GridBagConstraints gbc = new GridBagConstraints(); gbc.anchor = GridBagConstraints.WEST; @@ -74,25 +78,19 @@ public BackboneExporter(Frame parent) { + "Select here which statement type to use.

"; statementTypeLabel.setToolTipText(statementTypeToolTip); settingsPanel.add(statementTypeLabel, gbc); - - gbc.gridx = 1; - JLabel penaltyLabel = new JLabel("Penalty parameter (p)"); - String penaltyTooltipText = "

The penalty (p) determines how large the " + - "backbone set is, with larger penalties penalizing larger backbones more strongly, hence leading to " + - "smaller backbones and larger redundant sets. Typical values could be 5.5, 7.5, or 12.

"; - penaltyLabel.setToolTipText(penaltyTooltipText); - settingsPanel.add(penaltyLabel, gbc); - gbc.gridx = 2; - JLabel iterationsLabel = new JLabel("Iterations (T)"); - String iterationsTooltipText = "

The number of iterations (T) determines how long the " + - "algorithm should run. We have had good results with T = 50,000 iterations for a network of about " + - "200 actors and about 60 concepts, though fewer iterations might have been acceptable. The quality " + - "of the solution increases with larger T and reaches the global optimum asymptotically. How many " + - "iterations are required for a good solution depends on the number of entities on the second-mode " + - "variable.

"; - iterationsLabel.setToolTipText(iterationsTooltipText); - settingsPanel.add(iterationsLabel, gbc); + gbc.gridx = 1; + JLabel backboneLabel = new JLabel("Backbone method"); + String backboneMethodToolTip = + "

Which method should be used to partition the entities of the second variable (usually concepts) into a backbone set and a redundant set? Several methods are available:" + + "

" + + "
nested
A deterministic, agglomerative algorithm that starts with a full backbone set and empty redundant set and moves individual entities whose removal from the backbone would cause the lowest spectral loss step by step from the backbone set to the redundant set. The procedure creates a complete, nested hierarchy of entities. The method is relatively fast and deterministic. It computes the best solution given the constraint that the solution must be completely nested. Note that slightly better non-nested solutions may exist at any level.
" + + "
fixed
Simulated annealing without a penalty parameter and with a fixed number of entities in the backbone set. Results at different backbone size levels are not necessarily nested with this algorithm, which means sometimes backbone solutions with a smaller loss may not be fully contained within a larger backbone set. This combinatorial optimization algorithm returns an approximation, which should likely be the globally best solution but may sometimes deviate and provide a close to optimal solution because the solution space is not exhaustively explored. If you select this option, the simulated annealing algorithm will run once, for the specified backbone size (i.e., number of entities in the backbone set) specified as a parameter by the user. You can manually run this algorithm multiple times for different backbone sizes to compare the results.
" + + "
penalty
Simulated annealing with a penalty parameter and a defined number of iterations. The user does not have to specify how many entities precisely should be in the backbone set and how many should be in the redundant set. Instead, the algorithm may explore larger and smaller solutions in the search space but gravitate towards certain levels as defined by the penalty parameter. The penalty parameter is a real number, and useful values could be something like 3.5, 5.5, 7.5, or 12, for example. Larger penalties tend to produce smaller backbone sets and larger redundant sets. The number of iterations is larger than in the simulated annealing approach with a fixed number of entities and without penalty because there are many more possible solutions in the search space that need to be explored. Hence, due to more iterations required, the algorithm takes longer to compute the result. More iterations tend to approximate the globally optimal solution better. Useful numbers of iterations could be 10,000, 30,000, or 50,000, for example.
" + + "
" + + "

"; + backboneLabel.setToolTipText(backboneMethodToolTip); + settingsPanel.add(backboneLabel, gbc); gbc.gridx = 0; gbc.gridy = 1; @@ -156,22 +154,102 @@ public void actionPerformed(ActionEvent e) { } }); - gbc.gridx = 1; + String[] methods = new String[] {"nested", "fixed", "penalty"}; + backboneMethodBox = new JComboBox<>(methods); + backboneMethodBox.setSelectedIndex(0); + backboneMethodBox.setToolTipText(backboneMethodToolTip); + settingsPanel.add(backboneMethodBox, gbc); + + // backbone size setting + JLabel backboneSizeLabel = new JLabel("Size of backbone set"); + String backboneSizeTooltipText = "

The size of the backbone set to be generated, i.e., the number of entities that will be in the backbone set, rather than the redundant set.

"; + backboneSizeLabel.setToolTipText(backboneSizeTooltipText); + backboneSizeModel = new SpinnerNumberModel(1, 1, 1, 1); + JSpinner backboneSizeSpinner = new JSpinner(backboneSizeModel); + ((JSpinner.DefaultEditor) backboneSizeSpinner.getEditor()).getTextField().setColumns(4); + backboneSizeLabel.setLabelFor(backboneSizeSpinner); + backboneSizeSpinner.setToolTipText(backboneSizeTooltipText); + + // penalty setting + JLabel penaltyLabel = new JLabel("Penalty parameter (p)"); + String penaltyTooltipText = "

The penalty (p) determines how large the " + + "backbone set is, with larger penalties penalizing larger backbones more strongly, hence leading to " + + "smaller backbones and larger redundant sets. Typical values could be 5.5, 7.5, or 12.

"; + penaltyLabel.setToolTipText(penaltyTooltipText); SpinnerNumberModel penaltyModel = new SpinnerNumberModel(3.50, 0.00, 1000.00, 0.10); JSpinner penaltySpinner = new JSpinner(penaltyModel); penaltyLabel.setLabelFor(penaltySpinner); ((JSpinner.DefaultEditor) penaltySpinner.getEditor()).getTextField().setColumns(4); penaltySpinner.setToolTipText(penaltyTooltipText); - settingsPanel.add(penaltySpinner, gbc); - gbc.gridx = 2; + // iterations setting + JLabel iterationsLabel = new JLabel("Iterations (T)"); + String iterationsTooltipText = "

The number of iterations (T) determines how long the " + + "algorithm should run. We have had good results with T = 50,000 iterations for a network of about " + + "200 actors and about 60 concepts, though fewer iterations might have been acceptable. The quality " + + "of the solution increases with larger T and reaches the global optimum asymptotically. How many " + + "iterations are required for a good solution depends on the number of entities on the second-mode " + + "variable.

"; + iterationsLabel.setToolTipText(iterationsTooltipText); SpinnerNumberModel iterationsModel = new SpinnerNumberModel(10000, 0, 1000000, 1000); JSpinner iterationsSpinner = new JSpinner(iterationsModel); ((JSpinner.DefaultEditor) iterationsSpinner.getEditor()).getTextField().setColumns(7); iterationsLabel.setLabelFor(iterationsSpinner); iterationsSpinner.setToolTipText(iterationsTooltipText); - settingsPanel.add(iterationsSpinner, gbc); + + backboneMethodBox.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent itemEvent) { + if (itemEvent.getItem().equals("nested")) { + removeComponentFromGridBagPanel(settingsPanel, 2, 0); + removeComponentFromGridBagPanel(settingsPanel, 3, 0); + removeComponentFromGridBagPanel(settingsPanel, 2, 1); + removeComponentFromGridBagPanel(settingsPanel, 3, 1); + settingsPanel.revalidate(); + settingsPanel.repaint(); + } else if (itemEvent.getItem().equals("fixed")) { + int size = Dna.sql.getUniqueValues(((StatementType) statementTypeBox.getSelectedItem()).getId(), (String) var2Box.getSelectedItem()).size(); + backboneSizeModel.setMaximum(size); + backboneSizeModel.setValue(1); + removeComponentFromGridBagPanel(settingsPanel, 2, 0); + removeComponentFromGridBagPanel(settingsPanel, 3, 0); + removeComponentFromGridBagPanel(settingsPanel, 2, 1); + removeComponentFromGridBagPanel(settingsPanel, 3, 1); + gbc.gridwidth = 1; + gbc.gridy = 0; + gbc.gridx = 2; + settingsPanel.add(backboneSizeLabel, gbc); + gbc.gridy = 1; + settingsPanel.add(backboneSizeSpinner, gbc); + gbc.gridy = 0; + gbc.gridx = 3; + settingsPanel.add(iterationsLabel, gbc); + gbc.gridy = 1; + settingsPanel.add(iterationsSpinner, gbc); + settingsPanel.revalidate(); + settingsPanel.repaint(); + } else if (itemEvent.getItem().equals("penalty")) { + removeComponentFromGridBagPanel(settingsPanel, 2, 0); + removeComponentFromGridBagPanel(settingsPanel, 3, 0); + removeComponentFromGridBagPanel(settingsPanel, 2, 1); + removeComponentFromGridBagPanel(settingsPanel, 3, 1); + gbc.gridwidth = 1; + gbc.gridy = 0; + gbc.gridx = 2; + settingsPanel.add(penaltyLabel, gbc); + gbc.gridy = 1; + settingsPanel.add(penaltySpinner, gbc); + gbc.gridy = 0; + gbc.gridx = 3; + settingsPanel.add(iterationsLabel, gbc); + gbc.gridy = 1; + settingsPanel.add(iterationsSpinner, gbc); + settingsPanel.revalidate(); + settingsPanel.repaint(); + } + } + }); // second row of options gbc.insets = new Insets(10, 3, 3, 3); @@ -774,7 +852,7 @@ public void actionPerformed(ActionEvent e) { "The redundant set is the complementary subset of entities/values of Variable 2 that do not " + "contribute much additional value in structuring the one-mode network into clusters. A custom " + "simulated annealing algorithm is employed to find the backbone and redundant sets by " + - "minimizing penalized Euclidean spectral distances between the backbone network and the full network. " + + "minimizing (penalized) Euclidean spectral distances between the backbone network and the full network. " + "The results can inform how to recode entities or which entities to include or exclude during network " + "export. By pressing this button, the calculation will start, but the results will not be saved to " + "a file yet.

"; @@ -782,8 +860,10 @@ public void actionPerformed(ActionEvent e) { buttonPanel.add(backboneButton); backboneButton.addActionListener(al -> { // read settings from GUI elements and translate into values for Exporter class + String method = (String) backboneMethodBox.getSelectedItem(); double penalty = (double) penaltySpinner.getValue(); int iterations = (int) iterationsSpinner.getValue(); + int backboneSize = (int) backboneSizeSpinner.getValue(); StatementType statementType = (StatementType) statementTypeBox.getSelectedItem(); String variable1Name = (String) var1Box.getSelectedItem(); boolean variable1Document = var1Box.getSelectedIndex() > var1Box.getItemCount() - 7; @@ -812,10 +892,10 @@ public void actionPerformed(ActionEvent e) { LocalDateTime stopDateTime = stopPicker.getDateTimeStrict(); // start backbone thread - Thread backboneThread = new Thread(new GuiBackboneThread(penalty, iterations, statementType, variable1Name, - variable1Document, variable2Name, variable2Document, qualifier, qualifierDocument, - qualifierAggregation, normalization, duplicates, startDateTime, stopDateTime, - BackboneExporter.this.excludeValues, BackboneExporter.this.excludeAuthor, + Thread backboneThread = new Thread(new GuiBackboneThread(method, backboneSize, penalty, iterations, + statementType, variable1Name, variable1Document, variable2Name, variable2Document, qualifier, + qualifierDocument, qualifierAggregation, normalization, duplicates, startDateTime, + stopDateTime, BackboneExporter.this.excludeValues, BackboneExporter.this.excludeAuthor, BackboneExporter.this.excludeSource, BackboneExporter.this.excludeSection, BackboneExporter.this.excludeType, false, false, false, false, false), "Find backbone"); @@ -912,7 +992,28 @@ public void actionPerformed(ActionEvent e) { this.setLocationRelativeTo(null); this.setVisible(true); } - + + /** + * Remove a component from a panel with a GridBagLayout by locating the component through its coordinates. The panel is not revalidated and repainted; this needs to be done after calling this function. + * + * @param panel The panel from which to remove the component. + * @param targetX The horizontal coordinate (gridx). + * @param targetY The vertical coordinate (gridy). + */ + private void removeComponentFromGridBagPanel(JPanel panel, int targetX, int targetY) { + Component componentToRemove = null; + for (Component comp : panel.getComponents()) { + GridBagConstraints gc = g.getConstraints(comp); + if (gc.gridx == targetX && gc.gridy == targetY) { + componentToRemove = comp; + break; + } + } + if (componentToRemove != null) { + panel.remove(componentToRemove); + } + } + /** * Sets a new {@link DefaultListModel} in the excludeVariableList and adds variables conditional on the statement type selected */ @@ -947,11 +1048,11 @@ public void toggleHelp() { } /** - * GUI backbone thread. This is where the computations are executed and the - * data are written to a file. + * GUI backbone thread. This is where the computations are executed and the data are written to a file. */ private class GuiBackboneThread implements Runnable { - + private String method; + private int backboneSize; private double p; private int T; private String variable1, variable2, qualifier, qualifierAggregation, normalization, duplicates; @@ -963,6 +1064,8 @@ private class GuiBackboneThread implements Runnable { private ProgressMonitor progressMonitor; public GuiBackboneThread( + String method, + int backboneSize, double p, int T, StatementType statementType, @@ -987,6 +1090,8 @@ public GuiBackboneThread( boolean invertSources, boolean invertSections, boolean invertTypes) { + this.method = method; + this.backboneSize = backboneSize; this.p = p; this.T = T; this.statementType = statementType; @@ -1059,9 +1164,9 @@ public void run() { this.invertTypes, null, null); + if (progressMonitor.isCanceled()) { proceed = false; - progressMonitor.setProgress(2); progressMonitor.close(); } @@ -1069,7 +1174,7 @@ public void run() { if (proceed) { exporter.loadData(); exporter.filterStatements(); - if (exporter.getFilteredStatements().size() == 0) { + if (exporter.getFilteredStatements().isEmpty()) { proceed = false; LogEvent le = new LogEvent(Logger.ERROR, "No statements left after filtering.", @@ -1077,37 +1182,84 @@ public void run() { Dna.logger.log(le); } } + + // calculate maximum iterations for nested algorithm; necessary because the initial iterations take longer due to more loss comparisons, so it's best to count the number of loss comparisons rather than number of entities as iterations + if (this.method.equals("nested")) { + int iterations = 0; + for (int i = 0; i < exporter.getFullSize(); i++) { + iterations = iterations + (exporter.getFullSize() - i); + } + progressMonitor.setMaximum(iterations); + this.T = iterations; + } + + // initialise algorithm if (proceed) { - progressMonitor.setNote("Computing initial networks..."); - exporter.backbone(p, T); + progressMonitor.setNote("Initializing algorithm..."); + if (this.method.equals("nested")) { + exporter.initializeNestedBackbone(); + } else if (this.method.equals("fixed")) { + exporter.initializeSimulatedAnnealingBackbone(false, p, T, backboneSize); // p is inconsequential because penalty = false + } else if (this.method.equals("penalty")) { + exporter.initializeSimulatedAnnealingBackbone(true, p, T, backboneSize); // backboneSize is inconsequential because penalty = true + } } if (!proceed || progressMonitor.isCanceled()) { proceed = false; } - // step 3: simulated annealing + // step 3: backbone iterations if (proceed) { - progressMonitor.setNote("Simulated annealing..."); - try (ProgressBar pb = new ProgressBar("Simulated annealing...", T)) { - while (exporter.getCurrentT() <= T && !progressMonitor.isCanceled()) { // run up to upper bound of iterations T, provided by the user + if (this.method.equals("nested")) { + progressMonitor.setNote("Nested backbone iterations..."); + try (ProgressBar pb = new ProgressBar("Nested backbone...", this.T)) { + while (exporter.getBackboneSize() > 0 && !progressMonitor.isCanceled()) { // run up to the point where all concepts have been moved from the backbone set to the redundant set + if (progressMonitor.isCanceled()) { + pb.stepTo(exporter.getFullSize()); + break; + } else { + int iterations = 1; + for (int i = 0; i < exporter.getFullSize() - exporter.getBackboneSize(); i++) { + iterations = iterations + (exporter.getFullSize() - i); + } + pb.stepTo(iterations); // go from empty backbone set to full backbone set + progressMonitor.setProgress(iterations); + exporter.iterateNestedBackbone(); + } + } + if (!progressMonitor.isCanceled()) { + exporter.saveNestedBackboneResult(); + BackboneExporter.this.exporter = exporter; + progressMonitor.setProgress(this.T); + } + } finally { if (progressMonitor.isCanceled()) { - exporter.setCurrentT(T); - pb.stepTo(T); - break; - } else { - pb.stepTo(exporter.getCurrentT()); - progressMonitor.setProgress(exporter.getCurrentT()); - exporter.iterateBackbone(); + System.err.println("Canceled."); } } - if (!progressMonitor.isCanceled()) { - exporter.saveBackboneResult(); - BackboneExporter.this.exporter = exporter; - progressMonitor.setProgress(T); - } - } finally { - if (progressMonitor.isCanceled()) { - System.err.println("Canceled."); + } else if (this.method.equals("fixed") || this.method.equals("penalty")) { + progressMonitor.setNote("Simulated annealing..."); + try (ProgressBar pb = new ProgressBar("Simulated annealing...", this.T)) { + while (exporter.getCurrentT() <= T && !progressMonitor.isCanceled()) { // run up to upper bound of iterations T, provided by the user + if (progressMonitor.isCanceled()) { + exporter.setCurrentT(T); + pb.stepTo(T); + break; + } else { + pb.stepTo(exporter.getCurrentT()); + progressMonitor.setProgress(exporter.getCurrentT()); + exporter.iterateSimulatedAnnealingBackbone(this.method.equals("penalty")); + } + } + if (!progressMonitor.isCanceled()) { + exporter.saveSimulatedAnnealingBackboneResult(this.method.equals("penalty")); + BackboneExporter.this.exporter = exporter; + progressMonitor.setProgress(T); + } + } finally { + if (progressMonitor.isCanceled()) { + System.err.println("Canceled."); + } } } } diff --git a/dna/src/main/java/gui/CoderBadgePanel.java b/dna/src/main/java/gui/CoderBadgePanel.java index b8b19cd0..7cdaee7e 100644 --- a/dna/src/main/java/gui/CoderBadgePanel.java +++ b/dna/src/main/java/gui/CoderBadgePanel.java @@ -50,7 +50,7 @@ public CoderBadgePanel(Coder coder, int buttonSize, int border, int maxNameLengt */ public CoderBadgePanel() { if (Dna.sql.getConnectionProfile() == null || Dna.sql.getActiveCoder() == null) { - this.coder = new Coder(-1, "(no coder)", Color.BLACK); + this.coder = new Coder(-1, "(no coder)", new model.Color(0, 0, 0)); } else { this.coder = Dna.sql.getActiveCoder(); } @@ -61,8 +61,7 @@ public CoderBadgePanel() { /** * Creates the layout of the coder badge panel. For internal use in the * class. - * - * @param coder The coder. + * * @param buttonSize Height/width of the color button. * @param border Border margin. Can be 0. * @param maxNameLength Maximal character length of the name. @@ -73,7 +72,7 @@ private void createLayout(int buttonSize, int border, int maxNameLength) { private static final long serialVersionUID = -7254611710375602710L; public void paintComponent(Graphics g) { super.paintComponent(g); - g.setColor(CoderBadgePanel.this.coder.getColor()); + g.setColor(CoderBadgePanel.this.coder.getColor().toAWTColor()); g.fillRect(0, 0, buttonSize, buttonSize); } }); diff --git a/dna/src/main/java/gui/CoderManager.java b/dna/src/main/java/gui/CoderManager.java index b577b710..638cf541 100644 --- a/dna/src/main/java/gui/CoderManager.java +++ b/dna/src/main/java/gui/CoderManager.java @@ -142,10 +142,11 @@ public void removeUpdate(DocumentEvent e) { colorLabel.setEnabled(false); colorButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { - Color newColor = JColorChooser.showDialog(CoderManager.this, "Choose color...", colorButton.getColor()); + Color newColor = JColorChooser.showDialog(CoderManager.this, "Choose color...", colorButton.getColor().toAWTColor()); if (newColor != null) { - colorButton.setColor(newColor); - selectedCoderCopy.setColor(newColor); + model.Color modelColor = new model.Color(newColor.getRed(), newColor.getGreen(), newColor.getBlue()); + colorButton.setColor(modelColor); + selectedCoderCopy.setColor(modelColor); checkButtons(); } } @@ -469,7 +470,7 @@ public void actionPerformed(ActionEvent e) { AddCoderDialog addCoderDialog = new AddCoderDialog(frame); String coderName = addCoderDialog.getCoderName(); String coderPasswordHash = addCoderDialog.getCoderPasswordHash(); - Color coderColor = addCoderDialog.getCoderColor(); + model.Color coderColor = addCoderDialog.getCoderColor(); addCoderDialog.dispose(); if (coderName != null && coderColor != null && coderPasswordHash != null) { int coderId = Dna.sql.addCoder(coderName, coderColor, coderPasswordHash); @@ -768,7 +769,7 @@ private void loadCoder() { pw2Field.setEnabled(false); nameField.setText(""); - colorButton.setColor(Color.BLACK); + colorButton.setColor(new model.Color(0, 0, 0)); // coder relations coderRelationsPanel.getModel().clear(); @@ -850,7 +851,7 @@ private class AddCoderDialog extends JDialog { private JTextField addNameField; private JPasswordField addPw1Field, addPw2Field; private String name; - private Color color; + private model.Color color; private String passwordHash; /** @@ -883,15 +884,15 @@ public void removeUpdate(DocumentEvent arg0) { }); ColorButton addColorButton = new ColorButton(); - addColorButton.setColor(new Color(69, 212, 255)); + addColorButton.setColor(new model.Color(69, 212, 255)); addColorButton.setBorder(BorderFactory.createLineBorder(Color.BLACK, 1)); JLabel addColorLabel = new JLabel("Color", JLabel.TRAILING); addColorLabel.setLabelFor(addColorButton); addColorButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { - Color newColor = JColorChooser.showDialog(AddCoderDialog.this, "Choose color...", colorButton.getColor()); + Color newColor = JColorChooser.showDialog(AddCoderDialog.this, "Choose color...", colorButton.getColor().toAWTColor()); if (newColor != null) { - addColorButton.setColor(newColor); + addColorButton.setColor(new model.Color(newColor.getRed(), newColor.getGreen(), newColor.getBlue())); } } }); @@ -1032,7 +1033,7 @@ String getCoderPasswordHash() { * * @return The selected color. */ - Color getCoderColor() { + model.Color getCoderColor() { return this.color; } } diff --git a/dna/src/main/java/gui/ColorButton.java b/dna/src/main/java/gui/ColorButton.java index 06cd6d48..dd96045b 100644 --- a/dna/src/main/java/gui/ColorButton.java +++ b/dna/src/main/java/gui/ColorButton.java @@ -1,6 +1,5 @@ package gui; -import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; @@ -11,25 +10,25 @@ */ class ColorButton extends JButton { private static final long serialVersionUID = -8121834065246525986L; - private Color color; + private model.Color color; public ColorButton() { - this.color = Color.BLACK; + this.color = new model.Color(0, 0, 0); this.setPreferredSize(new Dimension(18, 18)); } public void paintComponent(Graphics g) { super.paintComponent(g); - g.setColor(color); + g.setColor(color.toAWTColor()); g.fillRect(0, 0, 18, 18); } - void setColor(Color color) { + void setColor(model.Color color) { this.color = color; this.repaint(); } - Color getColor() { + model.Color getColor() { return this.color; } } \ No newline at end of file diff --git a/dna/src/main/java/gui/DocumentEditor.java b/dna/src/main/java/gui/DocumentEditor.java index 4b14873e..ad4ed967 100644 --- a/dna/src/main/java/gui/DocumentEditor.java +++ b/dna/src/main/java/gui/DocumentEditor.java @@ -304,7 +304,7 @@ public void checkButton() { .limit(2) .count() <= 1); if (!oneCoder) { - eligibleCoders.add(0, new Coder(-1, "(keep multiple coders)", Color.BLACK)); + eligibleCoders.add(0, new Coder(-1, "(keep multiple coders)", new model.Color(0, 0, 0))); } // create and populate combo box with coders diff --git a/dna/src/main/java/gui/Gui.java b/dna/src/main/java/gui/Gui.java new file mode 100644 index 00000000..bfc67e52 --- /dev/null +++ b/dna/src/main/java/gui/Gui.java @@ -0,0 +1,10 @@ +package gui; + +public class Gui { + public gui.MainWindow mainWindow; + + public Gui() { + mainWindow = new gui.MainWindow(); + mainWindow.setVisible(true); + } +} diff --git a/dna/src/main/java/gui/Importer.java b/dna/src/main/java/gui/Importer.java index 014c92ff..4ef24333 100644 --- a/dna/src/main/java/gui/Importer.java +++ b/dna/src/main/java/gui/Importer.java @@ -1,7 +1,6 @@ package gui; import java.awt.BorderLayout; -import java.awt.Color; import java.awt.Dimension; import java.awt.Frame; import java.awt.GridBagConstraints; @@ -806,7 +805,7 @@ protected List doInBackground() { rs.getInt("Frequency"), new Coder(rs.getInt("CoderId"), rs.getString("CoderName"), - new Color(rs.getInt("Red"), rs.getInt("Green"), rs.getInt("Blue"))), + new model.Color(rs.getInt("Red"), rs.getInt("Green"), rs.getInt("Blue"))), rs.getString("Author"), rs.getString("Source"), rs.getString("Section"), @@ -1060,7 +1059,7 @@ public void run() { foreignStatementTypes.add(new StatementType( r1.getInt("ID"), r1.getString("Label"), - new Color(r1.getInt("Red"), + new model.Color(r1.getInt("Red"), r1.getInt("Green"), r1.getInt("Blue")), variables)); @@ -1081,7 +1080,7 @@ public void run() { domesticStatementTypes.add(new StatementType( r1.getInt("ID"), r1.getString("Label"), - new Color(r1.getInt("Red"), + new model.Color(r1.getInt("Red"), r1.getInt("Green"), r1.getInt("Blue")), variables)); diff --git a/dna/src/main/java/gui/MainWindow.java b/dna/src/main/java/gui/MainWindow.java index da85e7cb..89d384c7 100644 --- a/dna/src/main/java/gui/MainWindow.java +++ b/dna/src/main/java/gui/MainWindow.java @@ -503,7 +503,7 @@ private void popupMenu(Component comp, int x, int y) { StatementType statementType = statementTypes.get(i); JMenuItem menuItem = new JMenuItem("Format as " + statementType.getLabel()); menuItem.setOpaque(true); - menuItem.setBackground(statementType.getColor()); + menuItem.setBackground(statementType.getColor().toAWTColor()); popmen.add(menuItem); menuItem.addActionListener(new ActionListener() { @@ -1021,7 +1021,7 @@ protected List doInBackground() { rs.getInt("Frequency"), new Coder(rs.getInt("CoderId"), rs.getString("CoderName"), - new Color(rs.getInt("Red"), rs.getInt("Green"), rs.getInt("Blue"))), + new model.Color(rs.getInt("Red"), rs.getInt("Green"), rs.getInt("Blue"))), rs.getString("Author"), rs.getString("Source"), rs.getString("Section"), @@ -1247,7 +1247,7 @@ protected List doInBackground() { + "ORDER BY 1, 2 ASC;"; int statementTypeId, statementId, variableId; - Color sColor, cColor; + model.Color sColor, cColor; HashMap variableNameMap = new HashMap(); // variable ID to variable name HashMap variableDataTypeMap = new HashMap(); // variable ID to data type HashMap statementMap = new HashMap(); // statement ID to Statement @@ -1263,8 +1263,8 @@ protected List doInBackground() { while (r1.next()) { statementId = r1.getInt("StatementId"); statementTypeId = r1.getInt("StatementTypeId"); - sColor = new Color(r1.getInt("StatementTypeRed"), r1.getInt("StatementTypeGreen"), r1.getInt("StatementTypeBlue")); - cColor = new Color(r1.getInt("CoderRed"), r1.getInt("CoderGreen"), r1.getInt("CoderBlue")); + sColor = new model.Color(r1.getInt("StatementTypeRed"), r1.getInt("StatementTypeGreen"), r1.getInt("StatementTypeBlue")); + cColor = new model.Color(r1.getInt("CoderRed"), r1.getInt("CoderGreen"), r1.getInt("CoderBlue")); Statement statement = new Statement(statementId, r1.getInt("Start"), r1.getInt("Stop"), @@ -1688,7 +1688,7 @@ public void actionPerformed(ActionEvent e) { if (!key.equals("")) { ConnectionProfile cp = null; try { - cp = Dna.readConnectionProfile(filename, key); + cp = new ConnectionProfile(filename, key); } catch (EncryptionOperationNotPossibleException e2) { cp = null; } @@ -1780,7 +1780,7 @@ public void actionPerformed(ActionEvent e) { boolean authenticated = Dna.sql.authenticate(-1, key); if (authenticated) { // write the connection profile to disk, with an encrypted version of the password - Dna.writeConnectionProfile(fc.getFiles()[0].getPath(), new ConnectionProfile(Dna.sql.getConnectionProfile()), key); + ConnectionProfile.writeConnectionProfile(fc.getFiles()[0].getPath(), new ConnectionProfile(Dna.sql.getConnectionProfile()), key); validPasswordInput = true; // quit the while-loop after successful export JOptionPane.showMessageDialog(MainWindow.this, "The profile was saved as:\n" + fc.getFiles()[0].getAbsolutePath(), diff --git a/dna/src/main/java/gui/MenuBar.java b/dna/src/main/java/gui/MenuBar.java index ad855dfc..0938f219 100644 --- a/dna/src/main/java/gui/MenuBar.java +++ b/dna/src/main/java/gui/MenuBar.java @@ -203,8 +203,7 @@ public MenuBar(ActionOpenDatabase actionOpenDatabase, // export menu: open backbone exporter JMenuItem backboneExporterItem = new JMenuItem(actionBackboneExporter); - // exportMenu.add(backboneExporterItem); - // TODO: add back in once finalized + exportMenu.add(backboneExporterItem); // settings menu JMenu settingsMenu = new JMenu("Settings"); diff --git a/dna/src/main/java/gui/Popup.java b/dna/src/main/java/gui/Popup.java index 31ee6e0c..c30cfa69 100644 --- a/dna/src/main/java/gui/Popup.java +++ b/dna/src/main/java/gui/Popup.java @@ -49,7 +49,7 @@ class Popup extends JDialog { private Container c; private Statement statement; private Point los; - private Color color; + private model.Color color; private boolean windowDecoration, editable; private JPanel gridBagPanel; private int textFieldWidth; @@ -63,12 +63,12 @@ class Popup extends JDialog { * Popup dialog window to display the contents of a statements. The user can * edit the values of each variable. * - * @param X Horizontal coordinate for the window. - * @param Y Vertical coordinate for the window. - * @param statement The {@link Statement} to be edited. - * @param location Location of the DNA text panel on screen. - * @param coder The current coder who is viewing the statement. - * @param statementPanel A reference to the statement panel. + * @param X Horizontal coordinate for the window. + * @param Y Vertical coordinate for the window. + * @param statement The {@link Statement} to be edited. + * @param location Location of the DNA text panel on screen. + * @param coder The current coder who is viewing the statement. + * @param eligibleCoders A list of coders with the permission to edit the statement. */ Popup(double X, double Y, Statement statement, Point location, Coder coder, ArrayList eligibleCoders) { this.statement = statement; @@ -137,7 +137,7 @@ class Popup extends JDialog { JSeparator sep = new JSeparator(); JPanel colorPanel = new JPanel(); - colorPanel.setBackground(color); + colorPanel.setBackground(color.toAWTColor()); colorPanel.setPreferredSize(new Dimension(4, 4)); ImageIcon duplicateIcon = new ImageIcon(new ImageIcon(getClass().getResource("/icons/tabler-icon-copy.png")).getImage().getScaledInstance(h, h, Image.SCALE_SMOOTH)); @@ -236,7 +236,7 @@ class Popup extends JDialog { Color fg = javax.swing.UIManager.getColor("TextField.foreground"); // default unselected foreground color of JTextField for (int j = 0; j < box.getModel().getSize(); j++) { if (s.equals(box.getModel().getElementAt(j).getValue())) { - fg = box.getModel().getElementAt(j).getColor(); + fg = box.getModel().getElementAt(j).getColor().toAWTColor(); } } ((JTextField) box.getEditor().getEditorComponent()).setSelectedTextColor(fg); @@ -260,7 +260,7 @@ private void formatEntry() { Color fg = javax.swing.UIManager.getColor("TextField.foreground"); // default unselected foreground color of JTextField for (int i = 0; i < box.getModel().getSize(); i++) { if (((JTextField) box.getEditor().getEditorComponent()).getText().equals(box.getModel().getElementAt(i).getValue())) { - fg = box.getModel().getElementAt(i).getColor(); + fg = box.getModel().getElementAt(i).getColor().toAWTColor(); } } ((JTextField) box.getEditor().getEditorComponent()).setSelectedTextColor(fg); diff --git a/dna/src/main/java/gui/RegexEditor.java b/dna/src/main/java/gui/RegexEditor.java index 0479efca..3920134a 100644 --- a/dna/src/main/java/gui/RegexEditor.java +++ b/dna/src/main/java/gui/RegexEditor.java @@ -86,13 +86,13 @@ public void valueChanged(ListSelectionEvent e) { entryPanel.setBorder(new EmptyBorder(5, 10, 5, 10)); ColorButton colorButton = new ColorButton(); colorButton.setBorder(BorderFactory.createLineBorder(Color.BLACK, 1)); - colorButton.setColor(Color.RED); + colorButton.setColor(new model.Color(255, 0, 0)); colorButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { - Color currentColor = colorButton.getColor(); - Color newColor = JColorChooser.showDialog(RegexEditor.this, "Choose color...", currentColor); + model.Color currentColor = colorButton.getColor(); + Color newColor = JColorChooser.showDialog(RegexEditor.this, "Choose color...", currentColor.toAWTColor()); if (newColor != null) { - colorButton.setColor(newColor); + colorButton.setColor(new model.Color(newColor.getRed(), newColor.getGreen(), newColor.getBlue())); } } }); @@ -158,7 +158,7 @@ public void focusLost(FocusEvent e) { addButton.setEnabled(false); addButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { - Color cl = colorButton.getColor(); + model.Color cl = colorButton.getColor(); int red = cl.getRed(); int green = cl.getGreen(); int blue = cl.getBlue(); @@ -166,7 +166,7 @@ public void actionPerformed(ActionEvent e) { boolean added = Dna.sql.addRegex(text, red, green, blue); if (added) { changed = true; - Regex regex = new Regex(text, new Color(red, green, blue)); + Regex regex = new Regex(text, cl); regexListModel.addElement(regex); textField.setText(""); colorButton.setForeground(Color.RED); @@ -238,7 +238,7 @@ private class RegexListRenderer extends DefaultListCellRenderer { public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); label.setText((String)((Regex)value).getLabel()); - label.setForeground((Color)((Regex)value).getColor()); + label.setForeground(((Regex)value).getColor().toAWTColor()); label.setOpaque(true); return label; } diff --git a/dna/src/main/java/gui/StatementPanel.java b/dna/src/main/java/gui/StatementPanel.java index 283dfb18..8dcc5c5c 100644 --- a/dna/src/main/java/gui/StatementPanel.java +++ b/dna/src/main/java/gui/StatementPanel.java @@ -351,9 +351,9 @@ public Component getTableCellRendererComponent(JTable table, Object value, boole cbp.setBackground(javax.swing.UIManager.getColor("Table.selectionBackground")); } else { if (Dna.sql.getActiveCoder().isColorByCoder()) { - cbp.setBackground(s.getCoderColor()); + cbp.setBackground(s.getCoderColor().toAWTColor()); } else { - cbp.setBackground(s.getStatementTypeColor()); + cbp.setBackground(s.getStatementTypeColor().toAWTColor()); } } return cbp; @@ -362,9 +362,9 @@ public Component getTableCellRendererComponent(JTable table, Object value, boole c.setBackground(javax.swing.UIManager.getColor("Table.selectionBackground")); } else { if (Dna.sql.getActiveCoder().isColorByCoder()) { - c.setBackground(s.getCoderColor()); + c.setBackground(s.getCoderColor().toAWTColor()); } else { - c.setBackground(s.getStatementTypeColor()); + c.setBackground(s.getStatementTypeColor().toAWTColor()); } } return c; @@ -640,7 +640,7 @@ public Component getListCellRendererComponent(JList list, Object value, int i private static final long serialVersionUID = 435490918616472975L; public void paintComponent(Graphics g) { super.paintComponent(g); - g.setColor(s.getColor()); + g.setColor(s.getColor().toAWTColor()); g.fillRect(2, 2, 14, 14); } }); diff --git a/dna/src/main/java/gui/StatementRecoder.java b/dna/src/main/java/gui/StatementRecoder.java index a1251946..ec0c71f2 100644 --- a/dna/src/main/java/gui/StatementRecoder.java +++ b/dna/src/main/java/gui/StatementRecoder.java @@ -381,7 +381,7 @@ public void actionPerformed(ActionEvent e) { Color fg = javax.swing.UIManager.getColor("TextField.foreground"); // default unselected foreground color of JTextField for (int j = 0; j < box.getModel().getSize(); j++) { if (s.equals(box.getModel().getElementAt(j).getValue())) { - fg = box.getModel().getElementAt(j).getColor(); + fg = box.getModel().getElementAt(j).getColor().toAWTColor(); } } ((JTextField) box.getEditor().getEditorComponent()).setSelectedTextColor(fg); @@ -419,7 +419,7 @@ private void formatEntry() { Color fg = javax.swing.UIManager.getColor("TextField.foreground"); // default unselected foreground color of JTextField for (int j = 0; j < box.getModel().getSize(); j++) { if (((JTextField) box.getEditor().getEditorComponent()).getText().equals(box.getModel().getElementAt(j).getValue())) { - fg = box.getModel().getElementAt(j).getColor(); + fg = box.getModel().getElementAt(j).getColor().toAWTColor(); } } ((JTextField) box.getEditor().getEditorComponent()).setSelectedTextColor(fg); @@ -1031,7 +1031,7 @@ public void applyAll(int row, int variableIndex) { */ public void applyAllCoder(int row) { int coderId = statements.get(row).getCoderId(); - Color coderColor = statements.get(row).getCoderColor(); + model.Color coderColor = statements.get(row).getCoderColor(); String coderName = statements.get(row).getCoderName(); statements.stream().forEach(s -> { s.setCoderId(coderId); @@ -1049,7 +1049,7 @@ public void applyAllCoder(int row) { * @param coderName The new coder name. * @param coderColor The new coder color. */ - public void updateCoder(int row, int coderId, String coderName, Color coderColor) { + public void updateCoder(int row, int coderId, String coderName, model.Color coderColor) { statements.get(row).setCoderId(coderId); statements.get(row).setCoderName(coderName); statements.get(row).setCoderColor(coderColor); diff --git a/dna/src/main/java/gui/StatementTypeComboBoxRenderer.java b/dna/src/main/java/gui/StatementTypeComboBoxRenderer.java index fcb635e4..fa34085f 100644 --- a/dna/src/main/java/gui/StatementTypeComboBoxRenderer.java +++ b/dna/src/main/java/gui/StatementTypeComboBoxRenderer.java @@ -22,7 +22,7 @@ public Component getListCellRendererComponent(JList list, Object value, int i JButton colorRectangle = (new JButton() { public void paintComponent(Graphics g) { super.paintComponent(g); - g.setColor(statementType.getColor()); + g.setColor(statementType.getColor().toAWTColor()); g.fillRect(2, 2, 14, 14); } }); diff --git a/dna/src/main/java/gui/StatementTypeEditor.java b/dna/src/main/java/gui/StatementTypeEditor.java index 36a98318..816e3e0e 100644 --- a/dna/src/main/java/gui/StatementTypeEditor.java +++ b/dna/src/main/java/gui/StatementTypeEditor.java @@ -110,7 +110,7 @@ public void valueChanged(ListSelectionEvent e) { public void actionPerformed(ActionEvent e) { int dialog = JOptionPane.showConfirmDialog(StatementTypeEditor.this, "Add a new statement type to the database?\nThis action will be written to the database now.", "Confirmation", JOptionPane.YES_NO_OPTION); if (dialog == 0) { - int id = Dna.sql.addStatementType("(New statement type)", new Color(181, 255, 0)); + int id = Dna.sql.addStatementType("(New statement type)", new model.Color(181, 255, 0)); repopulateStatementTypeListFromDatabase(id); updateUI(e.getSource()); } @@ -188,9 +188,9 @@ public void removeUpdate(DocumentEvent e) { colorLabel.setEnabled(false); colorButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { - Color newColor = JColorChooser.showDialog(StatementTypeEditor.this, "Choose color...", colorButton.getColor()); + Color newColor = JColorChooser.showDialog(StatementTypeEditor.this, "Choose color...", colorButton.getColor().toAWTColor()); if (newColor != null) { - colorButton.setColor(newColor); + colorButton.setColor(new model.Color(newColor.getRed(), newColor.getGreen(), newColor.getBlue())); } updateUI(e.getSource()); } @@ -435,7 +435,7 @@ private void updateUI(Object source) { if (!(source instanceof DocumentEvent)) { nameField.setText(""); } - colorButton.setColor(Color.BLACK); + colorButton.setColor(new model.Color(0, 0, 0)); } else { idField.setEnabled(true); nameField.setEnabled(true); diff --git a/dna/src/main/java/gui/TextPanel.java b/dna/src/main/java/gui/TextPanel.java index 6471d85b..1f3ef8d8 100644 --- a/dna/src/main/java/gui/TextPanel.java +++ b/dna/src/main/java/gui/TextPanel.java @@ -146,9 +146,9 @@ void paintStatements() { (statements.get(i).getCoderId() == Dna.sql.getActiveCoder().getId() || Dna.sql.getActiveCoder().isPermissionViewOthersStatements()) && (statements.get(i).getCoderId() == Dna.sql.getActiveCoder().getId() || Dna.sql.getActiveCoder().getCoderRelations().get(statements.get(i).getCoderId()).isViewStatements())) { if (Dna.sql.getActiveCoder().isColorByCoder() == true) { - StyleConstants.setBackground(bgStyle, statements.get(i).getCoderColor()); + StyleConstants.setBackground(bgStyle, statements.get(i).getCoderColor().toAWTColor()); } else { - StyleConstants.setBackground(bgStyle, statements.get(i).getStatementTypeColor()); + StyleConstants.setBackground(bgStyle, statements.get(i).getStatementTypeColor().toAWTColor()); } } doc.setCharacterAttributes(start, statements.get(i).getStop() - start, bgStyle, false); @@ -158,13 +158,13 @@ void paintStatements() { ArrayList regex = Dna.sql.getRegexes(); for (i = 0; i < regex.size(); i++) { String label = regex.get(i).getLabel(); - Color color = regex.get(i).getColor(); + model.Color color = regex.get(i).getColor(); Pattern p = Pattern.compile(label, Pattern.CASE_INSENSITIVE); Matcher m = p.matcher(textWindow.getText()); while(m.find()) { start = m.start(); Style fgStyle = sc.addStyle("ConstantWidth", null); - StyleConstants.setForeground(fgStyle, color); + StyleConstants.setForeground(fgStyle, color.toAWTColor()); doc.setCharacterAttributes(start, m.end() - start, fgStyle, false); } } diff --git a/dna/src/main/java/model/Coder.java b/dna/src/main/java/model/Coder.java index 6eb4c16f..82057bc7 100644 --- a/dna/src/main/java/model/Coder.java +++ b/dna/src/main/java/model/Coder.java @@ -1,6 +1,5 @@ package model; -import java.awt.Color; import java.util.HashMap; /** diff --git a/dna/src/main/java/model/CoderRelation.java b/dna/src/main/java/model/CoderRelation.java index e497ccd7..69656760 100644 --- a/dna/src/main/java/model/CoderRelation.java +++ b/dna/src/main/java/model/CoderRelation.java @@ -1,7 +1,5 @@ package model; -import java.awt.Color; - /** * Represents relations with a target coder. Any source coder can hold multiple * permissions for all other (target) coders in a hash map of coder relations. diff --git a/dna/src/main/java/model/Color.java b/dna/src/main/java/model/Color.java new file mode 100644 index 00000000..e4f35a45 --- /dev/null +++ b/dna/src/main/java/model/Color.java @@ -0,0 +1,74 @@ +package model; + +import dna.Dna; +import logger.LogEvent; +import logger.Logger; + +/** + * A class representing RGB colors to avoid awt for headless mode. + */ +public class Color { + int red, green, blue; + + public int getRed() { + return red; + } + + public void setRed(int red) { + this.red = red; + } + + public int getGreen() { + return green; + } + + public void setGreen(int green) { + this.green = green; + } + + public int getBlue() { + return blue; + } + + public void setBlue(int blue) { + this.blue = blue; + } + + public Color(int red, int green, int blue) { + this.red = red; + this.green = green; + this.blue = blue; + } + + /** + * Constructor for hex colors. + * + * @param hexColor A hex color string, for example {@code "#FF11AB"} + */ + public Color(String hexColor) { + if (hexColor.charAt(0) == '#') { + hexColor = hexColor.substring(1); + } + + // check if the hex color has a length of 6 + if (hexColor.length() != 6) { + LogEvent l = new LogEvent(Logger.ERROR, + "Invalid hex color length. Expected length of 6.", + "Invalid hex color length. Expected length of 6 when parsing the following hex color string: " + hexColor + "."); + Dna.logger.log(l); + } + + this.red = Integer.parseInt(hexColor.substring(0, 2), 16); + this.green = Integer.parseInt(hexColor.substring(2, 4), 16); + this.blue = Integer.parseInt(hexColor.substring(4, 6), 16); + } + + /** + * Converts this Color instance to an instance of java.awt.Color. + * + * @return a java.awt.Color instance with the same RGB values. + */ + public java.awt.Color toAWTColor() { + return new java.awt.Color(this.red, this.green, this.blue); + } +} diff --git a/dna/src/main/java/model/Entity.java b/dna/src/main/java/model/Entity.java index b79dd0fe..586abc01 100644 --- a/dna/src/main/java/model/Entity.java +++ b/dna/src/main/java/model/Entity.java @@ -1,6 +1,5 @@ package model; -import java.awt.Color; import java.util.HashMap; /** @@ -91,7 +90,7 @@ public Entity(String value) { this.value = value; this.id = -1; this.variableId = -1; - this.color = Color.BLACK; + this.color = new Color(0, 0, 0); this.inDatabase = false; this.attributeValues = null; } diff --git a/dna/src/main/java/model/Regex.java b/dna/src/main/java/model/Regex.java index 92b39872..737536d3 100644 --- a/dna/src/main/java/model/Regex.java +++ b/dna/src/main/java/model/Regex.java @@ -1,7 +1,5 @@ package model; -import java.awt.Color; - /** * A class for regular expression patterns to be highlighted in the document * text in a specified color. diff --git a/dna/src/main/java/model/Statement.java b/dna/src/main/java/model/Statement.java index 66762977..5752468b 100644 --- a/dna/src/main/java/model/Statement.java +++ b/dna/src/main/java/model/Statement.java @@ -1,7 +1,5 @@ package model; - -import java.awt.Color; import java.time.LocalDateTime; import java.util.ArrayList; diff --git a/dna/src/main/java/model/StatementType.java b/dna/src/main/java/model/StatementType.java index 90f3e71b..dacb6b31 100644 --- a/dna/src/main/java/model/StatementType.java +++ b/dna/src/main/java/model/StatementType.java @@ -1,7 +1,5 @@ package model; -import javax.swing.*; -import java.awt.Color; import java.util.ArrayList; import java.util.stream.Stream; diff --git a/dna/src/main/java/model/Value.java b/dna/src/main/java/model/Value.java index 8cb2b46d..902d47db 100644 --- a/dna/src/main/java/model/Value.java +++ b/dna/src/main/java/model/Value.java @@ -1,6 +1,5 @@ package model; - /** * Represents values saved in a statement, corresponding to the variables saved * in the statement type that corresponds to the statement. diff --git a/dna/src/main/java/sql/ConnectionProfile.java b/dna/src/main/java/sql/ConnectionProfile.java index 0566f64f..89e50dbe 100644 --- a/dna/src/main/java/sql/ConnectionProfile.java +++ b/dna/src/main/java/sql/ConnectionProfile.java @@ -1,5 +1,16 @@ package sql; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonIOException; +import com.google.gson.JsonSyntaxException; +import logger.LogEvent; +import logger.Logger; +import org.jasypt.exceptions.EncryptionOperationNotPossibleException; +import org.jasypt.util.text.AES256TextEncryptor; + +import java.io.*; + public class ConnectionProfile { /** * The connection type. Valid values are {@code "sqlite"}, {@code "mysql"}, @@ -58,6 +69,17 @@ public ConnectionProfile(int coderId, String type, String url, String databaseNa this.port = port; } + public ConnectionProfile(String file, String key) { + ConnectionProfile p = readConnectionProfile(file, key); + this.type = p.getType(); + this.url = p.getUrl(); + this.user = p.getUser(); + this.password = p.getPassword(); + this.databaseName = p.getDatabaseName(); + this.port = p.getPort(); + this.coderId = p.getCoderId(); + } + /** * Copy constructor. Creates a deep copy of a connection profile. * @@ -130,4 +152,70 @@ public int getPort() { public String getDatabaseName() { return this.databaseName; } + + /** + * Read in a saved connection profile from a JSON file, decrypt the + * credentials, and return the connection profile. + * + * @param file The file name including path of the JSON connection profile + * @param key The key/password of the coder to decrypt the credentials + * @return Decrypted connection profile + */ + public static ConnectionProfile readConnectionProfile(String file, String key) throws EncryptionOperationNotPossibleException { + // read connection profile JSON file in, in String format but with encrypted credentials + ConnectionProfile cp = null; + Gson gson = new Gson(); + try (BufferedReader br = new BufferedReader(new FileReader(file))) { + cp = gson.fromJson(br, ConnectionProfile.class); + } catch (JsonSyntaxException | JsonIOException | IOException e) { + LogEvent l = new LogEvent(Logger.ERROR, + "Failed to read connection profile.", + "Tried to read a connection profile from a JSON file and failed. File: " + file + ".", + e); + dna.Dna.logger.log(l); + } + + // decrypt the URL, user name, and SQL connection password inside the profile + AES256TextEncryptor textEncryptor = new AES256TextEncryptor(); + textEncryptor.setPassword(key); + cp.setUrl(textEncryptor.decrypt(cp.getUrl())); + cp.setUser(textEncryptor.decrypt(cp.getUser())); + cp.setPassword(textEncryptor.decrypt(cp.getPassword())); + + return cp; + } + + /** + * Take a decrypted connection profile, encrypt the credentials, and write + * it to a JSON file on disk. + * + * @param file The file name including full path as a String + * @param cp The connection profile to be encrypted and saved + * @param key The key/password of the coder to encrypt the credentials + */ + public static void writeConnectionProfile(String file, ConnectionProfile cp, String key) { + // encrypt URL, user, and password using Jasypt + AES256TextEncryptor textEncryptor = new AES256TextEncryptor(); + textEncryptor.setPassword(key); + cp.setUrl(textEncryptor.encrypt(cp.getUrl())); + cp.setUser(textEncryptor.encrypt(cp.getUser())); + cp.setPassword(textEncryptor.encrypt(cp.getPassword())); + + // serialize Connection object to JSON file and save to disk + try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) { + Gson prettyGson = new GsonBuilder() + .setPrettyPrinting() + .serializeNulls() + .disableHtmlEscaping() + .create(); + String g = prettyGson.toJson(cp); + writer.write(g); + } catch (IOException e) { + LogEvent l = new LogEvent(Logger.ERROR, + "Failed to write connection profile.", + "Tried to write a connection profile to a JSON file and failed. File: " + file + ".", + e); + dna.Dna.logger.log(l); + } + } } \ No newline at end of file diff --git a/dna/src/main/java/sql/DataExchange.java b/dna/src/main/java/sql/DataExchange.java index 1e9ed86f..babe3f61 100644 --- a/dna/src/main/java/sql/DataExchange.java +++ b/dna/src/main/java/sql/DataExchange.java @@ -5,9 +5,9 @@ import logger.LogEvent; import logger.Logger; import me.tongfei.progressbar.ProgressBar; +import model.Color; import model.Entity; -import java.awt.*; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -315,7 +315,7 @@ static public void setAttributes(int variableId, DataFrame df, boolean simulate) for (int k = 4; k Description: Qualitative content analysis and discourse network analysis @@ -22,12 +26,19 @@ Depends: R (>= 4.0.0) Imports: rJava (>= 0.9-12), - ggplot2 (>= 3.3.6) + ggplot2 (>= 3.3.6), + rlang (>= 1.1.1) Suggests: testthat, askpass (>= 1.1), - igraph (>= 1.3.5), - ggraph (>= 2.1.0) + igraph (>= 0.8.1), + ggraph (>= 2.1.0), + cluster (>= 1.12.0), + sna (>= 2.4), + pbmcapply (>= 1.5.1), + MASS (>= 7.3-51.5), + factoextra (>= 1.0.7), + heatmaply (>= 1.4.2) SystemRequirements: Java (>= 11) License: GPL-2 LazyData: true diff --git a/rDNA/rDNA/NAMESPACE b/rDNA/rDNA/NAMESPACE index 612506ee..750c465f 100644 --- a/rDNA/rDNA/NAMESPACE +++ b/rDNA/rDNA/NAMESPACE @@ -2,44 +2,65 @@ S3method(as.matrix,dna_network_onemode) S3method(as.matrix,dna_network_twomode) +S3method(autoplot,dna_backbone) S3method(autoplot,dna_barplot) S3method(autoplot,dna_network_onemode) S3method(autoplot,dna_network_twomode) +S3method(autoplot,dna_phaseTransitions) +S3method(plot,dna_backbone) +S3method(print,dna_backbone) S3method(print,dna_barplot) +S3method(print,dna_multiclust) S3method(print,dna_network_onemode) S3method(print,dna_network_twomode) +S3method(print,dna_phaseTransitions) +export(dna_api) +export(dna_backbone) export(dna_barplot) export(dna_closeDatabase) +export(dna_evaluateBackboneSolution) export(dna_getAttributes) +export(dna_getVariables) export(dna_init) export(dna_jar) +export(dna_multiclust) export(dna_network) export(dna_openConnectionProfile) export(dna_openDatabase) +export(dna_phaseTransitions) export(dna_printDetails) export(dna_queryCoders) export(dna_sample) export(dna_saveConnectionProfile) importFrom(ggplot2,.pt) importFrom(ggplot2,aes) -importFrom(ggplot2,aes_string) importFrom(ggplot2,annotate) +importFrom(ggplot2,arrow) importFrom(ggplot2,autoplot) importFrom(ggplot2,coord_flip) importFrom(ggplot2,element_blank) importFrom(ggplot2,element_text) importFrom(ggplot2,geom_bar) importFrom(ggplot2,geom_line) +importFrom(ggplot2,geom_point) importFrom(ggplot2,geom_text) importFrom(ggplot2,ggplot) importFrom(ggplot2,ggtitle) +importFrom(ggplot2,guides) +importFrom(ggplot2,labs) importFrom(ggplot2,position_stack) importFrom(ggplot2,scale_color_identity) +importFrom(ggplot2,scale_colour_manual) importFrom(ggplot2,scale_fill_identity) +importFrom(ggplot2,scale_shape_manual) +importFrom(ggplot2,scale_x_continuous) +importFrom(ggplot2,scale_x_datetime) importFrom(ggplot2,scale_x_discrete) +importFrom(ggplot2,scale_y_continuous) importFrom(ggplot2,theme) importFrom(ggplot2,theme_bw) importFrom(ggplot2,theme_minimal) +importFrom(ggplot2,unit) importFrom(ggplot2,xlab) importFrom(ggplot2,ylab) importFrom(grDevices,col2rgb) @@ -52,8 +73,17 @@ importFrom(rJava,.jnew) importFrom(rJava,.jnull) importFrom(rJava,J) importFrom(rJava,is.jnull) +importFrom(rlang,.data) +importFrom(stats,as.dist) +importFrom(stats,cor) +importFrom(stats,cutree) +importFrom(stats,dist) importFrom(stats,filter) +importFrom(stats,hclust) +importFrom(stats,kmeans) +importFrom(utils,combn) importFrom(utils,download.file) +importFrom(utils,head) importFrom(utils,packageDescription) importFrom(utils,packageVersion) importFrom(utils,stack) diff --git a/rDNA/rDNA/R/rDNA.R b/rDNA/rDNA/R/rDNA.R index 1cbb24ee..c29a6ba3 100644 --- a/rDNA/rDNA/R/rDNA.R +++ b/rDNA/rDNA/R/rDNA.R @@ -12,7 +12,8 @@ dnaEnvironment <- new.env(hash = TRUE, parent = emptyenv()) 'Date: ', desc$Date, '\n', 'Author: Philip Leifeld (University of Essex)\n', 'Contributors: Tim Henrichsen (University of Warwick),\n', - ' Johannes B. Gruber (Vrije Universiteit Amsterdam)\n', + ' Johannes B. Gruber (University of Amsterdam)\n', + ' Kristijan Garic (University of Essex)\n', 'Project home: github.com/leifeld/dna' ) } @@ -539,6 +540,47 @@ dna_printDetails <- function() { .jcall(dnaEnvironment[["dna"]]$headlessDna, "V", "printDatabaseDetails") } +#' Get a reference to the headless Java class for R (API) +#' +#' Get a reference to the headless Java class for R (API). +#' +#' This function returns a Java object reference to the instance of the +#' \code{Dna/HeadlessDna} class in the DNA JAR file that is held in the rDNA +#' package environment and used by the functions in the package to exchange data +#' with the Java application. You can use the \pkg{rJava} package to access the +#' available functions in this class directly. API access requires detailed +#' knowledge of the DNA JAR classes and functions and is recommended for +#' developers and advanced users only. +#' +#' @return A Java object reference to the \code{Dna/HeadlessDna} class. +#' +#' @author Philip Leifeld +#' +#' @examples +#' \dontrun{ +#' library("rJava") # load rJava package to use functions in the Java API +#' dna_init() +#' dna_sample() +#' dna_openDatabase(coderId = 1, +#' coderPassword = "sample", +#' db_url = "sample.dna") +#' api <- dna_api() +#' +#' # use the \code{getVariables} function to retrieve variables +#' variable_references <- api$getVariables("DNA Statement") +#' +#' # iterate through variable references and print their data type +#' for (i in seq(variable_references$size()) - 1) { +#' print(variable_references$get(as.integer(i))$getDataType()) +#' } +#' } +#' +#' @family {rDNA database connections} +#' +#' @export +dna_api <- function() { + return(dnaEnvironment[["dna"]]$headlessDna) +} # Coder management-------------------------------------------------------------- @@ -611,6 +653,59 @@ dna_queryCoders <- function(db_url, } +# Variables -------------------------------------------------------------------- + +#' Retrieve a dataframe with all variables for a statement type +#' +#' Retrieve a dataframe with all variables defined in a given statement type. +#' +#' For a given statement type ID or label, this function creates a data frame +#' with one row per variable and contains columns for the variable ID, name and +#' data type. +#' +#' @param statementType The statement type for which statements should be +#' retrieved. The statement type can be supplied as an integer or character +#' string, for example \code{1} or \code{"DNA Statement"}. +#' +#' @examples +#' \dontrun{ +#' dna_init() +#' samp <- dna_sample() +#' dna_openDatabase(samp, coderId = 1, coderPassword = "sample") +#' variables <- dna_getVariables("DNA Statement") +#' variables +#' } +#' +#' @author Philip Leifeld +#' +#' @importFrom rJava J .jcall +#' @export +dna_getVariables <- function(statementType) { + if (is.null(statementType) || is.na(statementType) || length(statementType) != 1) { + stop("'statementType' must be an integer or character object of length 1.") + } + if (is.numeric(statementType) && !is.integer(statementType)) { + statementType <- as.integer(statementType) + } else if (!is.character(statementType) && !is.integer(statementType)) { + stop("'statementType' must be an integer or character object of length 1.") + } + + v <- J(dnaEnvironment[["dna"]]$headlessDna, "getVariables", statementType) # get an array list of Value objects representing the variables + l <- list() + for (i in seq(.jcall(v, "I", "size")) - 1) { # iterate through array list of Value objects + vi <- v$get(as.integer(i)) # save current Value as vi + row <- list() # create a list for the different slots + row$id <- .jcall(vi, "I", "getVariableId") + row$label <- .jcall(vi, "S", "getKey") + row$type <- .jcall(vi, "S", "getDataType") + l[[i + 1]] <- row # add the row to the list + } + d <- do.call(rbind.data.frame, l) # convert the list of lists to data frame + attributes(d)$statementType <- statementType + return(d) +} + + # Attributes ------------------------------------------------------------------- #' Get the entities and attributes for a variable @@ -1507,6 +1602,7 @@ print.dna_network_twomode <- print.dna_network_onemode #' @importFrom ggplot2 autoplot #' @importFrom ggplot2 aes #' @importFrom ggplot2 scale_color_identity +#' @importFrom rlang .data #' @name autoplot.dna_network NULL @@ -1745,8 +1841,7 @@ autoplot.dna_network_twomode <- autoplot.dna_network_onemode #' #' @author Philip Leifeld #' -#' @family {rDNA barplots} -#' +#' @rdname dna_barplot #' @importFrom rJava .jarray #' @importFrom rJava .jcall #' @importFrom rJava .jevalArray @@ -1880,8 +1975,7 @@ dna_barplot <- function(statementType = "DNA Statement", #' #' @author Philip Leifeld #' -#' @family {rDNA barplots} -#' +#' @rdname dna_barplot #' @export print.dna_barplot <- function(x, trim = 30, attr = TRUE, ...) { x2 <- x @@ -1968,11 +2062,10 @@ print.dna_barplot <- function(x, trim = 30, attr = TRUE, ...) { #' #' @author Johannes B. Gruber, Tim Henrichsen #' -#' @family {rDNA barplots} -#' +#' @rdname dna_barplot #' @importFrom ggplot2 autoplot #' @importFrom ggplot2 ggplot -#' @importFrom ggplot2 aes_string +#' @importFrom ggplot2 aes #' @importFrom ggplot2 geom_line #' @importFrom ggplot2 theme_minimal #' @importFrom ggplot2 theme @@ -1989,6 +2082,7 @@ print.dna_barplot <- function(x, trim = 30, attr = TRUE, ...) { #' @importFrom ggplot2 scale_x_discrete #' @importFrom utils stack #' @importFrom grDevices col2rgb +#' @importFrom rlang .data #' @export autoplot.dna_barplot <- function(object, ..., @@ -2127,44 +2221,44 @@ autoplot.dna_barplot <- function(object, # Plot g <- ggplot2::ggplot(object2, - ggplot2::aes_string(x = "entity", - y = "frequency", - fill = "agreement", - group = "agreement", - label = "label")) + ggplot2::aes(x = .data[["entity"]], + y = .data[["frequency"]], + fill = .data[["agreement"]], + group = .data[["agreement"]], + label = .data[["label"]])) if (binary) { # Bars for the binary case - g <- g + ggplot2::geom_bar(ggplot2::aes_string(fill = "color", - color = "text_color"), - stat = "identity", + g <- g + ggplot2::geom_bar(ggplot2::aes(fill = .data[["color"]], + color = .data[["text_color"]]), + stat = .data[["identity"]], width = barWidth, show.legend = FALSE) # For the integer case with positive and negative values } else if (max(w) > 0 & min(w) < 0) { - g <- g + ggplot2::geom_bar(ggplot2::aes_string(fill = "color", - color = "text_color"), + g <- g + ggplot2::geom_bar(ggplot2::aes(fill = .data[["color"]], + color = .data[["text_color"]]), stat = "identity", width = barWidth, show.legend = FALSE, data = object2[as.numeric(as.character(object2$agreement)) >= 0, ], position = ggplot2::position_stack(reverse = TRUE)) + - ggplot2::geom_bar(ggplot2::aes_string(fill = "color", - color = "text_color"), + ggplot2::geom_bar(ggplot2::aes(fill = .data[["color"]], + color = .data[["text_color"]]), stat = "identity", width = barWidth, show.legend = FALSE, data = object2[as.numeric(as.character(object2$agreement)) < 0, ]) # For the integer case with positive values only } else if (min(w) >= 0) { - g <- g + ggplot2::geom_bar(ggplot2::aes_string(fill = "color", - color = "text_color"), + g <- g + ggplot2::geom_bar(ggplot2::aes(fill = .data[["color"]], + color = .data[["text_color"]]), stat = "identity", width = barWidth, show.legend = FALSE, position = ggplot2::position_stack(reverse = TRUE)) # For the integer case with negative values only } else { - g <- g + ggplot2::geom_bar(ggplot2::aes_string(fill = "color", - color = "text_color"), + g <- g + ggplot2::geom_bar(ggplot2::aes(fill = .data[["color"]], + color = .data[["text_color"]]), stat = "identity", width = barWidth, show.legend = FALSE) @@ -2172,7 +2266,7 @@ autoplot.dna_barplot <- function(object, g <- g + ggplot2::coord_flip() + ggplot2::theme_minimal() + # Add intercept line - ggplot2::geom_line(ggplot2::aes_string(x = "x", y = "y"), + ggplot2::geom_line(ggplot2::aes(x = .data[["x"]], y = .data[["y"]]), data = yintercepts, linewidth = axisWidth, inherit.aes = FALSE) + @@ -2189,21 +2283,21 @@ autoplot.dna_barplot <- function(object, } if (binary) { # Add entity labels for binary case g <- g + - ggplot2::geom_text(ggplot2::aes_string(x = "entity", - y = "pos", - label = "label"), + ggplot2::geom_text(ggplot2::aes(x = .data[["entity"]], + y = .data[["pos"]], + label = .data[["label"]]), size = (fontSize / ggplot2::.pt), inherit.aes = FALSE, data = object2) # Add entity labels for integer case with positive and negative values } else if (max(w) > 0 & min(w) < 0) { g <- g + - ggplot2::geom_text(ggplot2::aes_string(color = "text_color"), + ggplot2::geom_text(ggplot2::aes(color = .data[["text_color"]]), size = (fontSize / ggplot2::.pt), position = ggplot2::position_stack(vjust = 0.5, reverse = TRUE), inherit.aes = TRUE, data = object2[object2$frequency >= 0, ]) + - ggplot2::geom_text(ggplot2::aes_string(color = "text_color"), + ggplot2::geom_text(ggplot2::aes(color = .data[["text_color"]]), size = (fontSize / ggplot2::.pt), position = ggplot2::position_stack(vjust = 0.5), inherit.aes = TRUE, @@ -2211,13 +2305,13 @@ autoplot.dna_barplot <- function(object, # Add entity labels for integer case with positive values only } else if (min(w) >= 0) { g <- g + - ggplot2::geom_text(ggplot2::aes_string(color = "text_color"), + ggplot2::geom_text(ggplot2::aes(color = .data[["text_color"]]), size = (fontSize / ggplot2::.pt), position = ggplot2::position_stack(vjust = 0.5, reverse = TRUE), inherit.aes = TRUE) } else { g <- g + - ggplot2::geom_text(ggplot2::aes_string(color = "text_color"), + ggplot2::geom_text(ggplot2::aes(color = .data[["text_color"]]), size = (fontSize / ggplot2::.pt), position = ggplot2::position_stack(vjust = 0.5), inherit.aes = TRUE) @@ -2260,6 +2354,40 @@ autoplot.dna_barplot <- function(object, #' network to partition the set of second-mode entities (e.g., concepts) into a #' backbone set and a complementary redundant set. #' +#' @param method The backbone algorithm used to compute the results. Several +#' methods are available: +#' \itemize{ +#' \item \code{"nested"}: A relatively fast, deterministic algorithm that +#' produces the full hierarchy of entities. It starts with a complete +#' backbone set resembling the full network. There are as many iterations +#' as entities on the second mode. In each iteration, the entity whose +#' removal would yield the smallest backbone loss is moved from the +#' backbone set into the redundant set, and the (unpenalized) spectral +#' loss is recorded. This creates a solution for all backbone sizes, where +#' each backbone set is fully nested in the next larger backbone set. The +#' solution usually resembles an unconstrained solution where nesting is +#' not required, but in some cases the loss of a non-nested solution may be +#' larger at a given level or number of elements in the backbone set. +#' \item \code{"fixed"}: Simulated annealing with a fixed number of elements +#' in the backbone set (i.e., only lateral changes are possible) and +#' without penalty. This method may yield more optimal solutions than the +#' nested algorithm because it does not require a strict hierarchy. +#' However, it produces an approximation of the global optimum and is +#' slower than the nested method. With this method, you can specify that +#' backbone set should have, for example, exactly 10 concepts. Then fewer +#' iterations are necessary than with the penalty method because the search +#' space is smaller. The backbone set size is defined in the +#' \code{"backboneSize"} argument. +#' \item \code{"penalty"}: Simulated annealing with a variable number of +#' elements in the backbone set. The solution is stabilized by a penalty +#' parameter (see \code{"penalty"} argument). This algorithm takes longest +#' to compute for a single solution, and it is only an approximation, but +#' it considers slightly larger or smaller backbone sets if the solution is +#' better, thus this algorithm adds some flexibility. It requires more +#' iterations than the fixed method for achieving the same quality. +#' } +#' @param backboneSize The number of elements in the backbone set, as a fixed +#' parameter. Only used when \code{method = "fixed"}. #' @param penalty The penalty parameter for large backbone sets. The larger the #' value, the more strongly larger backbone sets are punished and the smaller #' the resulting backbone is. Try out different values to find the right size @@ -2268,10 +2396,12 @@ autoplot.dna_barplot <- function(object, #' imposes no penalty on the size of the backbone set and produces a redundant #' set with only one element. Start with \code{0.0} if you want to weed out a #' single concept and subsequently increase the penalty to include more items -#' in the redundant set and shrink the backbone further. +#' in the redundant set and shrink the backbone further. Only used when +#' \code{method = "penalty"}. #' @param iterations The number of iterations of the simulated annealing #' algorithm. More iterations take more time but may lead to better -#' optimization results. +#' optimization results. Only used when \code{method = "penalty"} or +#' \code{method = "fixed"}. #' @param qualifierAggregation The aggregation rule for the \code{qualifier} #' variable. This must be \code{"ignore"} (for ignoring the qualifier #' variable), \code{"congruence"} (for recording a network tie only if both @@ -2299,8 +2429,9 @@ autoplot.dna_barplot <- function(object, #' dna_sample() #' dna_openDatabase("sample.dna", coderId = 1, coderPassword = "sample") #' -#' # compute backbone and redundant set -#' b <- dna_backbone(penalty = 3.5, +#' # compute backbone and redundant set using penalised spectral loss +#' b <- dna_backbone(method = "penalty", +#' penalty = 3.5, #' iterations = 10000, #' variable1 = "organization", #' variable2 = "concept", @@ -2342,16 +2473,41 @@ autoplot.dna_barplot <- function(object, #' # use the gridExtra package to arrange the diagnostics in a single plot #' library("gridExtra") #' grid.arrange(p[[1]], p[[2]], p[[3]], p[[4]]) +#' +#' # compute backbone with fixed size (here: 4 concepts) +#' b <- dna_backbone(method = "fixed", +#' backboneSize = 4, +#' iterations = 2000, +#' variable1 = "organization", +#' variable2 = "concept", +#' qualifier = "agreement", +#' qualifierAggregation = "subtract", +#' normalization = "average") +#' b +#' +#' # compute backbone with a nested structure and plot dendrogram +#' b <- dna_backbone(method = "nested", +#' variable1 = "organization", +#' variable2 = "concept", +#' qualifier = "agreement", +#' qualifierAggregation = "subtract", +#' normalization = "average") +#' b +#' plot(b) +#' autoplot(b) #' } #' #' @author Philip Leifeld, Tim Henrichsen #' +#' @rdname dna_backbone #' @importFrom rJava .jarray #' @importFrom rJava .jcall #' @importFrom rJava .jnull #' @importFrom rJava J -#' @noRd -dna_backbone <- function(penalty = 3.5, +#' @export +dna_backbone <- function(method = "nested", + backboneSize = 1, + penalty = 3.5, iterations = 10000, statementType = "DNA Statement", variable1 = "organization", @@ -2423,6 +2579,8 @@ dna_backbone <- function(penalty = 3.5, .jcall(dnaEnvironment[["dna"]]$headlessDna, "V", "rBackbone", + method, + as.integer(backboneSize), as.double(penalty), as.integer(iterations), statementType, @@ -2455,13 +2613,18 @@ dna_backbone <- function(penalty = 3.5, ) exporter <- .jcall(dnaEnvironment[["dna"]]$headlessDna, "Lexport/Exporter;", "getExporter") # get a reference to the Exporter object, in which results are stored - result <- .jcall(exporter, "Lexport/BackboneResult;", "getBackboneResult", simplify = TRUE) if (!is.null(outfile) && !is.null(fileFormat) && is.character(outfile) && is.character(fileFormat) && fileFormat %in% c("json", "xml")) { message("File exported.") - } else { + } else if (method[1] %in% c("penalty", "fixed")) { + result <- .jcall(exporter, "Lexport/SimulatedAnnealingBackboneResult;", "getSimulatedAnnealingBackboneResult", simplify = TRUE) # create a list with various results l <- list() l$penalty <- .jcall(result, "D", "getPenalty") + if (method[1] == "fixed") { + l$backbone_size <- as.integer(backboneSize) + } else { + l$backbone_size <- as.integer(NA) + } l$iterations <- .jcall(result, "I", "getIterations") l$backbone <- .jcall(result, "[S", "getBackboneEntities") l$redundant <- .jcall(result, "[S", "getRedundantEntities") @@ -2509,83 +2672,154 @@ dna_backbone <- function(penalty = 3.5, attributes(l$full_network)$call <- match.call() attributes(l$backbone_network)$call <- match.call() attributes(l$redundant_network)$call <- match.call() + attributes(l)$method <- method[1] class(l$full_network) <- c("dna_network_onemode", class(l$full_network)) class(l$backbone_network) <- c("dna_network_onemode", class(l$backbone_network)) class(l$redundant_network) <- c("dna_network_onemode", class(l$redundant_network)) - class(l) <- c("dna_backbone", class(l)) return(l) + } else if (method[1] == "nested") { + result <- .jcall(exporter, "Lexport/NestedBackboneResult;", "getNestedBackboneResult", simplify = TRUE) + d <- data.frame(i = .jcall(result, "[I", "getIteration"), + entity = .jcall(result, "[S", "getEntities"), + backboneLoss = .jcall(result, "[D", "getBackboneLoss"), + redundantLoss = .jcall(result, "[D", "getRedundantLoss"), + statements = .jcall(result, "[I", "getNumStatements")) + rownames(d) <- NULL + attributes(d)$numStatementsFull <- .jcall(result, "I", "getNumStatementsFull") + attributes(d)$start <- as.POSIXct(.jcall(result, "J", "getStart"), origin = "1970-01-01") # add the start date/time of the result as an attribute + attributes(d)$stop <- as.POSIXct(.jcall(result, "J", "getStop"), origin = "1970-01-01") # add the end date/time of the result as an attribute + attributes(d)$method <- "nested" + class(d) <- c("dna_backbone", class(d)) + return(d) } } #' @rdname dna_backbone #' @param x A \code{"dna_backbone"} object. -#' @noRd -print.dna_backbone <- function(x, ...) { - cat(paste0("Penalty: ", x$penalty, ". Iterations: ", x$iterations, ".\n\n")) - cat(paste0("Backbone set (loss: ", round(x$unpenalized_backbone_loss, 4), "):\n")) - cat(paste(1:length(x$backbone), x$backbone), sep = "\n") - cat(paste0("\nRedundant set (loss: ", round(x$unpenalized_redundant_loss, 4), "):\n")) - cat(paste(1:length(x$redundant), x$redundant), sep = "\n") +#' @param trim Number of maximum characters to display in entity labels. Labels +#' with more characters are truncated, and the last character is replaced by +#' an asterisk (\code{*}). +#' @export +print.dna_backbone <- function(x, trim = 50, ...) { + method <- attributes(x)$method + cat(paste0("Backbone method: ", method, ".\n\n")) + if (method %in% c("penalty", "fixed")) { + if (method == "penalty") { + cat(paste0("Penalty: ", x$penalty, ". Iterations: ", x$iterations, ".\n\n")) + } else { + cat(paste0("Backbone size: ", x$backbone_size, ". Iterations: ", x$iterations, ".\n\n")) + } + cat(paste0("Backbone set (loss: ", round(x$unpenalized_backbone_loss, 4), "):\n")) + cat(paste(1:length(x$backbone), x$backbone), sep = "\n") + cat(paste0("\nRedundant set (loss: ", round(x$unpenalized_redundant_loss, 4), "):\n")) + cat(paste(1:length(x$redundant), x$redundant), sep = "\n") + } else if (method == "nested") { + x2 <- x + x2$entity <- sapply(x2$entity, function(r) if (nchar(r) > trim) paste0(substr(r, 1, trim - 1), "*") else r) + print(as.data.frame(x2), row.names = FALSE) + } } #' @param ma Number of iterations to compute moving average. #' @rdname dna_backbone #' @importFrom graphics lines #' @importFrom stats filter -#' @noRd +#' @importFrom rlang .data +#' @export plot.dna_backbone <- function(x, ma = 500, ...) { - # temperature and acceptance probability - plot(x = x$diagnostics$iteration, - y = x$diagnostics$temperature, - col = "red", - type = "l", - lwd = 3, - xlab = "Iteration", - ylab = "Acceptance probability", - main = "Temperature and acceptance probability") - # note that better solutions are coded as -1 and need to be skipped: - lines(x = x$diagnostics$iteration[x$diagnostics$acceptance_prob >= 0], - y = x$diagnostics$acceptance_prob[x$diagnostics$acceptance_prob >= 0]) - - # spectral distance between full network and backbone network per iteration - bb_loss <- stats::filter(x$diagnostics$penalized_backbone_loss, - rep(1 / ma, ma), - sides = 1) - plot(x = x$diagnostics$iteration, - y = bb_loss, - type = "l", - xlab = "Iteration", - ylab = "Penalized backbone loss", - main = "Penalized spectral backbone distance") - - # number of concepts in the backbone solution per iteration - current_size_ma <- stats::filter(x$diagnostics$current_backbone_size, - rep(1 / ma, ma), - sides = 1) - optimal_size_ma <- stats::filter(x$diagnostics$optimal_backbone_size, - rep(1 / ma, ma), - sides = 1) - plot(x = x$diagnostics$iteration, - y = current_size_ma, - ylim = c(min(c(current_size_ma, optimal_size_ma), na.rm = TRUE), - max(c(current_size_ma, optimal_size_ma), na.rm = TRUE)), - type = "l", - xlab = "Iteration", - ylab = paste0("Number of elements (MA, last ", ma, ")"), - main = "Backbone size (red = best)") - lines(x = x$diagnostics$iteration, y = optimal_size_ma, col = "red") - - # ratio of recent acceptances - accept_ratio <- stats::filter(x$diagnostics$acceptance, - rep(1 / ma, ma), - sides = 1) - plot(x = x$diagnostics$iteration, - y = accept_ratio, - type = "l", - xlab = "Iteration", - ylab = paste("Acceptance ratio in the last", ma, "iterations"), - main = "Acceptance ratio") + + if (attr(x, "method") != "nested") { + # temperature and acceptance probability + plot(x = x$diagnostics$iteration, + y = x$diagnostics$temperature, + col = "red", + type = "l", + lwd = 3, + xlab = "Iteration", + ylab = "Acceptance probability", + main = "Temperature and acceptance probability") + # note that better solutions are coded as -1 and need to be skipped: + lines(x = x$diagnostics$iteration[x$diagnostics$acceptance_prob >= 0], + y = x$diagnostics$acceptance_prob[x$diagnostics$acceptance_prob >= 0]) + + # spectral distance between full network and backbone network per iteration + bb_loss <- stats::filter(x$diagnostics$penalized_backbone_loss, + rep(1 / ma, ma), + sides = 1) + if (attributes(x)$method == "penalty") { + yl <- "Penalized backbone loss" + ti <- "Penalized spectral backbone distance" + } else { + yl <- "Backbone loss" + ti <- "Spectral backbone distance" + } + plot(x = x$diagnostics$iteration, + y = bb_loss, + type = "l", + xlab = "Iteration", + ylab = yl, + main = ti) + + # number of concepts in the backbone solution per iteration + current_size_ma <- stats::filter(x$diagnostics$current_backbone_size, + rep(1 / ma, ma), + sides = 1) + optimal_size_ma <- stats::filter(x$diagnostics$optimal_backbone_size, + rep(1 / ma, ma), + sides = 1) + plot(x = x$diagnostics$iteration, + y = current_size_ma, + ylim = c(min(c(current_size_ma, optimal_size_ma), na.rm = TRUE), + max(c(current_size_ma, optimal_size_ma), na.rm = TRUE)), + type = "l", + xlab = "Iteration", + ylab = paste0("Number of elements (MA, last ", ma, ")"), + main = "Backbone size (red = best)") + lines(x = x$diagnostics$iteration, y = optimal_size_ma, col = "red") + + # ratio of recent acceptances + accept_ratio <- stats::filter(x$diagnostics$acceptance, + rep(1 / ma, ma), + sides = 1) + plot(x = x$diagnostics$iteration, + y = accept_ratio, + type = "l", + xlab = "Iteration", + ylab = paste("Acceptance ratio in the last", ma, "iterations"), + main = "Acceptance ratio") + } else { # create hclust object + # define merging pattern: negative numbers are leaves, positive are merged clusters + merges_clust <- matrix(nrow = nrow(x) - 1, ncol = 2) + + merges_clust[1,1] <- -nrow(x) + merges_clust[1,2] <- -(nrow(x) - 1) + + for (i in 2:(nrow(x) - 1)) { + merges_clust[i, 1] <- -(nrow(x) - i) + merges_clust[i, 2] <- i - 1 + } + + # Initialize empty object + a <- list() + + # Add merges + a$merge <- merges_clust + + # Define merge heights + a$height <- x$backboneLoss[1:nrow(x) - 1] + + # Order of leaves + a$order <- 1:nrow(x) + + # Labels of leaves + a$labels <- rev(x$entity) + + # Define hclust class + class(a) <- "hclust" + + plot(a, ylab = "") + } } #' @rdname dna_backbone @@ -2593,61 +2827,2141 @@ plot.dna_backbone <- function(x, ma = 500, ...) { #' @param ... Additional arguments. #' @importFrom ggplot2 autoplot #' @importFrom ggplot2 ggplot -#' @importFrom ggplot2 aes_string +#' @importFrom ggplot2 aes #' @importFrom ggplot2 geom_line #' @importFrom ggplot2 ylab #' @importFrom ggplot2 xlab #' @importFrom ggplot2 ggtitle #' @importFrom ggplot2 theme_bw #' @importFrom ggplot2 theme -#' @noRd +#' @importFrom ggplot2 coord_flip +#' @importFrom ggplot2 scale_x_continuous +#' @importFrom ggplot2 scale_y_continuous +#' @importFrom rlang .data +#' @export autoplot.dna_backbone <- function(object, ..., ma = 500) { - bd <- object$diagnostics - bd$bb_loss <- stats::filter(bd$penalized_backbone_loss, rep(1 / ma, ma), sides = 1) - bd$current_size_ma <- stats::filter(bd$current_backbone_size, rep(1 / ma, ma), sides = 1) - bd$optimal_size_ma <- stats::filter(bd$optimal_backbone_size, rep(1 / ma, ma), sides = 1) - bd$accept_ratio <- stats::filter(bd$acceptance, rep(1 / ma, ma), sides = 1) - - # temperature and acceptance probability - g_accept <- ggplot2::ggplot(bd, ggplot2::aes_string(y = "temperature", x = "iteration")) + - ggplot2::geom_line(color = "#a50f15") + - ggplot2::geom_line(data = bd[bd$acceptance_prob >= 0, ], - ggplot2::aes_string(y = "acceptance_prob", x = "iteration")) + - ggplot2::ylab("Acceptance probability") + - ggplot2::xlab("Iteration") + - ggplot2::ggtitle("Temperature and acceptance probability") + - ggplot2::theme_bw() - - # spectral distance between full network and backbone network per iteration - g_loss <- ggplot2::ggplot(bd, ggplot2::aes_string(y = "bb_loss", x = "iteration")) + - ggplot2::geom_line() + - ggplot2::ylab("Penalized backbone loss") + - ggplot2::xlab("Iteration") + - ggplot2::ggtitle("Penalized spectral backbone distance") + - ggplot2::theme_bw() - - # number of concepts in the backbone solution per iteration - d <- data.frame(iteration = rep(bd$iteration, 2), - size = c(bd$current_size_ma, bd$optimal_size_ma), - Criterion = c(rep("Current iteration", nrow(bd)), - rep("Best solution", nrow(bd)))) - g_size <- ggplot2::ggplot(d, ggplot2::aes_string(y = "size", x = "iteration", color = "Criterion")) + - ggplot2::geom_line() + - ggplot2::ylab(paste0("Number of elements (MA, last ", ma, ")")) + - ggplot2::xlab("Iteration") + - ggplot2::ggtitle("Backbone size") + - ggplot2::theme_bw() + - ggplot2::theme(legend.position = "bottom") - - # ratio of recent acceptances - g_ar <- ggplot2::ggplot(bd, ggplot2::aes_string(y = "accept_ratio", x = "iteration")) + - ggplot2::geom_line() + - ggplot2::ylab(paste("Acceptance ratio in the last", ma, "iterations")) + - ggplot2::xlab("Iteration") + - ggplot2::ggtitle("Acceptance ratio") + - ggplot2::theme_bw() - - # wrap in list - plots <- list(g_accept, g_loss, g_size, g_ar) - return(plots) -} \ No newline at end of file + if (attr(object, "method") != "nested") { + bd <- object$diagnostics + bd$bb_loss <- stats::filter(bd$penalized_backbone_loss, rep(1 / ma, ma), sides = 1) + bd$current_size_ma <- stats::filter(bd$current_backbone_size, rep(1 / ma, ma), sides = 1) + bd$optimal_size_ma <- stats::filter(bd$optimal_backbone_size, rep(1 / ma, ma), sides = 1) + bd$accept_ratio <- stats::filter(bd$acceptance, rep(1 / ma, ma), sides = 1) + + # temperature and acceptance probability + g_accept <- ggplot2::ggplot(bd, ggplot2::aes(y = .data[["temperature"]], x = .data[["iteration"]])) + + ggplot2::geom_line(color = "#a50f15") + + ggplot2::geom_line(data = bd[bd$acceptance_prob >= 0, ], + ggplot2::aes(y = .data[["acceptance_prob"]], x = .data[["iteration"]])) + + ggplot2::ylab("Acceptance probability") + + ggplot2::xlab("Iteration") + + ggplot2::ggtitle("Temperature and acceptance probability") + + ggplot2::theme_bw() + + # spectral distance between full network and backbone network per iteration + if (attributes(object)$method == "penalty") { + yl <- "Penalized backbone loss" + ti <- "Penalized spectral backbone distance" + } else { + yl <- "Backbone loss" + ti <- "Spectral backbone distance" + } + g_loss <- ggplot2::ggplot(bd, ggplot2::aes(y = .data[["bb_loss"]], x = .data[["iteration"]])) + + ggplot2::geom_line() + + ggplot2::ylab(yl) + + ggplot2::xlab("Iteration") + + ggplot2::ggtitle(ti) + + ggplot2::theme_bw() + + # number of concepts in the backbone solution per iteration + d <- data.frame(iteration = rep(bd$iteration, 2), + size = c(bd$current_size_ma, bd$optimal_size_ma), + Criterion = c(rep("Current iteration", nrow(bd)), + rep("Best solution", nrow(bd)))) + g_size <- ggplot2::ggplot(d, ggplot2::aes(y = .data[["size"]], x = .data[["iteration"]], color = .data[["Criterion"]])) + + ggplot2::geom_line() + + ggplot2::ylab(paste0("Number of elements (MA, last ", ma, ")")) + + ggplot2::xlab("Iteration") + + ggplot2::ggtitle("Backbone size") + + ggplot2::theme_bw() + + ggplot2::theme(legend.position = "bottom") + + # ratio of recent acceptances + g_ar <- ggplot2::ggplot(bd, ggplot2::aes(y = .data[["accept_ratio"]], x = .data[["iteration"]])) + + ggplot2::geom_line() + + ggplot2::ylab(paste("Acceptance ratio in the last", ma, "iterations")) + + ggplot2::xlab("Iteration") + + ggplot2::ggtitle("Acceptance ratio") + + ggplot2::theme_bw() + + # wrap in list + plots <- list(g_accept, g_loss, g_size, g_ar) + return(plots) + } else { # create hclust object + # define merging pattern: negative numbers are leaves, positive are merged clusters + merges_clust <- matrix(nrow = nrow(object) - 1, ncol = 2) + + merges_clust[1,1] <- -nrow(object) + merges_clust[1,2] <- -(nrow(object) - 1) + + for (i in 2:(nrow(object) - 1)) { + merges_clust[i, 1] <- -(nrow(object) - i) + merges_clust[i, 2] <- i - 1 + } + + # Initialize empty object + a <- list() + + # Add merges + a$merge <- merges_clust + + # Define merge heights + a$height <- object$backboneLoss[1:nrow(object) - 1] + height <- a$height + + # Order of leaves + a$order <- 1:nrow(object) + + # Labels of leaves + a$labels <- rev(object$entity) + + # Define hclust class + class(a) <- "hclust" + + # ensure ggraph is installed, otherwise throw error (better than importing it to avoid hard dependency) + if (!requireNamespace("ggraph", quietly = TRUE)) { + stop("The 'ggraph' package is required for plotting nested backbone dendrograms with 'ggplot2' but was not found. Consider installing it.") + } + + g_clust <- ggraph::ggraph(graph = a, + layout = "dendrogram", + circular = FALSE, + height = height) + # TODO @Tim: "height" does not seem to exist + ggraph::geom_edge_elbow() + + ggraph::geom_node_point(aes(filter = .data[["leaf"]])) + + ggplot2::theme_bw() + + ggplot2::theme(panel.border = element_blank(), + axis.title = element_blank(), + panel.grid.major = element_blank(), + panel.grid.minor = element_blank(), + axis.line = element_blank(), + axis.text.y = element_text(size = 6), + axis.ticks.y = element_blank()) + + ggplot2::scale_x_continuous(breaks = seq(0, nrow(object) - 1, by = 1), + labels = rev(object$entity)) + + ggplot2::scale_y_continuous(expand = c(0, 0.01)) + + ggplot2::coord_flip() + + return(g_clust) + } +} + +#' Evaluate the spectral loss for an arbitrary set of entities +#' +#' Compute the backbone loss for any set of entities, for example concepts. +#' +#' This function computes the spectral loss for an arbitrary backbone and its +#' complement, the redundant set, specified by the user. For example, the user +#' can evaluate how much structure would be lost if the second mode was composed +#' only of the concepts provided to this function. This can be used to compare +#' how useful different codebook models are. The penalty parameter \code{p} +#' applies a penalty factor to the spectral loss. The default value of \code{0} +#' switches off the penalty. +#' +#' @param backboneEntities A vector of character values to be included in the +#' backbone. The function will compute the spectral loss between the full +#' network and the network composed only of those entities on the second mode +#' that are contained in this vector. +#' @param p The penalty parameter. The default value of \code{0} means no +#' penalty for backbone size is applied. +#' @inheritParams dna_backbone +#' @return A vector with two numeric values: the backbone and redundant loss. +#' +#' @examples +#' \dontrun{ +#' dna_init() +#' dna_sample() +#' dna_openDatabase("sample.dna", coderId = 1, coderPassword = "sample") +#' +#' dna_evaluateBackboneSolution( +#' c("There should be legislation to regulate emissions.", +#' "Emissions legislation should regulate CO2.") +#' ) +#' } +#' +#' @author Philip Leifeld +#' +#' @rdname dna_backbone +#' @importFrom rJava .jarray +#' @importFrom rJava .jcall +#' @importFrom rJava .jnull +#' @export +dna_evaluateBackboneSolution <- function(backboneEntities, + p = 0, + statementType = "DNA Statement", + variable1 = "organization", + variable1Document = FALSE, + variable2 = "concept", + variable2Document = FALSE, + qualifier = "agreement", + qualifierDocument = FALSE, + qualifierAggregation = "subtract", + normalization = "average", + duplicates = "document", + start.date = "01.01.1900", + stop.date = "31.12.2099", + start.time = "00:00:00", + stop.time = "23:59:59", + excludeValues = list(), + excludeAuthors = character(), + excludeSources = character(), + excludeSections = character(), + excludeTypes = character(), + invertValues = FALSE, + invertAuthors = FALSE, + invertSources = FALSE, + invertSections = FALSE, + invertTypes = FALSE) { + + # wrap the vectors of exclude values for document variables into Java arrays + excludeAuthors <- .jarray(excludeAuthors) + excludeSources <- .jarray(excludeSources) + excludeSections <- .jarray(excludeSections) + excludeTypes <- .jarray(excludeTypes) + + # compile exclude variables and values vectors + dat <- matrix("", nrow = length(unlist(excludeValues)), ncol = 2) + count <- 0 + if (length(excludeValues) > 0) { + for (i in 1:length(excludeValues)) { + if (length(excludeValues[[i]]) > 0) { + for (j in 1:length(excludeValues[[i]])) { + count <- count + 1 + dat[count, 1] <- names(excludeValues)[i] + dat[count, 2] <- excludeValues[[i]][j] + } + } + } + var <- dat[, 1] + val <- dat[, 2] + } else { + var <- character() + val <- character() + } + var <- .jarray(var) # array of variable names of each excluded value + val <- .jarray(val) # array of values to be excluded + + # encode R NULL as Java null value if necessary + if (is.null(qualifier) || is.na(qualifier)) { + qualifier <- .jnull(class = "java/lang/String") + } + + # call rBackbone function to compute results + result <- .jcall(dnaEnvironment[["dna"]]$headlessDna, + "[D", + "rEvaluateBackboneSolution", + .jarray(backboneEntities), + as.integer(p), + statementType, + variable1, + variable1Document, + variable2, + variable2Document, + qualifier, + qualifierDocument, + qualifierAggregation, + normalization, + duplicates, + start.date, + stop.date, + start.time, + stop.time, + var, + val, + excludeAuthors, + excludeSources, + excludeSections, + excludeTypes, + invertValues, + invertAuthors, + invertSources, + invertSections, + invertTypes + ) + names(result) <- c("backbone loss", "redundant loss") + return(result) +} + + +# Clustering ------------------------------------------------------------------- + +#' Compute multiple cluster solutions for a discourse network +#' +#' Compute multiple cluster solutions for a discourse network. +#' +#' This function applies a number of different graph clustering techniques to +#' a discourse network dataset. The user provides many of the same arguments as +#' in the \code{\link{dna_network}} function and a few additional arguments that +#' determine which kinds of clustering methods should be used and how. In +#' particular, the \code{k} argument can be \code{0} (for arbitrary numbers of +#' clusters) or any positive integer value (e.g., \code{2}, for constraining the +#' number of clusters to exactly \code{k} groups). This is useful for assessing +#' the polarization of a discourse network. +#' +#' In particular, the function can be used to compute the maximal modularity of +#' a smoothed time series of discourse networks using the \code{timeWindow} and +#' \code{windowSize} arguments for a given \code{k} across a number of +#' clustering methods. +#' +#' It is also possible to switch off all but one clustering method using the +#' respective arguments and carry out a simple cluster analysis with the method +#' of choice for a certain time span of the discourse network, without any time +#' window options. +#' +#' @param saveObjects Store the original output of the respective clustering +#' method in the \code{cl} slot of the return object? If \code{TRUE}, one +#' cluster object per time point will be saved, for all time points for which +#' network data are available. At each time point, only the cluster object +#' with the highest modularity score will be saved, all others discarded. The +#' \code{max_mod} slot of the object contains additional information on which +#' measure was saved at each time point and what the corresponding modularity +#' score is. +#' @param k The number of clusters to compute. This constrains the choice of +#' clustering methods because some methods require a predefined \code{k} while +#' other methods do not. To permit arbitrary numbers of clusters, depending on +#' the respective algorithm (or the value of modularity in some cases), choose +#' \code{k = 0}. This corresponds to the theoretical notion of +#' "multipolarization". For "bipolarization", choose \code{k = 2} in order to +#' constrain the cluster solutions to exactly two groups. +#' @param k.max If \code{k = 0}, there can be arbitrary numbers of clusters. In +#' this case, \code{k.max} sets the maximal number of clusters that can be +#' identified. +#' @param single Include hierarchical clustering with single linkage in the pool +#' of clustering methods? The \code{\link[stats]{hclust}} function from +#' the \pkg{stats} package is applied to Jaccard distances in the affiliation +#' network for this purpose. Only valid if \code{k > 1}. +#' @param average Include hierarchical clustering with average linkage in the +#' pool of clustering methods? The \code{\link[stats]{hclust}} function from +#' the \pkg{stats} package is applied to Jaccard distances in the affiliation +#' network for this purpose. Only valid if \code{k > 1}. +#' @param complete Include hierarchical clustering with complete linkage in the +#' pool of clustering methods? The \code{\link[stats]{hclust}} function from +#' the \pkg{stats} package is applied to Jaccard distances in the affiliation +#' network for this purpose. Only valid if \code{k > 1}. +#' @param ward Include hierarchical clustering with Ward's algorithm in the +#' pool of clustering methods? The \code{\link[stats]{hclust}} function from +#' the \pkg{stats} package is applied to Jaccard distances in the affiliation +#' network for this purpose. If \code{k = 0} is selected, different solutions +#' with varying \code{k} are attempted, and the solution with the highest +#' modularity is retained. +#' @param kmeans Include k-means clustering in the pool of clustering methods? +#' The \code{\link[stats]{kmeans}} function from the \pkg{stats} package is +#' applied to Jaccard distances in the affiliation network for this purpose. +#' If \code{k = 0} is selected, different solutions with varying \code{k} are +#' attempted, and the solution with the highest modularity is retained. +#' @param pam Include partitioning around medoids in the pool of clustering +#' methods? The \code{\link[cluster]{pam}} function from the \pkg{cluster} +#' package is applied to Jaccard distances in the affiliation network for this +#' purpose. If \code{k = 0} is selected, different solutions with varying +#' \code{k} are attempted, and the solution with the highest modularity is +#' retained. +#' @param equivalence Include equivalence clustering (as implemented in the +#' \code{\link[sna]{equiv.clust}} function in the \pkg{sna} package), based on +#' shortest path distances between nodes (as implemented in the +#' \code{\link[sna]{sedist}} function in the \pkg{sna} package) in the +#' positive subtract network? If \code{k = 0} is selected, different solutions +#' with varying \code{k} are attempted, and the solution with the highest +#' modularity is retained. +#' @param concor_one Include CONvergence of iterative CORrelations (CONCOR) in +#' the pool of clustering methods? The algorithm is applied to the positive +#' subtract network to identify \code{k = 2} clusters. The method is omitted +#' if \code{k != 2}. +#' @param concor_two Include CONvergence of iterative CORrelations (CONCOR) in +#' the pool of clustering methods? The algorithm is applied to the affiliation +#' network to identify \code{k = 2} clusters. The method is omitted +#' if \code{k != 2}. +#' @param louvain Include the Louvain community detection algorithm in the pool +#' of clustering methods? The \code{\link[igraph]{cluster_louvain}} function +#' in the \pkg{igraph} package is applied to the positive subtract network for +#' this purpose. +#' @param fastgreedy Include the fast and greedy community detection algorithm +#' in the pool of clustering methods? The +#' \code{\link[igraph]{cluster_fast_greedy}} function in the \pkg{igraph} +#' package is applied to the positive subtract network for this purpose. +#' @param walktrap Include the Walktrap community detection algorithm +#' in the pool of clustering methods? The +#' \code{\link[igraph]{cluster_walktrap}} function in the \pkg{igraph} +#' package is applied to the positive subtract network for this purpose. +#' @param leading_eigen Include the leading eigenvector community detection +#' algorithm in the pool of clustering methods? The +#' \code{\link[igraph]{cluster_leading_eigen}} function in the \pkg{igraph} +#' package is applied to the positive subtract network for this purpose. +#' @param edge_betweenness Include the edge betweenness community detection +#' algorithm by Girvan and Newman in the pool of clustering methods? The +#' \code{\link[igraph]{cluster_edge_betweenness}} function in the \pkg{igraph} +#' package is applied to the positive subtract network for this purpose. +#' @param infomap Include the infomap community detection algorithm +#' in the pool of clustering methods? The +#' \code{\link[igraph]{cluster_infomap}} function in the \pkg{igraph} +#' package is applied to the positive subtract network for this purpose. +#' @param label_prop Include the label propagation community detection algorithm +#' in the pool of clustering methods? The +#' \code{\link[igraph]{cluster_label_prop}} function in the \pkg{igraph} +#' package is applied to the positive subtract network for this purpose. +#' @param spinglass Include the spinglass community detection algorithm +#' in the pool of clustering methods? The +#' \code{\link[igraph]{cluster_spinglass}} function in the \pkg{igraph} +#' package is applied to the positive subtract network for this purpose. Note +#' that this method is disabled by default because it is relatively slow. +#' @inheritParams dna_network +#' +#' @return The function creates a \code{dna_multiclust} object, which contains +#' the following items: +#' \describe{ +#' \item{k}{The number of clusters determined by the user.} +#' \item{cl}{Cluster objects returned by the respective cluster function. If +#' multiple methods are used, this returns the object with the highest +#' modularity.} +#' \item{max_mod}{A data frame with one row per time point (that is, only one +#' row in the default case and multiple rows if time windows are used) and +#' the maximal modularity for the given time point across all cluster +#' methods.} +#' \item{modularity}{A data frame with the modularity values for all separate +#' cluster methods and all time points.} +#' \item{membership}{A large data frame with all nodes' membership information +#' for each time point and each clustering method.} +#' } +#' +#' @author Philip Leifeld +#' +#' @examples +#' \dontrun{ +#' library("rDNA") +#' dna_init() +#' samp <- dna_sample() +#' dna_openDatabase(samp, coderId = 1, coderPassword = "sample") +#' +#' # example 1: compute 12 cluster solutions for one time point +#' mc1 <- dna_multiclust(variable1 = "organization", +#' variable2 = "concept", +#' qualifier = "agreement", +#' duplicates = "document", +#' k = 0, # flexible numbers of clusters +#' saveObjects = TRUE) # retain hclust object +#' +#' mc1$modularity # return modularity scores for 12 clustering methods +#' mc1$max_mod # return the maximal value of the 12, along with dates +#' mc1$memberships # return cluster memberships for all 12 cluster methods +#' plot(mc1$cl[[1]]) # plot hclust dendrogram +#' +#' # example 2: compute only Girvan-Newman edge betweenness with two clusters +#' set.seed(12345) +#' mc2 <- dna_multiclust(k = 2, +#' single = FALSE, +#' average = FALSE, +#' complete = FALSE, +#' ward = FALSE, +#' kmeans = FALSE, +#' pam = FALSE, +#' equivalence = FALSE, +#' concor_one = FALSE, +#' concor_two = FALSE, +#' louvain = FALSE, +#' fastgreedy = FALSE, +#' walktrap = FALSE, +#' leading_eigen = FALSE, +#' edge_betweenness = TRUE, +#' infomap = FALSE, +#' label_prop = FALSE, +#' spinglass = FALSE) +#' mc2$memberships # return membership in two clusters +#' mc2$modularity # return modularity of the cluster solution +#' +#' # example 3: smoothed modularity using time window algorithm +#' mc3 <- dna_multiclust(k = 2, +#' timeWindow = "events", +#' windowSize = 28) +#' mc3$max_mod # maximal modularity and method per time point +#' } +#' +#' @rdname dna_multiclust +#' @importFrom stats as.dist cor hclust cutree kmeans +#' @export +dna_multiclust <- function(statementType = "DNA Statement", + variable1 = "organization", + variable1Document = FALSE, + variable2 = "concept", + variable2Document = FALSE, + qualifier = "agreement", + duplicates = "include", + start.date = "01.01.1900", + stop.date = "31.12.2099", + start.time = "00:00:00", + stop.time = "23:59:59", + timeWindow = "no", + windowSize = 100, + excludeValues = list(), + excludeAuthors = character(), + excludeSources = character(), + excludeSections = character(), + excludeTypes = character(), + invertValues = FALSE, + invertAuthors = FALSE, + invertSources = FALSE, + invertSections = FALSE, + invertTypes = FALSE, + saveObjects = FALSE, + k = 0, + k.max = 5, + single = TRUE, + average = TRUE, + complete = TRUE, + ward = TRUE, + kmeans = TRUE, + pam = TRUE, + equivalence = TRUE, + concor_one = TRUE, + concor_two = TRUE, + louvain = TRUE, + fastgreedy = TRUE, + walktrap = TRUE, + leading_eigen = TRUE, + edge_betweenness = TRUE, + infomap = TRUE, + label_prop = TRUE, + spinglass = FALSE) { + + # check dependencies + if (!requireNamespace("igraph", quietly = TRUE)) { # version 0.8.1 required for edge betweenness to work fine. + stop("The 'dna_multiclust' function requires the 'igraph' package to be installed.\n", + "To do this, enter 'install.packages(\"igraph\")'.") + } else if (packageVersion("igraph") < "0.8.1" && edge_betweenness) { + warning("Package version of 'igraph' < 0.8.1. If edge betweenness algorithm encounters an empty network matrix, this will let R crash. See here: https://github.com/igraph/rigraph/issues/336. Consider updating 'igraph' to the latest version.") + } + if (pam && !requireNamespace("cluster", quietly = TRUE)) { + pam <- FALSE + warning("Argument 'pam = TRUE' requires the 'cluster' package, which is not installed.\nSetting 'pam = FALSE'. Consider installing the 'cluster' package.") + } + if (equivalence && !requireNamespace("sna", quietly = TRUE)) { + equivalence <- FALSE + warning("Argument 'equivalence = TRUE' requires the 'sna' package, which is not installed.\nSetting 'equivalence = FALSE'. Consider installing the 'sna' package.") + } + + # check argument validity + if (is.null(k) || is.na(k) || !is.numeric(k) || length(k) > 1 || is.infinite(k) || k < 0) { + stop("'k' must be a non-negative integer number. Can be 0 for flexible numbers of clusters.") + } + if (is.null(k.max) || is.na(k.max) || !is.numeric(k.max) || length(k.max) > 1 || is.infinite(k.max) || k.max < 1) { + stop("'k.max' must be a positive integer number.") + } + if (k == 1) { + k <- 0 + warning("'k' must be 0 (for arbitrary numbers of clusters) or larger than 1 (to constrain number of clusters). Using 'k = 0'.") + } + + # determine what kind of two-mode network to create + if (is.null(qualifier) || is.na(qualifier) || !is.character(qualifier)) { + qualifierAggregation <- "ignore" + } else { + v <- dna_getVariables(statementType = statementType) + if (v$type[v$label == qualifier] == "boolean") { + qualifierAggregation <- "combine" + } else { + qualifierAggregation <- "subtract" + } + } + + nw_aff <- dna_network(networkType = "twomode", + statementType = statementType, + variable1 = variable1, + variable1Document = variable1Document, + variable2 = variable2, + variable2Document = variable2Document, + qualifier = qualifier, + qualifierAggregation = qualifierAggregation, + normalization = "no", + duplicates = duplicates, + start.date = start.date, + stop.date = stop.date, + start.time = start.time, + stop.time = stop.time, + timeWindow = timeWindow, + windowSize = windowSize, + excludeValues = excludeValues, + excludeAuthors = excludeAuthors, + excludeSources = excludeSources, + excludeSections = excludeSections, + excludeTypes = excludeTypes, + invertValues = invertValues, + invertAuthors = invertAuthors, + invertSources = invertSources, + invertSections = invertSections, + invertTypes = invertTypes) + nw_sub <- dna_network(networkType = "onemode", + statementType = statementType, + variable1 = variable1, + variable1Document = variable1Document, + variable2 = variable2, + variable2Document = variable2Document, + qualifier = qualifier, + qualifierAggregation = "subtract", + normalization = "average", + duplicates = duplicates, + start.date = start.date, + stop.date = stop.date, + start.time = start.time, + stop.time = stop.time, + timeWindow = timeWindow, + windowSize = windowSize, + excludeValues = excludeValues, + excludeAuthors = excludeAuthors, + excludeSources = excludeSources, + excludeSections = excludeSections, + excludeTypes = excludeTypes, + invertValues = invertValues, + invertAuthors = invertAuthors, + invertSources = invertSources, + invertSections = invertSections, + invertTypes = invertTypes) + + if (timeWindow == "no") { + dta <- list() + dta$networks <- list(nw_sub) + nw_sub <- dta + dta <- list() + dta$networks <- list(nw_aff) + nw_aff <- dta + } + + obj <- list() + if (isTRUE(saveObjects)) { + obj$cl <- list() + } + dta_dat <- list() + dta_mem <- list() + dta_mod <- list() + counter <- 1 + if ("dna_network_onemode_timewindows" %in% class(nw_sub)) { + num_networks <- length(nw_sub) + } else { + num_networks <- 1 + } + for (i in 1:num_networks) { + + # prepare dates + if (timeWindow == "no") { + dta_dat[[i]] <- data.frame(i = i, + start = attributes(nw_sub$networks[[i]])$start, + stop = attributes(nw_sub$networks[[i]])$stop) + } else { + dta_dat[[i]] <- data.frame(i = i, + start.date = attributes(nw_sub[[i]])$start, + middle.date = attributes(nw_sub[[i]])$middle, + stop.date = attributes(nw_sub[[i]])$stop) + } + + # prepare two-mode network + if ("dna_network_onemode_timewindows" %in% class(nw_sub)) { + x <- nw_aff[[i]] + } else { + x <- nw_aff$networks[[i]] + } + if (qualifierAggregation == "combine") { + combined <- cbind(apply(x, 1:2, function(x) ifelse(x %in% c(1, 3), 1, 0)), + apply(x, 1:2, function(x) ifelse(x %in% c(2, 3), 1, 0))) + } else { + combined <- x + } + combined <- combined[rowSums(combined) > 0, , drop = FALSE] + rn <- rownames(combined) + + # Jaccard distances for two-mode network (could be done using vegdist function in vegan package, but saving the dependency) + combined <- matrix(as.integer(combined > 0), nrow = nrow(combined)) # probably not necessary, but ensure it's an integer matrix + intersections <- tcrossprod(combined) # compute intersections using cross-product + row_sums <- rowSums(combined) # compute row sums + unions <- matrix(outer(row_sums, row_sums, `+`), ncol = length(row_sums)) - intersections # compute unions + jaccard_similarities <- intersections / unions # calculate Jaccard similarities + jaccard_similarities[is.nan(jaccard_similarities)] <- 0 # avoid division by zero + jaccard_distances <- 1 - jaccard_similarities # convert to Jaccard distances + rownames(jaccard_distances) <- rn # re-attach the row names + jac <- stats::as.dist(jaccard_distances) # convert to dist object + + # prepare one-mode network + if ("dna_network_onemode_timewindows" %in% class(nw_sub)) { + y <- nw_sub[[i]] + } else { + y <- nw_sub$networks[[i]] + } + y[y < 0] <- 0 + class(y) <- "matrix" + g <- igraph::graph.adjacency(y, mode = "undirected", weighted = TRUE) + + if (nrow(combined) > 1) { + counter_current <- 1 + current_cl <- list() + current_mod <- numeric() + + # Hierarchical clustering with single linkage + if (isTRUE(single) && k > 1) { + try({ + suppressWarnings(cl <- stats::hclust(jac, method = "single")) + mem <- stats::cutree(cl, k = k) + dta_mem[[counter]] <- data.frame(i = rep(i, length(mem)), + method = rep("Hierarchical (Single)", length(mem)), + node = rownames(x), + cluster = mem, + stringsAsFactors = FALSE) + dta_mod[[counter]] <- data.frame(i = i, + method = "Hierarchical (Single)", + k = k, + modularity = igraph::modularity(x = g, membership = mem), + stringsAsFactors = FALSE) + if (isTRUE(saveObjects)) { + current_cl[[counter_current]] <- cl + current_mod[counter_current] <- dta_mod[[counter]]$modularity[nrow(dta_mod[[counter]])] + counter_current <- counter_current + 1 + } + counter <- counter + 1 + }, silent = TRUE) + } + + # Hierarchical clustering with single linkage with optimal k + if (isTRUE(single) && k < 2) { + try({ + suppressWarnings(cl <- stats::hclust(jac, method = "single")) + opt_k <- lapply(2:k.max, function(x) { + mem <- stats::cutree(cl, k = x) + mod <- igraph::modularity(x = g, membership = mem) + return(list(mem = mem, mod = mod)) + }) + mod <- sapply(opt_k, function(x) x$mod) + kk <- which.max(mod) + mod <- max(mod) + mem <- opt_k[[kk]]$mem + dta_mem[[counter]] <- data.frame(i = rep(i, length(mem)), + method = rep("Hierarchical (Single)", length(mem)), + node = rownames(x), + cluster = mem, + stringsAsFactors = FALSE) + dta_mod[[counter]] <- data.frame(i = i, + method = "Hierarchical (Single)", + k = kk + 1, # add one because the series started with k = 2 + modularity = mod, + stringsAsFactors = FALSE) + if (isTRUE(saveObjects)) { + current_cl[[counter_current]] <- cl + current_mod[counter_current] <- dta_mod[[counter]]$modularity[nrow(dta_mod[[counter]])] + counter_current <- counter_current + 1 + } + counter <- counter + 1 + }, silent = TRUE) + } + + # Hierarchical clustering with average linkage + if (isTRUE(average) && k > 1) { + try({ + suppressWarnings(cl <- stats::hclust(jac, method = "average")) + mem <- stats::cutree(cl, k = k) + dta_mem[[counter]] <- data.frame(i = rep(i, length(mem)), + method = rep("Hierarchical (Average)", length(mem)), + node = rownames(x), + cluster = mem, + stringsAsFactors = FALSE) + dta_mod[[counter]] <- data.frame(i = i, + method = "Hierarchical (Average)", + k = k, + modularity = igraph::modularity(x = g, membership = mem), + stringsAsFactors = FALSE) + if (isTRUE(saveObjects)) { + current_cl[[counter_current]] <- cl + current_mod[counter_current] <- dta_mod[[counter]]$modularity[nrow(dta_mod[[counter]])] + counter_current <- counter_current + 1 + } + counter <- counter + 1 + }, silent = TRUE) + } + + # Hierarchical clustering with average linkage with optimal k + if (isTRUE(average) && k < 2) { + try({ + suppressWarnings(cl <- stats::hclust(jac, method = "average")) + opt_k <- lapply(2:k.max, function(x) { + mem <- stats::cutree(cl, k = x) + mod <- igraph::modularity(x = g, membership = mem) + return(list(mem = mem, mod = mod)) + }) + mod <- sapply(opt_k, function(x) x$mod) + kk <- which.max(mod) + mod <- max(mod) + mem <- opt_k[[kk]]$mem + dta_mem[[counter]] <- data.frame(i = rep(i, length(mem)), + method = rep("Hierarchical (Average)", length(mem)), + node = rownames(x), + cluster = mem, + stringsAsFactors = FALSE) + dta_mod[[counter]] <- data.frame(i = i, + method = "Hierarchical (Average)", + k = kk + 1, # add one because the series started with k = 2 + modularity = mod, + stringsAsFactors = FALSE) + if (isTRUE(saveObjects)) { + current_cl[[counter_current]] <- cl + current_mod[counter_current] <- dta_mod[[counter]]$modularity[nrow(dta_mod[[counter]])] + counter_current <- counter_current + 1 + } + counter <- counter + 1 + }, silent = TRUE) + } + + # Hierarchical clustering with complete linkage + if (isTRUE(complete) && k > 1) { + try({ + suppressWarnings(cl <- stats::hclust(jac, method = "complete")) + mem <- stats::cutree(cl, k = k) + dta_mem[[counter]] <- data.frame(i = rep(i, length(mem)), + method = rep("Hierarchical (Complete)", length(mem)), + node = rownames(x), + cluster = mem, + stringsAsFactors = FALSE) + dta_mod[[counter]] <- data.frame(i = i, + method = "Hierarchical (Complete)", + k = k, + modularity = igraph::modularity(x = g, membership = mem), + stringsAsFactors = FALSE) + if (isTRUE(saveObjects)) { + current_cl[[counter_current]] <- cl + current_mod[counter_current] <- dta_mod[[counter]]$modularity[nrow(dta_mod[[counter]])] + counter_current <- counter_current + 1 + } + counter <- counter + 1 + }, silent = TRUE) + } + + # Hierarchical clustering with complete linkage with optimal k + if (isTRUE(complete) && k < 2) { + try({ + suppressWarnings(cl <- stats::hclust(jac, method = "complete")) + opt_k <- lapply(2:k.max, function(x) { + mem <- stats::cutree(cl, k = x) + mod <- igraph::modularity(x = g, membership = mem) + return(list(mem = mem, mod = mod)) + }) + mod <- sapply(opt_k, function(x) x$mod) + kk <- which.max(mod) + mod <- max(mod) + mem <- opt_k[[kk]]$mem + dta_mem[[counter]] <- data.frame(i = rep(i, length(mem)), + method = rep("Hierarchical (Complete)", length(mem)), + node = rownames(x), + cluster = mem, + stringsAsFactors = FALSE) + dta_mod[[counter]] <- data.frame(i = i, + method = "Hierarchical (Complete)", + k = kk + 1, # add one because the series started with k = 2 + modularity = mod, + stringsAsFactors = FALSE) + if (isTRUE(saveObjects)) { + current_cl[[counter_current]] <- cl + current_mod[counter_current] <- dta_mod[[counter]]$modularity[nrow(dta_mod[[counter]])] + counter_current <- counter_current + 1 + } + counter <- counter + 1 + }, silent = TRUE) + } + + # Hierarchical clustering with the Ward algorithm + if (isTRUE(ward) && k > 1) { + try({ + suppressWarnings(cl <- stats::hclust(jac, method = "ward.D2")) + mem <- stats::cutree(cl, k = k) + dta_mem[[counter]] <- data.frame(i = rep(i, length(mem)), + method = rep("Hierarchical (Ward)", length(mem)), + node = rownames(x), + cluster = mem, + stringsAsFactors = FALSE) + dta_mod[[counter]] <- data.frame(i = i, + method = "Hierarchical (Ward)", + k = k, + modularity = igraph::modularity(x = g, membership = mem), + stringsAsFactors = FALSE) + if (isTRUE(saveObjects)) { + current_cl[[counter_current]] <- cl + current_mod[counter_current] <- dta_mod[[counter]]$modularity[nrow(dta_mod[[counter]])] + counter_current <- counter_current + 1 + } + counter <- counter + 1 + }, silent = TRUE) + } + + # Hierarchical clustering with the Ward algorithm with optimal k + if (isTRUE(ward) && k < 2) { + try({ + suppressWarnings(cl <- stats::hclust(jac, method = "ward.D2")) + opt_k <- lapply(2:k.max, function(x) { + mem <- stats::cutree(cl, k = x) + mod <- igraph::modularity(x = g, membership = mem) + return(list(mem = mem, mod = mod)) + }) + mod <- sapply(opt_k, function(x) x$mod) + kk <- which.max(mod) + mod <- max(mod) + mem <- opt_k[[kk]]$mem + dta_mem[[counter]] <- data.frame(i = rep(i, length(mem)), + method = rep("Hierarchical (Ward)", length(mem)), + node = rownames(x), + cluster = mem, + stringsAsFactors = FALSE) + dta_mod[[counter]] <- data.frame(i = i, + method = "Hierarchical (Ward)", + k = kk + 1, # add one because the series started with k = 2 + modularity = mod, + stringsAsFactors = FALSE) + if (isTRUE(saveObjects)) { + current_cl[[counter_current]] <- cl + current_mod[counter_current] <- dta_mod[[counter]]$modularity[nrow(dta_mod[[counter]])] + counter_current <- counter_current + 1 + } + counter <- counter + 1 + }, silent = TRUE) + } + + # k-means + if (isTRUE(kmeans) && k > 1) { + try({ + suppressWarnings(cl <- stats::kmeans(jac, centers = k)) + mem <- cl$cluster + dta_mem[[counter]] <- data.frame(i = rep(i, length(mem)), + method = rep("k-Means", length(mem)), + node = rownames(x), + cluster = mem, + stringsAsFactors = FALSE) + dta_mod[[counter]] <- data.frame(i = i, + method = "k-Means", + k = k, + modularity = igraph::modularity(x = g, membership = mem), + stringsAsFactors = FALSE) + if (isTRUE(saveObjects)) { + current_cl[[counter_current]] <- cl + current_mod[counter_current] <- dta_mod[[counter]]$modularity[nrow(dta_mod[[counter]])] + counter_current <- counter_current + 1 + } + counter <- counter + 1 + }, silent = TRUE) + } + + # k-means with optimal k + if (isTRUE(kmeans) && k < 2) { + try({ + opt_k <- lapply(2:k.max, function(x) { + suppressWarnings(cl <- stats::kmeans(jac, centers = x)) + mem <- cl$cluster + mod <- igraph::modularity(x = g, membership = mem) + return(list(cl = cl, mem = mem, mod = mod)) + }) + mod <- sapply(opt_k, function(x) x$mod) + kk <- which.max(mod) + mod <- max(mod) + mem <- opt_k[[kk]]$mem + dta_mem[[counter]] <- data.frame(i = rep(i, length(mem)), + method = rep("k-Means", length(mem)), + node = rownames(x), + cluster = mem, + stringsAsFactors = FALSE) + dta_mod[[counter]] <- data.frame(i = i, + method = "k-Means", + k = kk + 1, # add one because the series started with k = 2 + modularity = mod, + stringsAsFactors = FALSE) + if (isTRUE(saveObjects)) { + cl <- opt_k[[kk]]$cl + current_cl[[counter_current]] <- cl + current_mod[counter_current] <- dta_mod[[counter]]$modularity[nrow(dta_mod[[counter]])] + counter_current <- counter_current + 1 + } + counter <- counter + 1 + }, silent = TRUE) + } + + # pam + if (isTRUE(pam) && k > 1) { + try({ + suppressWarnings(cl <- cluster::pam(jac, k = k)) + mem <- cl$cluster + dta_mem[[counter]] <- data.frame(i = rep(i, length(mem)), + method = rep("Partitioning around Medoids", length(mem)), + node = rownames(x), + cluster = mem, + stringsAsFactors = FALSE) + dta_mod[[counter]] <- data.frame(i = i, + method = "Partitioning around Medoids", + k = k, + modularity = igraph::modularity(x = g, membership = mem), + stringsAsFactors = FALSE) + if (isTRUE(saveObjects)) { + current_cl[[counter_current]] <- cl + current_mod[counter_current] <- dta_mod[[counter]]$modularity[nrow(dta_mod[[counter]])] + counter_current <- counter_current + 1 + } + counter <- counter + 1 + }, silent = TRUE) + } + + # pam with optimal k + if (isTRUE(pam) && k < 2) { + try({ + opt_k <- lapply(2:k.max, function(x) { + suppressWarnings(cl <- cluster::pam(jac, k = x)) + mem <- cl$cluster + mod <- igraph::modularity(x = g, membership = mem) + return(list(cl = cl, mem = mem, mod = mod)) + }) + mod <- sapply(opt_k, function(x) x$mod) + kk <- which.max(mod) + mod <- max(mod) + mem <- opt_k[[kk]]$mem + dta_mem[[counter]] <- data.frame(i = rep(i, length(mem)), + method = rep("Partitioning around Medoids", length(mem)), + node = rownames(x), + cluster = mem, + stringsAsFactors = FALSE) + dta_mod[[counter]] <- data.frame(i = i, + method = "Partitioning around Medoids", + k = kk + 1, # add one because the series started with k = 2 + modularity = mod, + stringsAsFactors = FALSE) + if (isTRUE(saveObjects)) { + cl <- opt_k[[kk]]$cl + current_cl[[counter_current]] <- cl + current_mod[counter_current] <- dta_mod[[counter]]$modularity[nrow(dta_mod[[counter]])] + counter_current <- counter_current + 1 + } + counter <- counter + 1 + }, silent = TRUE) + } + + # Equivalence clustering + if (isTRUE(equivalence) && k > 1) { + try({ + suppressWarnings(cl <- sna::equiv.clust(y, equiv.dist = sna::sedist(y, method = "euclidean"))) + mem <- stats::cutree(cl$cluster, k = k) + dta_mem[[counter]] <- data.frame(i = rep(i, length(mem)), + method = rep("Equivalence", length(mem)), + node = rownames(x), + cluster = mem, + stringsAsFactors = FALSE) + dta_mod[[counter]] <- data.frame(i = i, + method = "Equivalence", + k = k, + modularity = igraph::modularity(x = g, membership = mem), + stringsAsFactors = FALSE) + if (isTRUE(saveObjects)) { + current_cl[[counter_current]] <- cl + current_mod[counter_current] <- dta_mod[[counter]]$modularity[nrow(dta_mod[[counter]])] + counter_current <- counter_current + 1 + } + counter <- counter + 1 + }, silent = TRUE) + } + + # Equivalence clustering with optimal k + if (isTRUE(equivalence) && k < 2) { + try({ + suppressWarnings(cl <- sna::equiv.clust(y, equiv.dist = sna::sedist(y, method = "euclidean"))) + opt_k <- lapply(2:k.max, function(x) { + mem <- stats::cutree(cl$cluster, k = x) + mod <- igraph::modularity(x = g, membership = mem) + return(list(mem = mem, mod = mod)) + }) + mod <- sapply(opt_k, function(x) x$mod) + kk <- which.max(mod) + mod <- max(mod) + mem <- opt_k[[kk]]$mem + dta_mem[[counter]] <- data.frame(i = rep(i, length(mem)), + method = rep("Equivalence", length(mem)), + node = rownames(x), + cluster = mem, + stringsAsFactors = FALSE) + dta_mod[[counter]] <- data.frame(i = i, + method = "Equivalence", + k = kk + 1, # add one because the series started with k = 2 + modularity = mod, + stringsAsFactors = FALSE) + if (isTRUE(saveObjects)) { + current_cl[[counter_current]] <- cl + current_mod[counter_current] <- dta_mod[[counter]]$modularity[nrow(dta_mod[[counter]])] + counter_current <- counter_current + 1 + } + counter <- counter + 1 + }, silent = TRUE) + } + + # CONCOR based on the positive subtract network + if (isTRUE(concor_one) && k %in% c(0, 2)) { + try({ + suppressWarnings(mi <- stats::cor(y)) + iter <- 1 + while (any(abs(mi) <= 0.999) & iter <= 50) { + mi[is.na(mi)] <- 0 + mi <- stats::cor(mi) + iter <- iter + 1 + } + mem <- ((mi[, 1] > 0) * 1) + 1 + dta_mem[[counter]] <- data.frame(i = rep(i, length(mem)), + method = rep("CONCOR (One-Mode)", length(mem)), + node = rownames(x), + cluster = mem, + stringsAsFactors = FALSE) + dta_mod[[counter]] <- data.frame(i = i, + method = "CONCOR (One-Mode)", + k = 2, + modularity = igraph::modularity(x = g, membership = mem), + stringsAsFactors = FALSE) + if (isTRUE(saveObjects)) { + current_cl[[counter_current]] <- mem + current_mod[counter_current] <- dta_mod[[counter]]$modularity[nrow(dta_mod[[counter]])] + counter_current <- counter_current + 1 + } + counter <- counter + 1 + }, silent = TRUE) + } + + # CONCOR based on the combined affiliation network + if (isTRUE(concor_two) && k %in% c(0, 2)) { + try({ + suppressWarnings(mi <- stats::cor(t(combined))) + iter <- 1 + while (any(abs(mi) <= 0.999) & iter <= 50) { + mi[is.na(mi)] <- 0 + mi <- stats::cor(mi) + iter <- iter + 1 + } + mem <- ((mi[, 1] > 0) * 1) + 1 + dta_mem[[counter]] <- data.frame(i = rep(i, length(mem)), + method = rep("CONCOR (Two-Mode)", length(mem)), + node = rownames(x), + cluster = mem, + stringsAsFactors = FALSE) + dta_mod[[counter]] <- data.frame(i = i, + method = "CONCOR (Two-Mode)", + k = 2, + modularity = igraph::modularity(x = g, membership = mem), + stringsAsFactors = FALSE) + if (isTRUE(saveObjects)) { + current_cl[[counter_current]] <- mem + current_mod[counter_current] <- dta_mod[[counter]]$modularity[nrow(dta_mod[[counter]])] + counter_current <- counter_current + 1 + } + counter <- counter + 1 + }, silent = TRUE) + } + + # Louvain clustering + if (isTRUE(louvain) && k < 2) { + try({ + suppressWarnings(cl <- igraph::cluster_louvain(g)) + mem <- igraph::membership(cl) + dta_mem[[counter]] <- data.frame(i = rep(i, length(mem)), + method = rep("Louvain", length(mem)), + node = rownames(x), + cluster = as.numeric(mem), + stringsAsFactors = FALSE) + dta_mod[[counter]] <- data.frame(i = i, + method = "Louvain", + k = max(as.numeric(mem)), + modularity = igraph::modularity(x = g, membership = mem), + stringsAsFactors = FALSE) + if (isTRUE(saveObjects)) { + current_cl[[counter_current]] <- cl + current_mod[counter_current] <- dta_mod[[counter]]$modularity[nrow(dta_mod[[counter]])] + counter_current <- counter_current + 1 + } + counter <- counter + 1 + }, silent = TRUE) + } + + # Fast & Greedy community detection (with or without cut) + if (isTRUE(fastgreedy)) { + try({ + suppressWarnings(cl <- igraph::cluster_fast_greedy(g, merges = TRUE)) + if (k == 0) { + mem <- igraph::membership(cl) + } else { + mem <- suppressWarnings(igraph::cut_at(cl, no = k)) + if ((k + 1) %in% as.numeric(mem)) { + stop() + } + } + dta_mem[[counter]] <- data.frame(i = rep(i, length(mem)), + method = rep("Fast & Greedy", length(mem)), + node = rownames(x), + cluster = as.numeric(mem), + stringsAsFactors = FALSE) + dta_mod[[counter]] <- data.frame(i = i, + method = "Fast & Greedy", + k = ifelse(k == 0, max(as.numeric(mem)), k), + modularity = igraph::modularity(x = g, membership = mem), + stringsAsFactors = FALSE) + if (isTRUE(saveObjects)) { + current_cl[[counter_current]] <- cl + current_mod[counter_current] <- dta_mod[[counter]]$modularity[nrow(dta_mod[[counter]])] + counter_current <- counter_current + 1 + } + counter <- counter + 1 + }, silent = TRUE) + } + + # Walktrap community detection (with or without cut) + if (isTRUE(walktrap)) { + try({ + suppressWarnings(cl <- igraph::cluster_walktrap(g, merges = TRUE)) + if (k == 0) { + mem <- igraph::membership(cl) + } else { + mem <- suppressWarnings(igraph::cut_at(cl, no = k)) + if ((k + 1) %in% as.numeric(mem)) { + stop() + } + } + dta_mem[[counter]] <- data.frame(i = rep(i, length(mem)), + method = rep("Walktrap", length(mem)), + node = rownames(x), + cluster = as.numeric(mem), + stringsAsFactors = FALSE) + dta_mod[[counter]] <- data.frame(i = i, + method = "Walktrap", + k = ifelse(k == 0, max(as.numeric(mem)), k), + modularity = igraph::modularity(x = g, membership = mem), + stringsAsFactors = FALSE) + if (isTRUE(saveObjects)) { + current_cl[[counter_current]] <- cl + current_mod[counter_current] <- dta_mod[[counter]]$modularity[nrow(dta_mod[[counter]])] + counter_current <- counter_current + 1 + } + counter <- counter + 1 + }, silent = TRUE) + } + + # Leading Eigenvector community detection (only without cut) + if (isTRUE(leading_eigen) && k < 2) { # it *should* work with cut_at because is.hierarchical(cl) returns TRUE, but it never works... + try({ + suppressWarnings(cl <- igraph::cluster_leading_eigen(g)) + mem <- igraph::membership(cl) + dta_mem[[counter]] <- data.frame(i = rep(i, length(mem)), + method = rep("Leading Eigenvector", length(mem)), + node = rownames(x), + cluster = as.numeric(mem), + stringsAsFactors = FALSE) + dta_mod[[counter]] <- data.frame(i = i, + method = "Leading Eigenvector", + k = max(as.numeric(mem)), + modularity = igraph::modularity(x = g, membership = mem), + stringsAsFactors = FALSE) + if (isTRUE(saveObjects)) { + current_cl[[counter_current]] <- cl + current_mod[counter_current] <- dta_mod[[counter]]$modularity[nrow(dta_mod[[counter]])] + counter_current <- counter_current + 1 + } + counter <- counter + 1 + }, silent = TRUE) + } + + # Edge Betweenness community detection (with or without cut) + if (isTRUE(edge_betweenness)) { + try({ + suppressWarnings(cl <- igraph::cluster_edge_betweenness(g, merges = TRUE)) + if (k == 0) { + mem <- igraph::membership(cl) + } else { + mem <- suppressWarnings(igraph::cut_at(cl, no = k)) + if ((k + 1) %in% as.numeric(mem)) { + stop() + } + } + dta_mem[[counter]] <- data.frame(i = rep(i, length(mem)), + method = rep("Edge Betweenness", length(mem)), + node = rownames(x), + cluster = as.numeric(mem), + stringsAsFactors = FALSE) + dta_mod[[counter]] <- data.frame(i = i, + method = "Edge Betweenness", + k = ifelse(k == 0, max(as.numeric(mem)), k), + modularity = igraph::modularity(x = g, membership = mem), + stringsAsFactors = FALSE) + if (isTRUE(saveObjects)) { + current_cl[[counter_current]] <- cl + current_mod[counter_current] <- dta_mod[[counter]]$modularity[nrow(dta_mod[[counter]])] + counter_current <- counter_current + 1 + } + counter <- counter + 1 + }, silent = TRUE) + } + + # Infomap community detection + if (isTRUE(infomap) && k < 2) { + try({ + suppressWarnings(cl <- igraph::cluster_infomap(g)) + mem <- igraph::membership(cl) + dta_mem[[counter]] <- data.frame(i = rep(i, length(mem)), + method = rep("Infomap", length(mem)), + node = rownames(x), + cluster = as.numeric(mem), + stringsAsFactors = FALSE) + dta_mod[[counter]] <- data.frame(i = i, + method = "Infomap", + k = max(as.numeric(mem)), + modularity = igraph::modularity(x = g, membership = mem), + stringsAsFactors = FALSE) + if (isTRUE(saveObjects)) { + current_cl[[counter_current]] <- cl + current_mod[counter_current] <- dta_mod[[counter]]$modularity[nrow(dta_mod[[counter]])] + counter_current <- counter_current + 1 + } + counter <- counter + 1 + }, silent = TRUE) + } + + # Label Propagation community detection + if (isTRUE(label_prop) && k < 2) { + try({ + suppressWarnings(cl <- igraph::cluster_label_prop(g)) + mem <- igraph::membership(cl) + dta_mem[[counter]] <- data.frame(i = rep(i, length(mem)), + method = rep("Label Propagation", length(mem)), + node = rownames(x), + cluster = as.numeric(mem), + stringsAsFactors = FALSE) + dta_mod[[counter]] <- data.frame(i = i, + method = "Label Propagation", + k = max(as.numeric(mem)), + modularity = igraph::modularity(x = g, membership = mem), + stringsAsFactors = FALSE) + if (isTRUE(saveObjects)) { + current_cl[[counter_current]] <- cl + current_mod[counter_current] <- dta_mod[[counter]]$modularity[nrow(dta_mod[[counter]])] + counter_current <- counter_current + 1 + } + counter <- counter + 1 + }, silent = TRUE) + } + + # Spinglass community detection + if (isTRUE(spinglass) && k < 2) { + try({ + suppressWarnings(cl <- igraph::cluster_spinglass(g)) + mem <- igraph::membership(cl) + dta_mem[[counter]] <- data.frame(i = rep(i, length(mem)), + method = rep("Spinglass", length(mem)), + node = rownames(x), + cluster = as.numeric(mem), + stringsAsFactors = FALSE) + dta_mod[[counter]] <- data.frame(i = i, + method = "Spinglass", + k = max(as.numeric(mem)), + modularity = igraph::modularity(x = g, membership = mem), + stringsAsFactors = FALSE) + if (isTRUE(saveObjects)) { + current_cl[[counter_current]] <- cl + current_mod[counter_current] <- dta_mod[[counter]]$modularity[nrow(dta_mod[[counter]])] + counter_current <- counter_current + 1 + } + counter <- counter + 1 + }, silent = TRUE) + } + + # retain cluster object where modularity was maximal + if (isTRUE(saveObjects) && length(current_cl) > 0) { + obj$cl[[i]] <- current_cl[[which.max(current_mod)]] + } + } + } + obj$cl <- obj$cl[!sapply(obj$cl, is.null)] # remove NULL objects that may occur when the network is empty + obj$k <- k + obj$max_mod <- do.call(rbind, dta_dat) + memberships <- do.call(rbind, dta_mem) + rownames(memberships) <- NULL + obj$memberships <- memberships + obj$modularity <- do.call(rbind, dta_mod) + if (nrow(obj$modularity) == 0) { + stop("No output rows. Either you switched all clustering methods off, or all methods you used produced errors.") + } + obj$max_mod <- obj$max_mod[obj$max_mod$i %in% obj$modularity$i, ] # remove date entries where the network is empty + obj$max_mod$max_mod <- sapply(obj$max_mod$i, function(x) max(obj$modularity$modularity[obj$modularity$i == x], na.rm = TRUE)) # attach max_mod to $max_mod + # attach max_method to $max_mod + obj$max_mod$max_method <- sapply(obj$max_mod$i, + function(x) obj$modularity$method[obj$modularity$i == x & obj$modularity$modularity == max(obj$modularity$modularity[obj$modularity$i == x], na.rm = TRUE)][1]) + # attach k to max_mod + obj$max_mod$k <- sapply(obj$max_mod$i, function(x) max(obj$modularity$k[obj$modularity$i == x], na.rm = TRUE)) + + # diagnostics + if (isTRUE(single) && !"Hierarchical (Single)" %in% obj$modularity$method && k > 1) { + warning("'single' omitted due to an unknown problem.") + } + if (isTRUE(average) && !"Hierarchical (Average)" %in% obj$modularity$method && k > 1) { + warning("'average' omitted due to an unknown problem.") + } + if (isTRUE(complete) && !"Hierarchical (Complete)" %in% obj$modularity$method && k > 1) { + warning("'complete' omitted due to an unknown problem.") + } + if (isTRUE(ward) && !"Hierarchical (Ward)" %in% obj$modularity$method) { + warning("'ward' omitted due to an unknown problem.") + } + if (isTRUE(kmeans) && !"k-Means" %in% obj$modularity$method) { + warning("'kmeans' omitted due to an unknown problem.") + } + if (isTRUE(pam) && !"Partitioning around Medoids" %in% obj$modularity$method) { + warning("'pam' omitted due to an unknown problem.") + } + if (isTRUE(equivalence) && !"Equivalence" %in% obj$modularity$method) { + warning("'equivalence' omitted due to an unknown problem.") + } + if (isTRUE(concor_one) && !"CONCOR (One-Mode)" %in% obj$modularity$method && k %in% c(0, 2)) { + warning("'concor_one' omitted due to an unknown problem.") + } + if (isTRUE(concor_two) && !"CONCOR (Two-Mode)" %in% obj$modularity$method && k %in% c(0, 2)) { + warning("'concor_two' omitted due to an unknown problem.") + } + if (isTRUE(louvain) && !"Louvain" %in% obj$modularity$method && k < 2) { + warning("'louvain' omitted due to an unknown problem.") + } + if (isTRUE(fastgreedy) && !"Fast & Greedy" %in% obj$modularity$method) { + warning("'fastgreedy' omitted due to an unknown problem.") + } + if (isTRUE(walktrap) && !"Walktrap" %in% obj$modularity$method) { + warning("'walktrap' omitted due to an unknown problem.") + } + if (isTRUE(leading_eigen) && !"Leading Eigenvector" %in% obj$modularity$method && k < 2) { + warning("'leading_eigen' omitted due to an unknown problem.") + } + if (isTRUE(edge_betweenness) && !"Edge Betweenness" %in% obj$modularity$method) { + warning("'edge_betweenness' omitted due to an unknown problem.") + } + if (isTRUE(infomap) && !"Infomap" %in% obj$modularity$method && k < 2) { + warning("'infomap' omitted due to an unknown problem.") + } + if (isTRUE(label_prop) && !"Label Propagation" %in% obj$modularity$method && k < 2) { + warning("'label_prop' omitted due to an unknown problem.") + } + if (isTRUE(spinglass) && !"Spinglass" %in% obj$modularity$method && k < 2) { + warning("'spinglass' omitted due to an unknown problem.") + } + + class(obj) <- "dna_multiclust" + return(obj) +} + +#' Print the summary of a \code{dna_multiclust} object +#' +#' Show details of a \code{dna_multiclust} object. +#' +#' Print abbreviated contents for the slots of a \code{dna_multiclust} object, +#' which can be created using the \link{dna_multiclust} function. +#' +#' @param x A \code{dna_multiclust} object. +#' @param ... Further options (currently not used). +#' +#' @author Philip Leifeld +#' +#' @rdname dna_multiclust +#' @importFrom utils head +#' @export +print.dna_multiclust <- function(x, ...) { + cat(paste0("$k\n", x$k, "\n")) + if ("cl" %in% names(x)) { + cat(paste0("\n$cl\n", length(x$cl), " cluster object(s) embedded.\n")) + } + cat("\n$max_mod\n") + print(utils::head(x$max_mod)) + if (nrow(x$max_mod) > 6) { + cat(paste0("[... ", nrow(x$max_mod), " rows]\n")) + } + cat("\n$modularity\n") + print(utils::head(x$modularity)) + if (nrow(x$modularity) > 6) { + cat(paste0("[... ", nrow(x$modularity), " rows]\n")) + } + cat("\n$memberships\n") + print(utils::head(x$memberships)) + if (nrow(x$memberships) > 6) { + cat(paste0("[... ", nrow(x$memberships), " rows]\n")) + } +} + + +# Phase transitions ------------------------------------------------------------ + +#' Detect phase transitions and states in a discourse network +#' +#' Detect phase transitions and states in a discourse network. +#' +#' This function applies the state dynamics methods of Masuda and Holme to a +#' time window discourse network. It computes temporally overlapping discourse +#' networks, computes the dissimilarity between all networks, and clusters them. +#' For the dissimilarity, the sum of absolute edge weight differences and the +#' Euclidean spectral distance are available. Several clustering techniques can +#' be applied to identify the different stages and phases from the resulting +#' distance matrix. +#' +#' @param distanceMethod The distance measure that expresses the dissimilarity +#' between any two network matrices. The following choices are available: +#' \itemize{ +#' \item \code{"absdiff"}: The sum of the cell-wise absolute differences +#' between the two matrices, i.e., the sum of differences in edge weights. +#' This is equivalent to the graph edit distance because the network +#' dimensions are kept constant across all networks by including all nodes +#' at all time points (i.e., by including isolates). +#' \item \code{"spectral"}: The Euclidean distance between the normalized +#' eigenvalues of the graph Laplacian matrices, also called the spectral +#' distance between two network matrices. Any negative values (e.g., from +#' the subtract method) are replaced by zero before computing the +#' distance. +#' \item \code{"modularity"}: The difference in maximal modularity as +#' obtained through an approximation via community detection. Note that +#' this method has not been implemented. +#' } +#' @param normalizeNetwork Divide all cells by their sum before computing +#' the dissimilarity between two network matrices? This normalization scales +#' all edge weights to a sum of \code{1.0}. +#' @param clusterMethods The clustering techniques that are applied to the +#' distance matrix in the end. Hierarchical methods are repeatedly cut off at +#' different levels, and solutions are compared using network modularity to +#' pick the best-fitting cluster membership vector. Some of the methods are +#' slower than others, hence they are not included by default. It is possible +#' to include any number of methods in the argument. For each included method, +#' the cluster membership vector (i.e., the states over time) along with the +#' associated time stamps of the networks are returned, and the modularity of +#' each included method is computed for comparison. The following methods are +#' available: +#' \itemize{ +#' \item \code{"single"}: Hierarchical clustering with single linkage using +#' the \code{\link[stats]{hclust}} function from the \pkg{stats} package. +#' \item \code{"average"}: Hierarchical clustering with average linkage +#' using the \code{\link[stats]{hclust}} function from the \pkg{stats} +#' package. +#' \item \code{"complete"}: Hierarchical clustering with complete linkage +#' using the \code{\link[stats]{hclust}} function from the \pkg{stats} +#' package. +#' \item \code{"ward"}: Hierarchical clustering with Ward's method (D2) +#' using the \code{\link[stats]{hclust}} function from the \pkg{stats} +#' package. +#' \item \code{"kmeans"}: k-means clustering using the +#' \code{\link[stats]{kmeans}} function from the \pkg{stats} package. +#' \item \code{"pam"}: Partitioning around medoids using the +#' \code{\link[cluster]{pam}} function from the \pkg{cluster} package. +#' \item \code{"spectral"}: Spectral clustering. An affinity matrix using a +#' Gaussian (RBF) kernel is created. The Laplacian matrix of the affinity +#' matrix is computed and normalized. The first first k eigenvectors of +#' the normalized Laplacian matrix are clustered using k-means. +#' \item \code{"concor"}: CONvergence of iterative CORrelations (CONCOR) +#' with exactly \code{k = 2} clusters. (Not included by default because of +#' the limit to \code{k = 2}.) +#' \item \code{"fastgreedy"}: Fast & greedy community detection using the +#' \code{\link[igraph]{cluster_fast_greedy}} function in the \pkg{igraph} +#' package. +#' \item \code{"walktrap"}: Walktrap community detection using the +#' \code{\link[igraph]{cluster_walktrap}} function in the \pkg{igraph} +#' package. +#' \item \code{"leading_eigen"}: Leading eigenvector community detection +#' using the \code{\link[igraph]{cluster_leading_eigen}} function in the +#' \pkg{igraph} package. (Can be slow, hence not included by default.) +#' \item \code{"edge_betweenness"}: Girvan-Newman edge betweenness community +#' detection using the \code{\link[igraph]{cluster_edge_betweenness}} +#' function in the \pkg{igraph} package. (Can be slow, hence not included +#' by default.) +#' } +#' @param k.min For the hierarchical cluster methods, how many clusters or +#' states should at least be identified? Only the best solution between +#' \code{k.min} and \code{k.max} clusters is retained and compared to other +#' methods. +#' @param k.max For the hierarchical cluster methods, up to how many clusters or +#' states should be identified? Only the best solution between \code{k.min} +#' and \code{k.max} clusters is retained and compared to other methods. +#' @param splitNodes Split the nodes of a one-mode network (e.g., concepts in a +#' concept x concept congruence network) into multiple separate nodes, one for +#' each qualifier level? For example, if the qualifier variable is the boolean +#' \code{"agreement"} variable with values \code{0} or \code{1}, then each +#' concept is included twice, one time with suffix \code{"- 0"} and one time +#' with suffix \code{"- 1"}. This is useful when the nodes do not possess +#' agency and positive and negative levels of the variable should not be +#' connected through congruence ties. It helps preserve the signed nature of +#' the data in this case. Do not use with actor networks! +#' @param cores The number of computing cores for parallel processing. If +#' \code{1} (the default), no parallel processing is used. If a larger number, +#' the \pkg{pbmcapply} package is used to parallelize the computation of the +#' distance matrix and the clustering. Note that this method is based on +#' forking and is only available on Unix operating systems, including MacOS +#' and Linux. +#' @inheritParams dna_network +#' +#' @examples +#' \dontrun{ +#' dna_init() +#' dna_sample() +#' dna_openDatabase("sample.dna", coderId = 1, coderPassword = "sample") +#' +#' # compute states and phases for sample dataset +#' results <- dna_phaseTransitions(distanceMethod = "absdiff", +#' clusterMethods = c("ward", +#' "pam", +#' "concor", +#' "walktrap"), +#' cores = 1, +#' k.min = 2, +#' k.max = 6, +#' networkType = "onemode", +#' variable1 = "organization", +#' variable2 = "concept", +#' timeWindow = "events", +#' windowSize = 15) +#' results +#' } +#' +#' @author Philip Leifeld, Kristijan Garic +#' +#' @rdname dna_phaseTransitions +#' @importFrom stats dist +#' @importFrom utils combn +#' @export +dna_phaseTransitions <- function(distanceMethod = "absdiff", + normalizeNetwork = FALSE, + clusterMethods = c("single", + "average", + "complete", + "ward", + "kmeans", + "pam", + "spectral", + "fastgreedy", + "walktrap"), + k.min = 2, + k.max = 6, + splitNodes = FALSE, + cores = 1, + networkType = "twomode", + statementType = "DNA Statement", + variable1 = "organization", + variable1Document = FALSE, + variable2 = "concept", + variable2Document = FALSE, + qualifier = "agreement", + qualifierDocument = FALSE, + qualifierAggregation = "subtract", + normalization = "no", + duplicates = "document", + start.date = "01.01.1900", + stop.date = "31.12.2099", + start.time = "00:00:00", + stop.time = "23:59:59", + timeWindow = "days", + windowSize = 150, + excludeValues = list(), + excludeAuthors = character(), + excludeSources = character(), + excludeSections = character(), + excludeTypes = character(), + invertValues = FALSE, + invertAuthors = FALSE, + invertSources = FALSE, + invertSections = FALSE, + invertTypes = FALSE) { + + # check arguments and packages + if (distanceMethod == "spectral" && networkType == "twomode") { + distanceMethod <- "absdiff" + warning("Spectral distances only work with one-mode networks. Using 'distanceMethod = \"absdiff\"' instead.") + } + if (cores > 1 && !requireNamespace("pbmcapply", quietly = TRUE)) { + pbmclapply <- FALSE + warning("Argument 'cores' requires the 'pbmcapply' package, which is not installed.\nSetting 'cores = 1'. Consider installing the 'pbmcapply' package if you use Linux or MacOS.") + } + igraphMethods <- c("louvain", "fastgreedy", "walktrap", "leading_eigen", "edge_betweenness", "infomap", "label_prop", "spinglass") + if (any(igraphMethods %in% clusterMethods) && !requireNamespace("igraph", quietly = TRUE)) { + clusterMethods <- clusterMethods[-igraphMethods] + warning("'igraph' package not installed. Dropping clustering methods from the 'igraph' package. Consider installing 'igraph'.") + } + if ("pam" %in% clusterMethods && !requireNamespace("cluster", quietly = TRUE)) { + clusterMethods <- clusterMethods[which(clusterMethods != "pam")] + warning("'cluster' package not installed. Dropping clustering methods from the 'cluster' package. Consider installing 'cluster'.") + } + if ("concor" %in% clusterMethods && k.min > 2) { + clusterMethods <- clusterMethods[which(clusterMethods != "concor")] + warning("Dropping 'concor' from clustering methods because the CONCOR implementation in rDNA can only find exactly two clusters, but the 'k.min' argument was larger than 2.") + } + clusterMethods <- rev(clusterMethods) # reverse order to save time during parallel computation by starting the computationally intensive methods first + mcall <- match.call() # save the arguments for storing them in the results later + + # generate the time window networks + if (is.null(timeWindow) || is.na(timeWindow) || !is.character(timeWindow) || length(timeWindow) != 1 || !timeWindow %in% c("events", "seconds", "minutes", "hours", "days", "weeks", "months", "years")) { + timeWindow <- "events" + warning("The 'timeWindow' argument was invalid. Proceeding with 'timeWindow = \"events\" instead.") + } + if (qualifierAggregation == "split") { # splitting first-mode nodes by qualifier + if (networkType != "onemode") { + stop("The 'qualifierAggregation' argument accepts the value \"split\" only in conjunction with creating one-mode networks.") + } + v <- dna_getVariables(statementType) + if (v$type[v$label == qualifier] != "boolean") { + stop("The 'qualifierAggregation = \"split\" argument currently only works with boolean qualifier variables.") + } + aff <- dna_network(networkType = "twomode", + statementType = statementType, + variable1 = variable1, + variable1Document = variable1Document, + variable2 = variable2, + variable2Document = variable2Document, + qualifier = qualifier, + qualifierDocument = qualifierDocument, + qualifierAggregation = "combine", + normalization = "no", + isolates = TRUE, + duplicates = duplicates, + start.date = start.date, + stop.date = stop.date, + start.time = start.time, + stop.time = stop.time, + timeWindow = timeWindow, + windowSize = windowSize, + excludeValues = excludeValues, + excludeAuthors = excludeAuthors, + excludeSources = excludeSources, + excludeSections = excludeSections, + excludeTypes = excludeTypes, + invertValues = invertValues, + invertAuthors = invertAuthors, + invertSources = invertSources, + invertSections = invertSections, + invertTypes = invertTypes, + fileFormat = NULL, + outfile = NULL) + cat("Splitting nodes... ") + nw <- lapply(aff, function(x) { + pos <- x + pos[pos == 2] <- 0 + rownames(pos) <- paste(rownames(pos), "- 1") + neg <- x + neg[neg == 1] <- 0 + rownames(neg) <- paste(rownames(neg), "- 0") + combined <- rbind(pos, neg) + congruence <- combined %*% t(combined) + diag(congruence) <- 0 + rs <- x + rs[rs != 0] <- 1 + rs <- rowSums(rs) + if (normalization == "average") { + denominator <- matrix(1, nrow = nrow(congruence), ncol = ncol(congruence)) + for (i in 1:nrow(x)) { + for (j in 1:ncol(x)) { + d <- (rs[i] + rs[j]) / 2 + if (d != 0) denominator[i, j] <- d + } + } + result <- congruence / denominator + } else if (normalization == "jaccard") { + unions <- matrix(rep(rs, times = length(rs)), nrow = length(rs)) + t(matrix(rep(rs, times = length(rs)), nrow = length(rs))) - congruence + result <- congruence / unions + diag(result) <- 0 + } else if (normalization == "cosine") { + magnitudes <- sqrt(rs) + result <- congruence / (outer(magnitudes, magnitudes)) + diag(result) <- 1 + } else if (normalization == "no") { + result <- congruence + diag(result) <- 0 + } else { + normalization <- "no" + result <- congruence + diag(result) <- 0 + warning("Invalid normalization setting for one-mode networks. Switching off normalization.") + } + class(result) <- c("dna_network_onemode", class(result)) + attributes(result)$start <- attributes(x)$start + attributes(result)$stop <- attributes(x)$stop + attributes(result)$middle <- attributes(x)$middle + attributes(result)$numStatements <- attributes(x)$numStatements + attributes(result)$call <- mcall + return(result) + }) + cat(intToUtf8(0x2714), "\n") + } else { # letting dna_network create the networks + nw <- dna_network(networkType = networkType, + statementType = statementType, + variable1 = variable1, + variable1Document = variable1Document, + variable2 = variable2, + variable2Document = variable2Document, + qualifier = qualifier, + qualifierDocument = qualifierDocument, + qualifierAggregation = qualifierAggregation, + normalization = normalization, + isolates = TRUE, + duplicates = duplicates, + start.date = start.date, + stop.date = stop.date, + start.time = start.time, + stop.time = stop.time, + timeWindow = timeWindow, + windowSize = windowSize, + excludeValues = excludeValues, + excludeAuthors = excludeAuthors, + excludeSources = excludeSources, + excludeSections = excludeSections, + excludeTypes = excludeTypes, + invertValues = invertValues, + invertAuthors = invertAuthors, + invertSources = invertSources, + invertSections = invertSections, + invertTypes = invertTypes, + fileFormat = NULL, + outfile = NULL) + } + + # normalize network matrices to sum to 1.0 + if (normalizeNetwork) { + cat("Normalizing network to sum to 1... ") + nw <- lapply(nw, function(x) { + s <- sum(x) + ifelse(s == 0, return(x), return(x / s)) + }) + cat(intToUtf8(0x2714), "\n") + } + + # define distance function for network comparison + if (distanceMethod == "absdiff") { + d <- function(pair, data) { + sum(abs(data[[pair[1]]] - data[[pair[2]]])) + } + } else if (distanceMethod == "spectral") { + d <- function(pair, data) { + data[[pair[1]]][data[[pair[1]]] < 0] <- 0 # replace negative values by zero + data[[pair[2]]][data[[pair[2]]] < 0] <- 0 # for example, in a subtract network + eigen_x <- eigen(diag(rowSums(data[[pair[1]]])) - data[[pair[1]]]) # eigenvalues for each Laplacian matrix + eigen_y <- eigen(diag(rowSums(data[[pair[2]]])) - data[[pair[2]]]) + eigen_x <- eigen_x$values / sum(eigen_x$values) # normalize to sum to 1.0 + eigen_y <- eigen_y$values / sum(eigen_y$values) + return(sqrt(sum((eigen_x - eigen_y)^2))) # return square root of the sum of squared differences (Euclidean distance) between the normalized eigenvalues + } + } else if (distanceMethod == "modularity") { + stop("Differences in modularity have not been implemented yet. Please use absolute differences or spectral Euclidean distance as a distance method.") + } else { + stop("Distance method not recognized. Try \"absdiff\" or \"spectral\".") + } + + # apply distance function and create distance matrix + distance_mat <- matrix(0, nrow = length(nw), ncol = length(nw)) + pairs <- combn(length(nw), 2, simplify = FALSE) + if (cores > 1) { + cat(paste("Computing distances on", cores, "cores.\n")) + a <- Sys.time() + distances <- pbmcapply::pbmclapply(pairs, d, data = nw, mc.cores = cores) + b <- Sys.time() + } else { + cat("Computing distances... ") + a <- Sys.time() + distances <- lapply(pairs, d, data = nw) + b <- Sys.time() + cat(intToUtf8(0x2714), "\n") + } + distance_mat[lower.tri(distance_mat)] <- unlist(distances) + distance_mat <- distance_mat + t(distance_mat) + distance_mat[is.nan(distance_mat)] <- 0 # replace NaN values with zeros + # distance_mat <- distance_mat + 1e-12 # adding small constant + distance_mat <- distance_mat / max(distance_mat) # rescale between 0 and 1 + print(b - a) + + # define clustering function + hclustMethods <- c("single", "average", "complete", "ward") + cl <- function(method, distmat) { + tryCatch({ + similarity_mat <- 1 - distmat + g <- igraph::graph.adjacency(similarity_mat, mode = "undirected", weighted = TRUE, diag = FALSE) # graph needs to be based on similarity, not distance + if (method %in% hclustMethods) { + if (method == "single") { + suppressWarnings(cl <- stats::hclust(as.dist(distmat), method = "single")) + } else if (method == "average") { + suppressWarnings(cl <- stats::hclust(as.dist(distmat), method = "average")) + } else if (method == "complete") { + suppressWarnings(cl <- stats::hclust(as.dist(distmat), method = "complete")) + } else if (method == "ward") { + suppressWarnings(cl <- stats::hclust(as.dist(distmat), method = "ward.D2")) + } + opt_k <- lapply(k.min:k.max, function(x) { + mem <- stats::cutree(cl, k = x) + mod <- igraph::modularity(x = g, weights = igraph::E(g)$weight, membership = mem) + return(list(mem = mem, mod = mod)) + }) + mod <- sapply(opt_k, function(x) x$mod) + kk <- which.max(mod) + mem <- opt_k[[kk]]$mem + } else if (method == "kmeans") { + opt_k <- lapply(k.min:k.max, function(x) { + suppressWarnings(cl <- stats::kmeans(distmat, centers = x)) + mem <- cl$cluster + mod <- igraph::modularity(x = g, weights = igraph::E(g)$weight, membership = mem) + return(list(cl = cl, mem = mem, mod = mod)) + }) + mod <- sapply(opt_k, function(x) x$mod) + kk <- which.max(mod) + mem <- opt_k[[kk]]$mem + } else if (method == "pam") { + opt_k <- lapply(k.min:k.max, function(x) { + suppressWarnings(cl <- cluster::pam(distmat, k = x)) + mem <- cl$cluster + mod <- igraph::modularity(x = g, weights = igraph::E(g)$weight, membership = mem) + return(list(cl = cl, mem = mem, mod = mod)) + }) + mod <- sapply(opt_k, function(x) x$mod) + kk <- which.max(mod) + mem <- opt_k[[kk]]$mem + } else if (method == "spectral") { + sigma <- 1.0 + affinity_matrix <- exp(-distmat^2 / (2 * sigma^2)) + L <- diag(rowSums(affinity_matrix)) - affinity_matrix + D.sqrt.inv <- diag(1 / sqrt(rowSums(affinity_matrix))) + L.norm <- D.sqrt.inv %*% L %*% D.sqrt.inv + eigenvalues <- eigen(L.norm) # eigenvalue decomposition + opt_k <- lapply(k.min:k.max, function(x) { + U <- eigenvalues$vectors[, 1:x] + mem <- kmeans(U, centers = x)$cluster # cluster the eigenvectors + mod <- igraph::modularity(x = g, weights = igraph::E(g)$weight, membership = mem) + return(list(mem = mem, mod = mod)) + }) + mod <- sapply(opt_k, function(x) x$mod) + kk <- which.max(mod) + mem <- opt_k[[kk]]$mem + } else if (method == "concor") { + suppressWarnings(mi <- stats::cor(similarity_mat)) + iter <- 1 + while (any(abs(mi) <= 0.999) & iter <= 50) { + mi[is.na(mi)] <- 0 + mi <- stats::cor(mi) + iter <- iter + 1 + } + mem <- ((mi[, 1] > 0) * 1) + 1 + } else if (method %in% igraphMethods) { + if (method == "fastgreedy") { + suppressWarnings(cl <- igraph::cluster_fast_greedy(g)) + } else if (method == "walktrap") { + suppressWarnings(cl <- igraph::cluster_walktrap(g)) + } else if (method == "leading_eigen") { + suppressWarnings(cl <- igraph::cluster_leading_eigen(g)) + } else if (method == "edge_betweenness") { + suppressWarnings(cl <- igraph::cluster_edge_betweenness(g)) + } else if (method == "spinglass") { + suppressWarnings(cl <- igraph::cluster_spinglass(g)) + } + opt_k <- lapply(k.min:k.max, function(x) { + mem <- igraph::cut_at(communities = cl, no = x) + mod <- igraph::modularity(x = g, weights = igraph::E(g)$weight, membership = mem) + return(list(mem = mem, mod = mod)) + }) + mod <- sapply(opt_k, function(x) x$mod) + kk <- which.max(mod) + mem <- opt_k[[kk]]$mem + } + list(method = method, + modularity = igraph::modularity(x = g, weights = igraph::E(g)$weight, membership = mem), + memberships = mem) + }, + error = function(e) { + warning("Cluster method '", method, "' could not be computed due to an error: ", e) + }, + warning = function(w) { + warning("Cluster method '", method, "' threw a warning: ", w) + }) + } + + # apply all clustering methods to distance matrix + if (cores > 1) { + cat(paste("Clustering distance matrix on", cores, "cores.\n")) + a <- Sys.time() + l <- pbmcapply::pbmclapply(clusterMethods, cl, distmat = distance_mat, mc.cores = cores) + b <- Sys.time() + } else { + cat("Clustering distance matrix... ") + a <- Sys.time() + l <- lapply(clusterMethods, cl, distmat = distance_mat) + b <- Sys.time() + cat(intToUtf8(0x2714), "\n") + } + print(b - a) + for (i in length(l):1) { + if (length(l[[i]]) == 1) { + l <- l[-i] + clusterMethods <- clusterMethods[-i] + } + } + results <- list() + mod <- sapply(l, function(x) x$modularity) + best <- which(mod == max(mod))[1] + results$modularity <- mod[best] + results$clusterMethod <- clusterMethods[best] + dates <- sapply(nw, function(x) attributes(x)$middle) + + # temporal embedding via MDS + if (!requireNamespace("MASS", quietly = TRUE)) { + mem <- data.frame("date" = as.POSIXct(dates, format = "%d-%m-%Y", tz = "UTC"), + "state" = l[[best]]$memberships) + results$states <- mem + warning("Skipping temporal embedding because the 'MASS' package is not installed. Consider installing it.") + } else { + cat("Temporal embedding...\n") + a <- Sys.time() + distmat <- distance_mat + 1e-12 + mds <- MASS::isoMDS(distmat) # MDS of distance matrix + points <- mds$points + mem <- data.frame("date" = as.POSIXct(dates, format = "%d-%m-%Y", tz = "UTC"), + "state" = l[[best]]$memberships, + "X1" = points[, 1], + "X2" = points[, 2]) + results$states <- mem + b <- Sys.time() + print(b - a) + } + + results$distmat <- distance_mat + class(results) <- "dna_phaseTransitions" + attributes(results)$stress <- ifelse(ncol(results$states) == 2, NA, mds$stress) + attributes(results)$call <- mcall + return(results) +} + +#' Print the summary of a \code{dna_phaseTransitions} object +#' +#' Show details of a \code{dna_phaseTransitions} object. +#' +#' Print a summary of a \code{dna_phaseTransitions} object, which can be created +#' using the \link{dna_phaseTransitions} function. +#' +#' @param x A \code{dna_phaseTransitions} object. +#' @param ... Further options (currently not used). +#' +#' @author Philip Leifeld +#' +#' @rdname dna_phaseTransitions +#' @importFrom utils head +#' @export +print.dna_phaseTransitions <- function(x, ...) { + cat(paste0("States: ", max(x$states$state), ". Cluster method: ", x$clusterMethod, ". Modularity: ", round(x$modularity, 3), ".\n\n")) + print(utils::head(x$states, 20)) + cat(paste0("...", nrow(x$states), " further rows\n")) +} + +#' @rdname dna_phaseTransitions +#' @param object A \code{"dna_phaseTransitions"} object. +#' @param ... Additional arguments. Currently not in use. +#' @param plots The plots to include in the output list. Can be one or more of +#' the following: \code{"heatmap"}, \code{"silhouette"}, \code{"mds"}, +#' \code{"states"}. +#' +#' @author Philip Leifeld, Kristijan Garic +#' @importFrom ggplot2 autoplot ggplot aes geom_line geom_point xlab ylab +#' labs ggtitle theme_bw theme arrow unit scale_shape_manual element_text +#' scale_x_datetime scale_colour_manual guides +#' @importFrom rlang .data +#' @export +autoplot.dna_phaseTransitions <- function(object, ..., plots = c("heatmap", "silhouette", "mds", "states")) { + # settings for all plots + k <- max(object$states$state) + shapes <- c(21:25, 0:14)[1:k] + l <- list() + + # heatmap + if ("heatmap" %in% plots) { + try({ + if (!requireNamespace("heatmaply", quietly = TRUE)) { + warning("Heatmap skipped because the 'heatmaply' package is not installed.") + } else { + l[[length(l) + 1]] <- heatmaply::ggheatmap(1 - object$distmat, + dendrogram = "both", + showticklabels = FALSE, # remove axis labels + show_dendrogram = TRUE, + hide_colorbar = TRUE) + } + }) + } + + # silhouette plot + if ("silhouette" %in% plots) { + try({ + if (!requireNamespace("cluster", quietly = TRUE)) { + warning("Silhouette plot skipped because the 'cluster' package is not installed.") + } else if (!requireNamespace("factoextra", quietly = TRUE)) { + warning("Silhouette plot skipped because the 'factoextra' package is not installed.") + } else { + sil <- cluster::silhouette(object$states$state, dist(object$distmat)) + l[[length(l) + 1]] <- factoextra::fviz_silhouette(sil, print.summary = FALSE) + + ggplot2::ggtitle(paste0("Cluster silhouettes (mean width: ", round(mean(sil[, 3]), 3), ")")) + + ggplot2::ylab("Silhouette width") + + ggplot2::labs(fill = "State", color = "State") + + ggplot2::theme_classic() + + ggplot2::theme(axis.text.x = element_blank(), axis.ticks.x = element_blank()) + } + }) + } + + # temporal embedding + if ("mds" %in% plots) { + try({ + if (is.na(attributes(object)$stress)) { + warning("No temporal embedding found. Skipping this plot.") + } else if (!requireNamespace("igraph", quietly = TRUE)) { + warning("Temporal embedding plot skipped because the 'igraph' package is not installed.") + } else if (!requireNamespace("ggraph", quietly = TRUE)) { + warning("Temporal embedding plot skipped because the 'ggraph' package is not installed.") + } else { + nodes <- object$states + nodes$date <- as.character(nodes$date) + nodes$State <- as.factor(nodes$state) + + # Extract state values + state_values <- nodes$State + + edges <- data.frame(sender = as.character(object$states$date), + receiver = c(as.character(object$states$date[2:(nrow(object$states))]), "NA")) + edges <- edges[-nrow(edges), ] + g <- igraph::graph_from_data_frame(edges, directed = TRUE, vertices = nodes) + l[[length(l) + 1]] <- ggraph::ggraph(g, layout = "manual", x = igraph::V(g)$X1, y = igraph::V(g)$X2) + + ggraph::geom_edge_link(arrow = ggplot2::arrow(type = "closed", length = ggplot2::unit(2, "mm")), + start_cap = ggraph::circle(1, "mm"), + end_cap = ggraph::circle(2, "mm")) + + ggraph::geom_node_point(ggplot2::aes(shape = state_values, fill = state_values), size = 2) + + ggplot2::scale_shape_manual(values = shapes) + + ggplot2::ggtitle("Temporal embedding (MDS)") + + ggplot2::xlab("Dimension 1") + + ggplot2::ylab("Dimension 2") + + ggplot2::theme_bw() + + ggplot2::guides(size = "none") + + ggplot2::labs(shape = "State", fill = "State") + } + }) + } + + # state dynamics + if ("states" %in% plots) { + try({ + d <- data.frame( + time = object$states$date, + id = cumsum(c(TRUE, diff(object$states$state) != 0)), + State = factor(object$states$state, levels = 1:k, labels = paste("State", 1:k)), + time1 = as.Date(object$states$date) + ) + + # Extracting values + time_values <- d$time + state_values <- d$State + id_values <- d$id + + l[[length(l) + 1]] <- ggplot2::ggplot(d, ggplot2::aes(x = time_values, y = state_values, colour = state_values)) + + ggplot2::geom_line(aes(group = 1), linewidth = 2, color = "black", lineend = "square") + + ggplot2::geom_line(aes(group = id_values), linewidth = 2, lineend = "square") + + ggplot2::scale_x_datetime(date_labels = "%b %Y", breaks = "4 months") + # format x-axis as month year + ggplot2::xlab("Time") + + ggplot2::ylab("") + + ggplot2::ggtitle("State dynamics") + + ggplot2::theme_bw() + + ggplot2::theme(axis.text.x = ggplot2::element_text(angle = 45, hjust = 1)) + + ggplot2::guides(linewidth = "none") + + ggplot2::labs(color = "State") + }) + } + + return(l) +} diff --git a/rDNA/rDNA/man/autoplot.dna_barplot.Rd b/rDNA/rDNA/man/autoplot.dna_barplot.Rd deleted file mode 100644 index cab27a97..00000000 --- a/rDNA/rDNA/man/autoplot.dna_barplot.Rd +++ /dev/null @@ -1,106 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/rDNA.R -\name{autoplot.dna_barplot} -\alias{autoplot.dna_barplot} -\title{Plot \code{dna_barplot} object.} -\usage{ -\method{autoplot}{dna_barplot}( - object, - ..., - lab.pos = "Agreement", - lab.neg = "Disagreement", - lab = TRUE, - colors = FALSE, - fontSize = 12, - barWidth = 0.6, - axisWidth = 1.5, - truncate = 40, - exclude.min = NULL -) -} -\arguments{ -\item{object}{A \code{dna_barplot} object.} - -\item{...}{Additional arguments; currently not in use.} - -\item{lab.pos, lab.neg}{Names for (dis-)agreement labels.} - -\item{lab}{Should (dis-)agreement labels and title be displayed?} - -\item{colors}{If \code{TRUE}, the \code{Colors} column in the -\code{dna_barplot} object will be used to fill the bars. Also accepts -character objects matching one of the attribute variables of the -\code{dna_barplot} object.} - -\item{fontSize}{Text size in pt.} - -\item{barWidth}{Thickness of the bars. Bars will touch when set to \code{1}. -When set to \code{0.5}, space between two bars is the same as thickness of -bars.} - -\item{axisWidth}{Thickness of the x-axis which separates agreement from -disagreement.} - -\item{truncate}{Sets the number of characters to which axis labels should be -truncated.} - -\item{exclude.min}{Reduces the plot to entities with a minimum frequency of -statements.} -} -\description{ -Plot a barplot generated from \code{\link{dna_barplot}}. -} -\details{ -This function plots \code{dna_barplot} objects generated by the -\code{\link{dna_barplot}} function. It plots agreement and disagreement with -DNA statements for different entities such as \code{"concept"}, -\code{"organization"}, or \code{"person"}. Colors can be modified before -plotting (see examples). -} -\examples{ -\dontrun{ -dna_init() -dna_sample() - -dna_openDatabase("sample.dna", coderId = 1, coderPassword = "sample") - -# compute barplot data -b <- dna_barplot(statementType = "DNA Statement", - variable = "concept", - qualifier = "agreement") - -# plot barplot with ggplot2 -library("ggplot2") -autoplot(b) - -# use entity colours (here: colors of organizations as an illustration) -b <- dna_barplot(statementType = "DNA Statement", - variable = "organization", - qualifier = "agreement") -autoplot(b, colors = TRUE) - -# edit the colors before plotting -b$Color[b$Type == "NGO"] <- "red" # change NGO color to red -b$Color[b$Type == "Government"] <- "blue" # change government color to blue -autoplot(b, colors = TRUE) - -# use an attribute, such as type, to color the bars -autoplot(b, colors = "Type") + - scale_colour_manual(values = "black") - -# replace colors for the three possible actor types with custom colors -autoplot(b, colors = "Type") + - scale_fill_manual(values = c("red", "blue", "green")) + - scale_colour_manual(values = "black") -} - -} -\seealso{ -Other {rDNA barplots}: -\code{\link{dna_barplot}()}, -\code{\link{print.dna_barplot}()} -} -\author{ -Johannes B. Gruber, Tim Henrichsen -} -\concept{{rDNA barplots}} diff --git a/rDNA/rDNA/man/dna_api.Rd b/rDNA/rDNA/man/dna_api.Rd new file mode 100644 index 00000000..26830bf1 --- /dev/null +++ b/rDNA/rDNA/man/dna_api.Rd @@ -0,0 +1,55 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/rDNA.R +\name{dna_api} +\alias{dna_api} +\title{Get a reference to the headless Java class for R (API)} +\usage{ +dna_api() +} +\value{ +A Java object reference to the \code{Dna/HeadlessDna} class. +} +\description{ +Get a reference to the headless Java class for R (API). +} +\details{ +This function returns a Java object reference to the instance of the +\code{Dna/HeadlessDna} class in the DNA JAR file that is held in the rDNA +package environment and used by the functions in the package to exchange data +with the Java application. You can use the \pkg{rJava} package to access the +available functions in this class directly. API access requires detailed +knowledge of the DNA JAR classes and functions and is recommended for +developers and advanced users only. +} +\examples{ +\dontrun{ +library("rJava") # load rJava package to use functions in the Java API +dna_init() +dna_sample() +dna_openDatabase(coderId = 1, + coderPassword = "sample", + db_url = "sample.dna") +api <- dna_api() + +# use the \code{getVariables} function to retrieve variables +variable_references <- api$getVariables("DNA Statement") + +# iterate through variable references and print their data type +for (i in seq(variable_references$size()) - 1) { + print(variable_references$get(as.integer(i))$getDataType()) +} +} + +} +\seealso{ +Other {rDNA database connections}: +\code{\link{dna_closeDatabase}()}, +\code{\link{dna_openConnectionProfile}()}, +\code{\link{dna_openDatabase}()}, +\code{\link{dna_printDetails}()}, +\code{\link{dna_saveConnectionProfile}()} +} +\author{ +Philip Leifeld +} +\concept{{rDNA database connections}} diff --git a/rDNA/rDNA/man/dna_backbone.Rd b/rDNA/rDNA/man/dna_backbone.Rd new file mode 100644 index 00000000..128d460c --- /dev/null +++ b/rDNA/rDNA/man/dna_backbone.Rd @@ -0,0 +1,428 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/rDNA.R +\name{dna_backbone} +\alias{dna_backbone} +\alias{print.dna_backbone} +\alias{plot.dna_backbone} +\alias{autoplot.dna_backbone} +\alias{dna_evaluateBackboneSolution} +\title{Compute and retrieve the backbone and redundant set} +\usage{ +dna_backbone( + method = "nested", + backboneSize = 1, + penalty = 3.5, + iterations = 10000, + statementType = "DNA Statement", + variable1 = "organization", + variable1Document = FALSE, + variable2 = "concept", + variable2Document = FALSE, + qualifier = "agreement", + qualifierDocument = FALSE, + qualifierAggregation = "subtract", + normalization = "average", + duplicates = "document", + start.date = "01.01.1900", + stop.date = "31.12.2099", + start.time = "00:00:00", + stop.time = "23:59:59", + excludeValues = list(), + excludeAuthors = character(), + excludeSources = character(), + excludeSections = character(), + excludeTypes = character(), + invertValues = FALSE, + invertAuthors = FALSE, + invertSources = FALSE, + invertSections = FALSE, + invertTypes = FALSE, + fileFormat = NULL, + outfile = NULL +) + +\method{print}{dna_backbone}(x, trim = 50, ...) + +\method{plot}{dna_backbone}(x, ma = 500, ...) + +\method{autoplot}{dna_backbone}(object, ..., ma = 500) + +dna_evaluateBackboneSolution( + backboneEntities, + p = 0, + statementType = "DNA Statement", + variable1 = "organization", + variable1Document = FALSE, + variable2 = "concept", + variable2Document = FALSE, + qualifier = "agreement", + qualifierDocument = FALSE, + qualifierAggregation = "subtract", + normalization = "average", + duplicates = "document", + start.date = "01.01.1900", + stop.date = "31.12.2099", + start.time = "00:00:00", + stop.time = "23:59:59", + excludeValues = list(), + excludeAuthors = character(), + excludeSources = character(), + excludeSections = character(), + excludeTypes = character(), + invertValues = FALSE, + invertAuthors = FALSE, + invertSources = FALSE, + invertSections = FALSE, + invertTypes = FALSE +) +} +\arguments{ +\item{method}{The backbone algorithm used to compute the results. Several +methods are available: +\itemize{ + \item \code{"nested"}: A relatively fast, deterministic algorithm that + produces the full hierarchy of entities. It starts with a complete + backbone set resembling the full network. There are as many iterations + as entities on the second mode. In each iteration, the entity whose + removal would yield the smallest backbone loss is moved from the + backbone set into the redundant set, and the (unpenalized) spectral + loss is recorded. This creates a solution for all backbone sizes, where + each backbone set is fully nested in the next larger backbone set. The + solution usually resembles an unconstrained solution where nesting is + not required, but in some cases the loss of a non-nested solution may be + larger at a given level or number of elements in the backbone set. + \item \code{"fixed"}: Simulated annealing with a fixed number of elements + in the backbone set (i.e., only lateral changes are possible) and + without penalty. This method may yield more optimal solutions than the + nested algorithm because it does not require a strict hierarchy. + However, it produces an approximation of the global optimum and is + slower than the nested method. With this method, you can specify that + backbone set should have, for example, exactly 10 concepts. Then fewer + iterations are necessary than with the penalty method because the search + space is smaller. The backbone set size is defined in the + \code{"backboneSize"} argument. + \item \code{"penalty"}: Simulated annealing with a variable number of + elements in the backbone set. The solution is stabilized by a penalty + parameter (see \code{"penalty"} argument). This algorithm takes longest + to compute for a single solution, and it is only an approximation, but + it considers slightly larger or smaller backbone sets if the solution is + better, thus this algorithm adds some flexibility. It requires more + iterations than the fixed method for achieving the same quality. +}} + +\item{backboneSize}{The number of elements in the backbone set, as a fixed +parameter. Only used when \code{method = "fixed"}.} + +\item{penalty}{The penalty parameter for large backbone sets. The larger the +value, the more strongly larger backbone sets are punished and the smaller +the resulting backbone is. Try out different values to find the right size +of the backbone set. Reasonable values could be \code{2.5}, \code{5}, +\code{7.5}, or \code{12}, for example. The minimum is \code{0.0}, which +imposes no penalty on the size of the backbone set and produces a redundant +set with only one element. Start with \code{0.0} if you want to weed out a +single concept and subsequently increase the penalty to include more items +in the redundant set and shrink the backbone further. Only used when +\code{method = "penalty"}.} + +\item{iterations}{The number of iterations of the simulated annealing +algorithm. More iterations take more time but may lead to better +optimization results. Only used when \code{method = "penalty"} or +\code{method = "fixed"}.} + +\item{statementType}{The name of the statement type in which the variable +of interest is nested. For example, \code{"DNA Statement"}.} + +\item{variable1}{The first variable for network construction. In a one-mode +network, this is the variable for both the rows and columns. In a +two-mode network, this is the variable for the rows only. In an event +list, this variable is only used to check for duplicates (depending on +the setting of the \code{duplicates} argument).} + +\item{variable1Document}{A boolean value indicating whether the first +variable is at the document level (i.e., \code{"author"}, +\code{"source"}, \code{"section"}, \code{"type"}, \code{"id"}, or +\code{"title"}).} + +\item{variable2}{The second variable for network construction. In a one-mode +network, this is the variable over which the ties are created. For +example, if an organization x organization network is created, and ties +in this network indicate co-reference to a concept, then the second +variable is the \code{"concept"}. In a two-mode network, this is the +variable used for the columns of the network matrix. In an event list, +this variable is only used to check for duplicates (depending on the +setting of the \code{duplicates} argument).} + +\item{variable2Document}{A boolean value indicating whether the second +variable is at the document level (i.e., \code{"author"}, +\code{"source"}, \code{"section"}, \code{"type"}, \code{"id"}, or +\code{"title"}} + +\item{qualifier}{The qualifier variable. In a one-mode network, this + variable can be used to count only congruence or conflict ties. For + example, in an organization x organization network via common concepts, + a binary \code{"agreement"} qualifier could be used to record only ties + where both organizations have a positive stance on the concept or where + both organizations have a negative stance on the concept. With an + integer qualifier, the tie weight between the organizations would be + proportional to the similarity or distance between the two organizations + on the scale of the integer variable. With a short text variable as a + qualifier, agreement on common categorical values of the qualifier is + required, for example a tie is established (or a tie weight increased) if + two actors both refer to the same value on the second variable AND match on + the categorical qualifier, for example the type of referral. + + In a two-mode network, the qualifier variable can be used to retain only + positive or only negative statements or subtract negative from positive + mentions. All of this depends on the setting of the + \code{qualifierAggregation} argument. For event lists, the qualifier + variable is only used for filtering out duplicates (depending on the + setting of the \code{duplicates} argument. + + The qualifier can also be \code{NULL}, in which case it is ignored, meaning + that values in \code{variable1} and \code{variable2} are unconditionally + associated with each other in the network when they co-occur. This is + identical to selecting a qualifier variable and setting + \code{qualifierAggregation = "ignore"}.} + +\item{qualifierDocument}{A boolean value indicating whether the qualifier +variable is at the document level (i.e., \code{"author"}, +\code{"source"}, \code{"section"}, \code{"type"}, \code{"id"}, or +\code{"title"}} + +\item{qualifierAggregation}{The aggregation rule for the \code{qualifier} +variable. This must be \code{"ignore"} (for ignoring the qualifier +variable), \code{"congruence"} (for recording a network tie only if both +nodes have the same qualifier value in the binary case or for recording the +similarity between the two nodes on the qualifier variable in the integer +case), \code{"conflict"} (for recording a network tie only if both nodes +have a different qualifier value in the binary case or for recording the +distance between the two nodes on the qualifier variable in the integer +case), or \code{"subtract"} (for subtracting the conflict tie value from +the congruence tie value in each dyad; note that negative values will be +replaced by \code{0} in the backbone calculation).} + +\item{normalization}{Normalization of edge weights. Valid settings are +\code{"no"} (for switching off normalization), \code{"average"} (for +average activity normalization), \code{"jaccard"} (for Jaccard coefficient +normalization), and \code{"cosine"} (for cosine similarity normalization).} + +\item{duplicates}{Setting for excluding duplicate statements before network +construction. Valid settings are \code{"include"} (for including all +statements in network construction), \code{"document"} (for counting +only one identical statement per document), \code{"week"} (for counting +only one identical statement per calendar week), \code{"month"} (for +counting only one identical statement per calendar month), \code{"year"} +(for counting only one identical statement per calendar year), and +\code{"acrossrange"} (for counting only one identical statement across +the whole time range).} + +\item{start.date}{The start date for network construction in the format +\code{"dd.mm.yyyy"}. All statements before this date will be excluded.} + +\item{stop.date}{The stop date for network construction in the format +\code{"dd.mm.yyyy"}. All statements after this date will be excluded.} + +\item{start.time}{The start time for network construction on the specified +\code{start.date}. All statements before this time on the specified date +will be excluded.} + +\item{stop.time}{The stop time for network construction on the specified +\code{stop.date}. All statements after this time on the specified date +will be excluded.} + +\item{excludeValues}{A list of named character vectors that contains entries +which should be excluded during network construction. For example, +\code{list(concept = c("A", "B"), organization = c("org A", "org B"))} +would exclude all statements containing concepts "A" or "B" or +organizations "org A" or "org B" when the network is constructed. This +is irrespective of whether these values appear in \code{variable1}, +\code{variable2}, or the \code{qualifier}. Note that only variables at +the statement level can be used here. There are separate arguments for +excluding statements nested in documents with certain meta-data.} + +\item{excludeAuthors}{A character vector of authors. If a statement is +nested in a document where one of these authors is set in the "Author" +meta-data field, the statement is excluded from network construction.} + +\item{excludeSources}{A character vector of sources. If a statement is +nested in a document where one of these sources is set in the "Source" +meta-data field, the statement is excluded from network construction.} + +\item{excludeSections}{A character vector of sections. If a statement is +nested in a document where one of these sections is set in the "Section" +meta-data field, the statement is excluded from network construction.} + +\item{excludeTypes}{A character vector of types. If a statement is +nested in a document where one of these types is set in the "Type" +meta-data field, the statement is excluded from network construction.} + +\item{invertValues}{A boolean value indicating whether the entries provided +by the \code{excludeValues} argument should be excluded from network +construction (\code{invertValues = FALSE}) or if they should be the only +values that should be included during network construction +(\code{invertValues = TRUE}).} + +\item{invertAuthors}{A boolean value indicating whether the entries provided +by the \code{excludeAuthors} argument should be excluded from network +construction (\code{invertAuthors = FALSE}) or if they should be the +only values that should be included during network construction +(\code{invertAuthors = TRUE}).} + +\item{invertSources}{A boolean value indicating whether the entries provided +by the \code{excludeSources} argument should be excluded from network +construction (\code{invertSources = FALSE}) or if they should be the +only values that should be included during network construction +(\code{invertSources = TRUE}).} + +\item{invertSections}{A boolean value indicating whether the entries +provided by the \code{excludeSections} argument should be excluded from +network construction (\code{invertSections = FALSE}) or if they should +be the only values that should be included during network construction +(\code{invertSections = TRUE}).} + +\item{invertTypes}{A boolean value indicating whether the entries provided +by the \code{excludeTypes} argument should be excluded from network +construction (\code{invertTypes = FALSE}) or if they should be the +only values that should be included during network construction +(\code{invertTypes = TRUE}).} + +\item{fileFormat}{An optional file format specification for saving the +backbone results to a file instead of returning an object. Valid values +are \code{"json"}, \code{"xml"}, and \code{NULL} (for returning the results +instead of writing them to a file).} + +\item{outfile}{An optional output file name for saving the resulting +network(s) to a file instead of returning an object.} + +\item{x}{A \code{"dna_backbone"} object.} + +\item{trim}{Number of maximum characters to display in entity labels. Labels +with more characters are truncated, and the last character is replaced by +an asterisk (\code{*}).} + +\item{...}{Additional arguments.} + +\item{ma}{Number of iterations to compute moving average.} + +\item{object}{A \code{"dna_backbone"} object.} + +\item{backboneEntities}{A vector of character values to be included in the +backbone. The function will compute the spectral loss between the full +network and the network composed only of those entities on the second mode +that are contained in this vector.} + +\item{p}{The penalty parameter. The default value of \code{0} means no +penalty for backbone size is applied.} +} +\value{ +A vector with two numeric values: the backbone and redundant loss. +} +\description{ +Compute and retrieve the backbone and redundant set of a discourse network. + +Compute the backbone loss for any set of entities, for example concepts. +} +\details{ +This function applies a simulated annealing algorithm to the discourse +network to partition the set of second-mode entities (e.g., concepts) into a +backbone set and a complementary redundant set. + +This function computes the spectral loss for an arbitrary backbone and its +complement, the redundant set, specified by the user. For example, the user +can evaluate how much structure would be lost if the second mode was composed +only of the concepts provided to this function. This can be used to compare +how useful different codebook models are. The penalty parameter \code{p} +applies a penalty factor to the spectral loss. The default value of \code{0} +switches off the penalty. +} +\examples{ +\dontrun{ +dna_init() +dna_sample() +dna_openDatabase("sample.dna", coderId = 1, coderPassword = "sample") + +# compute backbone and redundant set using penalised spectral loss +b <- dna_backbone(method = "penalty", + penalty = 3.5, + iterations = 10000, + variable1 = "organization", + variable2 = "concept", + qualifier = "agreement", + qualifierAggregation = "subtract", + normalization = "average") + +b # display main results + +# extract results from the object +b$backbone # show the set of backbone concepts +b$redundant # show the set of redundant concepts +b$unpenalized_backbone_loss # spectral loss between full and backbone network +b$unpenalized_redundant_loss # spectral loss of redundant network +b$backbone_network # show the backbone network +b$redundant_network # show the redundant network +b$full_network # show the full network + +# plot diagnostics with base R +plot(b, ma = 500) + +# arrange plots in a 2 x 2 view +par(mfrow = c(2, 2)) +plot(b) + +# plot diagnostics with ggplot2 +library("ggplot2") +p <- autoplot(b) +p + +# pick a specific diagnostic +p[[3]] + +# use the patchwork package to arrange the diagnostics in a single plot +library("patchwork") +new_plot <- p[[1]] + p[[2]] + p[[3]] + p[[4]] +new_plot & theme_grey() + theme(legend.position = "bottom") + +# use the gridExtra package to arrange the diagnostics in a single plot +library("gridExtra") +grid.arrange(p[[1]], p[[2]], p[[3]], p[[4]]) + +# compute backbone with fixed size (here: 4 concepts) +b <- dna_backbone(method = "fixed", + backboneSize = 4, + iterations = 2000, + variable1 = "organization", + variable2 = "concept", + qualifier = "agreement", + qualifierAggregation = "subtract", + normalization = "average") +b + +# compute backbone with a nested structure and plot dendrogram +b <- dna_backbone(method = "nested", + variable1 = "organization", + variable2 = "concept", + qualifier = "agreement", + qualifierAggregation = "subtract", + normalization = "average") +b +plot(b) +autoplot(b) +} + +\dontrun{ +dna_init() +dna_sample() +dna_openDatabase("sample.dna", coderId = 1, coderPassword = "sample") + +dna_evaluateBackboneSolution( + c("There should be legislation to regulate emissions.", + "Emissions legislation should regulate CO2.") +) +} + +} +\author{ +Philip Leifeld, Tim Henrichsen + +Philip Leifeld +} diff --git a/rDNA/rDNA/man/dna_barplot.Rd b/rDNA/rDNA/man/dna_barplot.Rd index 5fcf03d9..06dce2f6 100644 --- a/rDNA/rDNA/man/dna_barplot.Rd +++ b/rDNA/rDNA/man/dna_barplot.Rd @@ -2,6 +2,8 @@ % Please edit documentation in R/rDNA.R \name{dna_barplot} \alias{dna_barplot} +\alias{print.dna_barplot} +\alias{autoplot.dna_barplot} \title{Generate the data necessary for creating a barplot for a variable} \usage{ dna_barplot( @@ -24,6 +26,22 @@ dna_barplot( invertSections = FALSE, invertTypes = FALSE ) + +\method{print}{dna_barplot}(x, trim = 30, attr = TRUE, ...) + +\method{autoplot}{dna_barplot}( + object, + ..., + lab.pos = "Agreement", + lab.neg = "Disagreement", + lab = TRUE, + colors = FALSE, + fontSize = 12, + barWidth = 0.6, + axisWidth = 1.5, + truncate = 40, + exclude.min = NULL +) } \arguments{ \item{statementType}{The name of the statement type in which the variable @@ -114,14 +132,64 @@ by the \code{excludeTypes} argument should be excluded from network construction (\code{invertTypes = FALSE}) or if they should be the only values that should be included during network construction (\code{invertTypes = TRUE}).} + +\item{x}{A \code{dna_barplot} object, as returned by the +\code{\link{dna_barplot}} function.} + +\item{trim}{Number of maximum characters to display in entity labels. +Entities with more characters are truncated, and the last character is +replaced by an asterisk (\code{*}).} + +\item{attr}{Display attributes, such as the name of the variable and the +levels of the qualifier variable if available.} + +\item{...}{Additional arguments; currently not in use.} + +\item{object}{A \code{dna_barplot} object.} + +\item{lab.pos, lab.neg}{Names for (dis-)agreement labels.} + +\item{lab}{Should (dis-)agreement labels and title be displayed?} + +\item{colors}{If \code{TRUE}, the \code{Colors} column in the +\code{dna_barplot} object will be used to fill the bars. Also accepts +character objects matching one of the attribute variables of the +\code{dna_barplot} object.} + +\item{fontSize}{Text size in pt.} + +\item{barWidth}{Thickness of the bars. Bars will touch when set to \code{1}. +When set to \code{0.5}, space between two bars is the same as thickness of +bars.} + +\item{axisWidth}{Thickness of the x-axis which separates agreement from +disagreement.} + +\item{truncate}{Sets the number of characters to which axis labels should be +truncated.} + +\item{exclude.min}{Reduces the plot to entities with a minimum frequency of +statements.} } \description{ Generate the data necessary for creating a barplot for a variable. + +Show details of a \code{dna_barplot} object. + +Plot a barplot generated from \code{\link{dna_barplot}}. } \details{ Create a \code{dna_barplot} object, which contains a data frame with entity value frequencies grouped by the levels of a qualifier variable. The qualifier variable is optional. + +Print the data frame returned by the \code{\link{dna_barplot}} function. + +This function plots \code{dna_barplot} objects generated by the +\code{\link{dna_barplot}} function. It plots agreement and disagreement with +DNA statements for different entities such as \code{"concept"}, +\code{"organization"}, or \code{"person"}. Colors can be modified before +plotting (see examples). } \examples{ \dontrun{ @@ -136,13 +204,45 @@ b <- dna_barplot(statementType = "DNA Statement", b } +\dontrun{ +dna_init() +dna_sample() + +dna_openDatabase("sample.dna", coderId = 1, coderPassword = "sample") + +# compute barplot data +b <- dna_barplot(statementType = "DNA Statement", + variable = "concept", + qualifier = "agreement") + +# plot barplot with ggplot2 +library("ggplot2") +autoplot(b) + +# use entity colours (here: colors of organizations as an illustration) +b <- dna_barplot(statementType = "DNA Statement", + variable = "organization", + qualifier = "agreement") +autoplot(b, colors = TRUE) + +# edit the colors before plotting +b$Color[b$Type == "NGO"] <- "red" # change NGO color to red +b$Color[b$Type == "Government"] <- "blue" # change government color to blue +autoplot(b, colors = TRUE) + +# use an attribute, such as type, to color the bars +autoplot(b, colors = "Type") + + scale_colour_manual(values = "black") + +# replace colors for the three possible actor types with custom colors +autoplot(b, colors = "Type") + + scale_fill_manual(values = c("red", "blue", "green")) + + scale_colour_manual(values = "black") } -\seealso{ -Other {rDNA barplots}: -\code{\link{autoplot.dna_barplot}()}, -\code{\link{print.dna_barplot}()} + } \author{ Philip Leifeld + +Johannes B. Gruber, Tim Henrichsen } -\concept{{rDNA barplots}} diff --git a/rDNA/rDNA/man/dna_closeDatabase.Rd b/rDNA/rDNA/man/dna_closeDatabase.Rd index aef6faa9..a9488a7c 100644 --- a/rDNA/rDNA/man/dna_closeDatabase.Rd +++ b/rDNA/rDNA/man/dna_closeDatabase.Rd @@ -26,6 +26,7 @@ dna_closeDatabase() } \seealso{ Other {rDNA database connections}: +\code{\link{dna_api}()}, \code{\link{dna_openConnectionProfile}()}, \code{\link{dna_openDatabase}()}, \code{\link{dna_printDetails}()}, diff --git a/rDNA/rDNA/man/dna_getVariables.Rd b/rDNA/rDNA/man/dna_getVariables.Rd new file mode 100644 index 00000000..d632fe79 --- /dev/null +++ b/rDNA/rDNA/man/dna_getVariables.Rd @@ -0,0 +1,34 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/rDNA.R +\name{dna_getVariables} +\alias{dna_getVariables} +\title{Retrieve a dataframe with all variables for a statement type} +\usage{ +dna_getVariables(statementType) +} +\arguments{ +\item{statementType}{The statement type for which statements should be +retrieved. The statement type can be supplied as an integer or character +string, for example \code{1} or \code{"DNA Statement"}.} +} +\description{ +Retrieve a dataframe with all variables defined in a given statement type. +} +\details{ +For a given statement type ID or label, this function creates a data frame +with one row per variable and contains columns for the variable ID, name and +data type. +} +\examples{ +\dontrun{ +dna_init() +samp <- dna_sample() +dna_openDatabase(samp, coderId = 1, coderPassword = "sample") +variables <- dna_getVariables("DNA Statement") +variables +} + +} +\author{ +Philip Leifeld +} diff --git a/rDNA/rDNA/man/dna_multiclust.Rd b/rDNA/rDNA/man/dna_multiclust.Rd new file mode 100644 index 00000000..87ea34bc --- /dev/null +++ b/rDNA/rDNA/man/dna_multiclust.Rd @@ -0,0 +1,437 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/rDNA.R +\name{dna_multiclust} +\alias{dna_multiclust} +\alias{print.dna_multiclust} +\title{Compute multiple cluster solutions for a discourse network} +\usage{ +dna_multiclust( + statementType = "DNA Statement", + variable1 = "organization", + variable1Document = FALSE, + variable2 = "concept", + variable2Document = FALSE, + qualifier = "agreement", + duplicates = "include", + start.date = "01.01.1900", + stop.date = "31.12.2099", + start.time = "00:00:00", + stop.time = "23:59:59", + timeWindow = "no", + windowSize = 100, + excludeValues = list(), + excludeAuthors = character(), + excludeSources = character(), + excludeSections = character(), + excludeTypes = character(), + invertValues = FALSE, + invertAuthors = FALSE, + invertSources = FALSE, + invertSections = FALSE, + invertTypes = FALSE, + saveObjects = FALSE, + k = 0, + k.max = 5, + single = TRUE, + average = TRUE, + complete = TRUE, + ward = TRUE, + kmeans = TRUE, + pam = TRUE, + equivalence = TRUE, + concor_one = TRUE, + concor_two = TRUE, + louvain = TRUE, + fastgreedy = TRUE, + walktrap = TRUE, + leading_eigen = TRUE, + edge_betweenness = TRUE, + infomap = TRUE, + label_prop = TRUE, + spinglass = FALSE +) + +\method{print}{dna_multiclust}(x, ...) +} +\arguments{ +\item{statementType}{The name of the statement type in which the variable +of interest is nested. For example, \code{"DNA Statement"}.} + +\item{variable1}{The first variable for network construction. In a one-mode +network, this is the variable for both the rows and columns. In a +two-mode network, this is the variable for the rows only. In an event +list, this variable is only used to check for duplicates (depending on +the setting of the \code{duplicates} argument).} + +\item{variable1Document}{A boolean value indicating whether the first +variable is at the document level (i.e., \code{"author"}, +\code{"source"}, \code{"section"}, \code{"type"}, \code{"id"}, or +\code{"title"}).} + +\item{variable2}{The second variable for network construction. In a one-mode +network, this is the variable over which the ties are created. For +example, if an organization x organization network is created, and ties +in this network indicate co-reference to a concept, then the second +variable is the \code{"concept"}. In a two-mode network, this is the +variable used for the columns of the network matrix. In an event list, +this variable is only used to check for duplicates (depending on the +setting of the \code{duplicates} argument).} + +\item{variable2Document}{A boolean value indicating whether the second +variable is at the document level (i.e., \code{"author"}, +\code{"source"}, \code{"section"}, \code{"type"}, \code{"id"}, or +\code{"title"}} + +\item{qualifier}{The qualifier variable. In a one-mode network, this + variable can be used to count only congruence or conflict ties. For + example, in an organization x organization network via common concepts, + a binary \code{"agreement"} qualifier could be used to record only ties + where both organizations have a positive stance on the concept or where + both organizations have a negative stance on the concept. With an + integer qualifier, the tie weight between the organizations would be + proportional to the similarity or distance between the two organizations + on the scale of the integer variable. With a short text variable as a + qualifier, agreement on common categorical values of the qualifier is + required, for example a tie is established (or a tie weight increased) if + two actors both refer to the same value on the second variable AND match on + the categorical qualifier, for example the type of referral. + + In a two-mode network, the qualifier variable can be used to retain only + positive or only negative statements or subtract negative from positive + mentions. All of this depends on the setting of the + \code{qualifierAggregation} argument. For event lists, the qualifier + variable is only used for filtering out duplicates (depending on the + setting of the \code{duplicates} argument. + + The qualifier can also be \code{NULL}, in which case it is ignored, meaning + that values in \code{variable1} and \code{variable2} are unconditionally + associated with each other in the network when they co-occur. This is + identical to selecting a qualifier variable and setting + \code{qualifierAggregation = "ignore"}.} + +\item{duplicates}{Setting for excluding duplicate statements before network +construction. Valid settings are \code{"include"} (for including all +statements in network construction), \code{"document"} (for counting +only one identical statement per document), \code{"week"} (for counting +only one identical statement per calendar week), \code{"month"} (for +counting only one identical statement per calendar month), \code{"year"} +(for counting only one identical statement per calendar year), and +\code{"acrossrange"} (for counting only one identical statement across +the whole time range).} + +\item{start.date}{The start date for network construction in the format +\code{"dd.mm.yyyy"}. All statements before this date will be excluded.} + +\item{stop.date}{The stop date for network construction in the format +\code{"dd.mm.yyyy"}. All statements after this date will be excluded.} + +\item{start.time}{The start time for network construction on the specified +\code{start.date}. All statements before this time on the specified date +will be excluded.} + +\item{stop.time}{The stop time for network construction on the specified +\code{stop.date}. All statements after this time on the specified date +will be excluded.} + +\item{timeWindow}{Possible values are \code{"no"}, \code{"events"}, +\code{"seconds"}, \code{"minutes"}, \code{"hours"}, \code{"days"}, +\code{"weeks"}, \code{"months"}, and \code{"years"}. If \code{"no"} is +selected (= the default setting), no time window will be used. If any of +the time units is selected, a moving time window will be imposed, and +only the statements falling within the time period defined by the window +will be used to create the network. The time window will then be moved +forward by one time unit at a time, and a new network with the new time +boundaries will be created. This is repeated until the end of the overall +time span is reached. All time windows will be saved as separate +networks in a list. The duration of each time window is defined by the +\code{windowSize} argument. For example, this could be used to create a +time window of 6 months which moves forward by one month each time, thus +creating time windows that overlap by five months. If \code{"events"} is +used instead of a natural time unit, the time window will comprise +exactly as many statements as defined in the \code{windowSize} argument. +However, if the start or end statement falls on a date and time where +multiple events happen, those additional events that occur simultaneously +are included because there is no other way to decide which of the +statements should be selected. Therefore the window size is sometimes +extended when the start or end point of a time window is ambiguous in +event time.} + +\item{windowSize}{The number of time units of which a moving time window is +comprised. This can be the number of statement events, the number of days +etc., as defined in the \code{"timeWindow"} argument.} + +\item{excludeValues}{A list of named character vectors that contains entries +which should be excluded during network construction. For example, +\code{list(concept = c("A", "B"), organization = c("org A", "org B"))} +would exclude all statements containing concepts "A" or "B" or +organizations "org A" or "org B" when the network is constructed. This +is irrespective of whether these values appear in \code{variable1}, +\code{variable2}, or the \code{qualifier}. Note that only variables at +the statement level can be used here. There are separate arguments for +excluding statements nested in documents with certain meta-data.} + +\item{excludeAuthors}{A character vector of authors. If a statement is +nested in a document where one of these authors is set in the "Author" +meta-data field, the statement is excluded from network construction.} + +\item{excludeSources}{A character vector of sources. If a statement is +nested in a document where one of these sources is set in the "Source" +meta-data field, the statement is excluded from network construction.} + +\item{excludeSections}{A character vector of sections. If a statement is +nested in a document where one of these sections is set in the "Section" +meta-data field, the statement is excluded from network construction.} + +\item{excludeTypes}{A character vector of types. If a statement is +nested in a document where one of these types is set in the "Type" +meta-data field, the statement is excluded from network construction.} + +\item{invertValues}{A boolean value indicating whether the entries provided +by the \code{excludeValues} argument should be excluded from network +construction (\code{invertValues = FALSE}) or if they should be the only +values that should be included during network construction +(\code{invertValues = TRUE}).} + +\item{invertAuthors}{A boolean value indicating whether the entries provided +by the \code{excludeAuthors} argument should be excluded from network +construction (\code{invertAuthors = FALSE}) or if they should be the +only values that should be included during network construction +(\code{invertAuthors = TRUE}).} + +\item{invertSources}{A boolean value indicating whether the entries provided +by the \code{excludeSources} argument should be excluded from network +construction (\code{invertSources = FALSE}) or if they should be the +only values that should be included during network construction +(\code{invertSources = TRUE}).} + +\item{invertSections}{A boolean value indicating whether the entries +provided by the \code{excludeSections} argument should be excluded from +network construction (\code{invertSections = FALSE}) or if they should +be the only values that should be included during network construction +(\code{invertSections = TRUE}).} + +\item{invertTypes}{A boolean value indicating whether the entries provided +by the \code{excludeTypes} argument should be excluded from network +construction (\code{invertTypes = FALSE}) or if they should be the +only values that should be included during network construction +(\code{invertTypes = TRUE}).} + +\item{saveObjects}{Store the original output of the respective clustering +method in the \code{cl} slot of the return object? If \code{TRUE}, one +cluster object per time point will be saved, for all time points for which +network data are available. At each time point, only the cluster object +with the highest modularity score will be saved, all others discarded. The +\code{max_mod} slot of the object contains additional information on which +measure was saved at each time point and what the corresponding modularity +score is.} + +\item{k}{The number of clusters to compute. This constrains the choice of +clustering methods because some methods require a predefined \code{k} while +other methods do not. To permit arbitrary numbers of clusters, depending on +the respective algorithm (or the value of modularity in some cases), choose +\code{k = 0}. This corresponds to the theoretical notion of +"multipolarization". For "bipolarization", choose \code{k = 2} in order to +constrain the cluster solutions to exactly two groups.} + +\item{k.max}{If \code{k = 0}, there can be arbitrary numbers of clusters. In +this case, \code{k.max} sets the maximal number of clusters that can be +identified.} + +\item{single}{Include hierarchical clustering with single linkage in the pool +of clustering methods? The \code{\link[stats]{hclust}} function from +the \pkg{stats} package is applied to Jaccard distances in the affiliation +network for this purpose. Only valid if \code{k > 1}.} + +\item{average}{Include hierarchical clustering with average linkage in the +pool of clustering methods? The \code{\link[stats]{hclust}} function from +the \pkg{stats} package is applied to Jaccard distances in the affiliation +network for this purpose. Only valid if \code{k > 1}.} + +\item{complete}{Include hierarchical clustering with complete linkage in the +pool of clustering methods? The \code{\link[stats]{hclust}} function from +the \pkg{stats} package is applied to Jaccard distances in the affiliation +network for this purpose. Only valid if \code{k > 1}.} + +\item{ward}{Include hierarchical clustering with Ward's algorithm in the +pool of clustering methods? The \code{\link[stats]{hclust}} function from +the \pkg{stats} package is applied to Jaccard distances in the affiliation +network for this purpose. If \code{k = 0} is selected, different solutions +with varying \code{k} are attempted, and the solution with the highest +modularity is retained.} + +\item{kmeans}{Include k-means clustering in the pool of clustering methods? +The \code{\link[stats]{kmeans}} function from the \pkg{stats} package is +applied to Jaccard distances in the affiliation network for this purpose. +If \code{k = 0} is selected, different solutions with varying \code{k} are +attempted, and the solution with the highest modularity is retained.} + +\item{pam}{Include partitioning around medoids in the pool of clustering +methods? The \code{\link[cluster]{pam}} function from the \pkg{cluster} +package is applied to Jaccard distances in the affiliation network for this +purpose. If \code{k = 0} is selected, different solutions with varying +\code{k} are attempted, and the solution with the highest modularity is +retained.} + +\item{equivalence}{Include equivalence clustering (as implemented in the +\code{\link[sna]{equiv.clust}} function in the \pkg{sna} package), based on +shortest path distances between nodes (as implemented in the +\code{\link[sna]{sedist}} function in the \pkg{sna} package) in the +positive subtract network? If \code{k = 0} is selected, different solutions +with varying \code{k} are attempted, and the solution with the highest +modularity is retained.} + +\item{concor_one}{Include CONvergence of iterative CORrelations (CONCOR) in +the pool of clustering methods? The algorithm is applied to the positive +subtract network to identify \code{k = 2} clusters. The method is omitted +if \code{k != 2}.} + +\item{concor_two}{Include CONvergence of iterative CORrelations (CONCOR) in +the pool of clustering methods? The algorithm is applied to the affiliation +network to identify \code{k = 2} clusters. The method is omitted +if \code{k != 2}.} + +\item{louvain}{Include the Louvain community detection algorithm in the pool +of clustering methods? The \code{\link[igraph]{cluster_louvain}} function +in the \pkg{igraph} package is applied to the positive subtract network for +this purpose.} + +\item{fastgreedy}{Include the fast and greedy community detection algorithm +in the pool of clustering methods? The +\code{\link[igraph]{cluster_fast_greedy}} function in the \pkg{igraph} +package is applied to the positive subtract network for this purpose.} + +\item{walktrap}{Include the Walktrap community detection algorithm +in the pool of clustering methods? The +\code{\link[igraph]{cluster_walktrap}} function in the \pkg{igraph} +package is applied to the positive subtract network for this purpose.} + +\item{leading_eigen}{Include the leading eigenvector community detection +algorithm in the pool of clustering methods? The +\code{\link[igraph]{cluster_leading_eigen}} function in the \pkg{igraph} +package is applied to the positive subtract network for this purpose.} + +\item{edge_betweenness}{Include the edge betweenness community detection +algorithm by Girvan and Newman in the pool of clustering methods? The +\code{\link[igraph]{cluster_edge_betweenness}} function in the \pkg{igraph} +package is applied to the positive subtract network for this purpose.} + +\item{infomap}{Include the infomap community detection algorithm +in the pool of clustering methods? The +\code{\link[igraph]{cluster_infomap}} function in the \pkg{igraph} +package is applied to the positive subtract network for this purpose.} + +\item{label_prop}{Include the label propagation community detection algorithm +in the pool of clustering methods? The +\code{\link[igraph]{cluster_label_prop}} function in the \pkg{igraph} +package is applied to the positive subtract network for this purpose.} + +\item{spinglass}{Include the spinglass community detection algorithm +in the pool of clustering methods? The +\code{\link[igraph]{cluster_spinglass}} function in the \pkg{igraph} +package is applied to the positive subtract network for this purpose. Note +that this method is disabled by default because it is relatively slow.} + +\item{x}{A \code{dna_multiclust} object.} + +\item{...}{Further options (currently not used).} +} +\value{ +The function creates a \code{dna_multiclust} object, which contains + the following items: +\describe{ + \item{k}{The number of clusters determined by the user.} + \item{cl}{Cluster objects returned by the respective cluster function. If + multiple methods are used, this returns the object with the highest + modularity.} + \item{max_mod}{A data frame with one row per time point (that is, only one + row in the default case and multiple rows if time windows are used) and + the maximal modularity for the given time point across all cluster + methods.} + \item{modularity}{A data frame with the modularity values for all separate + cluster methods and all time points.} + \item{membership}{A large data frame with all nodes' membership information + for each time point and each clustering method.} +} +} +\description{ +Compute multiple cluster solutions for a discourse network. + +Show details of a \code{dna_multiclust} object. +} +\details{ +This function applies a number of different graph clustering techniques to +a discourse network dataset. The user provides many of the same arguments as +in the \code{\link{dna_network}} function and a few additional arguments that +determine which kinds of clustering methods should be used and how. In +particular, the \code{k} argument can be \code{0} (for arbitrary numbers of +clusters) or any positive integer value (e.g., \code{2}, for constraining the +number of clusters to exactly \code{k} groups). This is useful for assessing +the polarization of a discourse network. + +In particular, the function can be used to compute the maximal modularity of +a smoothed time series of discourse networks using the \code{timeWindow} and +\code{windowSize} arguments for a given \code{k} across a number of +clustering methods. + +It is also possible to switch off all but one clustering method using the +respective arguments and carry out a simple cluster analysis with the method +of choice for a certain time span of the discourse network, without any time +window options. + +Print abbreviated contents for the slots of a \code{dna_multiclust} object, +which can be created using the \link{dna_multiclust} function. +} +\examples{ +\dontrun{ +library("rDNA") +dna_init() +samp <- dna_sample() +dna_openDatabase(samp, coderId = 1, coderPassword = "sample") + +# example 1: compute 12 cluster solutions for one time point +mc1 <- dna_multiclust(variable1 = "organization", + variable2 = "concept", + qualifier = "agreement", + duplicates = "document", + k = 0, # flexible numbers of clusters + saveObjects = TRUE) # retain hclust object + +mc1$modularity # return modularity scores for 12 clustering methods +mc1$max_mod # return the maximal value of the 12, along with dates +mc1$memberships # return cluster memberships for all 12 cluster methods +plot(mc1$cl[[1]]) # plot hclust dendrogram + +# example 2: compute only Girvan-Newman edge betweenness with two clusters +set.seed(12345) +mc2 <- dna_multiclust(k = 2, + single = FALSE, + average = FALSE, + complete = FALSE, + ward = FALSE, + kmeans = FALSE, + pam = FALSE, + equivalence = FALSE, + concor_one = FALSE, + concor_two = FALSE, + louvain = FALSE, + fastgreedy = FALSE, + walktrap = FALSE, + leading_eigen = FALSE, + edge_betweenness = TRUE, + infomap = FALSE, + label_prop = FALSE, + spinglass = FALSE) +mc2$memberships # return membership in two clusters +mc2$modularity # return modularity of the cluster solution + +# example 3: smoothed modularity using time window algorithm +mc3 <- dna_multiclust(k = 2, + timeWindow = "events", + windowSize = 28) +mc3$max_mod # maximal modularity and method per time point +} + +} +\author{ +Philip Leifeld +} diff --git a/rDNA/rDNA/man/dna_openConnectionProfile.Rd b/rDNA/rDNA/man/dna_openConnectionProfile.Rd index 322a50a6..8cd02116 100644 --- a/rDNA/rDNA/man/dna_openConnectionProfile.Rd +++ b/rDNA/rDNA/man/dna_openConnectionProfile.Rd @@ -46,6 +46,7 @@ dna_openConnectionProfile(file = "my profile.dnc", coderPassword = "sample") } \seealso{ Other {rDNA database connections}: +\code{\link{dna_api}()}, \code{\link{dna_closeDatabase}()}, \code{\link{dna_openDatabase}()}, \code{\link{dna_printDetails}()}, diff --git a/rDNA/rDNA/man/dna_openDatabase.Rd b/rDNA/rDNA/man/dna_openDatabase.Rd index 34f38c0f..a235d245 100644 --- a/rDNA/rDNA/man/dna_openDatabase.Rd +++ b/rDNA/rDNA/man/dna_openDatabase.Rd @@ -70,6 +70,7 @@ dna_openDatabase(coderId = 1, \code{\link{dna_queryCoders}} Other {rDNA database connections}: +\code{\link{dna_api}()}, \code{\link{dna_closeDatabase}()}, \code{\link{dna_openConnectionProfile}()}, \code{\link{dna_printDetails}()}, diff --git a/rDNA/rDNA/man/dna_phaseTransitions.Rd b/rDNA/rDNA/man/dna_phaseTransitions.Rd new file mode 100644 index 00000000..d1a8343f --- /dev/null +++ b/rDNA/rDNA/man/dna_phaseTransitions.Rd @@ -0,0 +1,397 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/rDNA.R +\name{dna_phaseTransitions} +\alias{dna_phaseTransitions} +\alias{print.dna_phaseTransitions} +\alias{autoplot.dna_phaseTransitions} +\title{Detect phase transitions and states in a discourse network} +\usage{ +dna_phaseTransitions( + distanceMethod = "absdiff", + normalizeNetwork = FALSE, + clusterMethods = c("single", "average", "complete", "ward", "kmeans", "pam", + "spectral", "fastgreedy", "walktrap"), + k.min = 2, + k.max = 6, + splitNodes = FALSE, + cores = 1, + networkType = "twomode", + statementType = "DNA Statement", + variable1 = "organization", + variable1Document = FALSE, + variable2 = "concept", + variable2Document = FALSE, + qualifier = "agreement", + qualifierDocument = FALSE, + qualifierAggregation = "subtract", + normalization = "no", + duplicates = "document", + start.date = "01.01.1900", + stop.date = "31.12.2099", + start.time = "00:00:00", + stop.time = "23:59:59", + timeWindow = "days", + windowSize = 150, + excludeValues = list(), + excludeAuthors = character(), + excludeSources = character(), + excludeSections = character(), + excludeTypes = character(), + invertValues = FALSE, + invertAuthors = FALSE, + invertSources = FALSE, + invertSections = FALSE, + invertTypes = FALSE +) + +\method{print}{dna_phaseTransitions}(x, ...) + +\method{autoplot}{dna_phaseTransitions}(object, ..., plots = c("heatmap", "silhouette", "mds", "states")) +} +\arguments{ +\item{distanceMethod}{The distance measure that expresses the dissimilarity +between any two network matrices. The following choices are available: +\itemize{ + \item \code{"absdiff"}: The sum of the cell-wise absolute differences + between the two matrices, i.e., the sum of differences in edge weights. + This is equivalent to the graph edit distance because the network + dimensions are kept constant across all networks by including all nodes + at all time points (i.e., by including isolates). + \item \code{"spectral"}: The Euclidean distance between the normalized + eigenvalues of the graph Laplacian matrices, also called the spectral + distance between two network matrices. Any negative values (e.g., from + the subtract method) are replaced by zero before computing the + distance. + \item \code{"modularity"}: The difference in maximal modularity as + obtained through an approximation via community detection. Note that + this method has not been implemented. +}} + +\item{normalizeNetwork}{Divide all cells by their sum before computing +the dissimilarity between two network matrices? This normalization scales +all edge weights to a sum of \code{1.0}.} + +\item{clusterMethods}{The clustering techniques that are applied to the +distance matrix in the end. Hierarchical methods are repeatedly cut off at +different levels, and solutions are compared using network modularity to +pick the best-fitting cluster membership vector. Some of the methods are +slower than others, hence they are not included by default. It is possible +to include any number of methods in the argument. For each included method, +the cluster membership vector (i.e., the states over time) along with the +associated time stamps of the networks are returned, and the modularity of +each included method is computed for comparison. The following methods are +available: +\itemize{ + \item \code{"single"}: Hierarchical clustering with single linkage using + the \code{\link[stats]{hclust}} function from the \pkg{stats} package. + \item \code{"average"}: Hierarchical clustering with average linkage + using the \code{\link[stats]{hclust}} function from the \pkg{stats} + package. + \item \code{"complete"}: Hierarchical clustering with complete linkage + using the \code{\link[stats]{hclust}} function from the \pkg{stats} + package. + \item \code{"ward"}: Hierarchical clustering with Ward's method (D2) + using the \code{\link[stats]{hclust}} function from the \pkg{stats} + package. + \item \code{"kmeans"}: k-means clustering using the + \code{\link[stats]{kmeans}} function from the \pkg{stats} package. + \item \code{"pam"}: Partitioning around medoids using the + \code{\link[cluster]{pam}} function from the \pkg{cluster} package. + \item \code{"spectral"}: Spectral clustering. An affinity matrix using a + Gaussian (RBF) kernel is created. The Laplacian matrix of the affinity + matrix is computed and normalized. The first first k eigenvectors of + the normalized Laplacian matrix are clustered using k-means. + \item \code{"concor"}: CONvergence of iterative CORrelations (CONCOR) + with exactly \code{k = 2} clusters. (Not included by default because of + the limit to \code{k = 2}.) + \item \code{"fastgreedy"}: Fast & greedy community detection using the + \code{\link[igraph]{cluster_fast_greedy}} function in the \pkg{igraph} + package. + \item \code{"walktrap"}: Walktrap community detection using the + \code{\link[igraph]{cluster_walktrap}} function in the \pkg{igraph} + package. + \item \code{"leading_eigen"}: Leading eigenvector community detection + using the \code{\link[igraph]{cluster_leading_eigen}} function in the + \pkg{igraph} package. (Can be slow, hence not included by default.) + \item \code{"edge_betweenness"}: Girvan-Newman edge betweenness community + detection using the \code{\link[igraph]{cluster_edge_betweenness}} + function in the \pkg{igraph} package. (Can be slow, hence not included + by default.) +}} + +\item{k.min}{For the hierarchical cluster methods, how many clusters or +states should at least be identified? Only the best solution between +\code{k.min} and \code{k.max} clusters is retained and compared to other +methods.} + +\item{k.max}{For the hierarchical cluster methods, up to how many clusters or +states should be identified? Only the best solution between \code{k.min} +and \code{k.max} clusters is retained and compared to other methods.} + +\item{splitNodes}{Split the nodes of a one-mode network (e.g., concepts in a +concept x concept congruence network) into multiple separate nodes, one for +each qualifier level? For example, if the qualifier variable is the boolean +\code{"agreement"} variable with values \code{0} or \code{1}, then each +concept is included twice, one time with suffix \code{"- 0"} and one time +with suffix \code{"- 1"}. This is useful when the nodes do not possess +agency and positive and negative levels of the variable should not be +connected through congruence ties. It helps preserve the signed nature of +the data in this case. Do not use with actor networks!} + +\item{cores}{The number of computing cores for parallel processing. If +\code{1} (the default), no parallel processing is used. If a larger number, +the \pkg{pbmcapply} package is used to parallelize the computation of the +distance matrix and the clustering. Note that this method is based on +forking and is only available on Unix operating systems, including MacOS +and Linux.} + +\item{networkType}{The kind of network to be computed. Can be +\code{"twomode"}, \code{"onemode"}, or \code{"eventlist"}.} + +\item{statementType}{The name of the statement type in which the variable +of interest is nested. For example, \code{"DNA Statement"}.} + +\item{variable1}{The first variable for network construction. In a one-mode +network, this is the variable for both the rows and columns. In a +two-mode network, this is the variable for the rows only. In an event +list, this variable is only used to check for duplicates (depending on +the setting of the \code{duplicates} argument).} + +\item{variable1Document}{A boolean value indicating whether the first +variable is at the document level (i.e., \code{"author"}, +\code{"source"}, \code{"section"}, \code{"type"}, \code{"id"}, or +\code{"title"}).} + +\item{variable2}{The second variable for network construction. In a one-mode +network, this is the variable over which the ties are created. For +example, if an organization x organization network is created, and ties +in this network indicate co-reference to a concept, then the second +variable is the \code{"concept"}. In a two-mode network, this is the +variable used for the columns of the network matrix. In an event list, +this variable is only used to check for duplicates (depending on the +setting of the \code{duplicates} argument).} + +\item{variable2Document}{A boolean value indicating whether the second +variable is at the document level (i.e., \code{"author"}, +\code{"source"}, \code{"section"}, \code{"type"}, \code{"id"}, or +\code{"title"}} + +\item{qualifier}{The qualifier variable. In a one-mode network, this + variable can be used to count only congruence or conflict ties. For + example, in an organization x organization network via common concepts, + a binary \code{"agreement"} qualifier could be used to record only ties + where both organizations have a positive stance on the concept or where + both organizations have a negative stance on the concept. With an + integer qualifier, the tie weight between the organizations would be + proportional to the similarity or distance between the two organizations + on the scale of the integer variable. With a short text variable as a + qualifier, agreement on common categorical values of the qualifier is + required, for example a tie is established (or a tie weight increased) if + two actors both refer to the same value on the second variable AND match on + the categorical qualifier, for example the type of referral. + + In a two-mode network, the qualifier variable can be used to retain only + positive or only negative statements or subtract negative from positive + mentions. All of this depends on the setting of the + \code{qualifierAggregation} argument. For event lists, the qualifier + variable is only used for filtering out duplicates (depending on the + setting of the \code{duplicates} argument. + + The qualifier can also be \code{NULL}, in which case it is ignored, meaning + that values in \code{variable1} and \code{variable2} are unconditionally + associated with each other in the network when they co-occur. This is + identical to selecting a qualifier variable and setting + \code{qualifierAggregation = "ignore"}.} + +\item{qualifierDocument}{A boolean value indicating whether the qualifier +variable is at the document level (i.e., \code{"author"}, +\code{"source"}, \code{"section"}, \code{"type"}, \code{"id"}, or +\code{"title"}} + +\item{qualifierAggregation}{The aggregation rule for the \code{qualifier} + variable. In one-mode networks, this must be \code{"ignore"} (for + ignoring the qualifier variable), \code{"congruence"} (for recording a + network tie only if both nodes have the same qualifier value in the + binary case or for recording the similarity between the two nodes on the + qualifier variable in the integer case), \code{"conflict"} (for + recording a network tie only if both nodes have a different qualifier + value in the binary case or for recording the distance between the two + nodes on the qualifier variable in the integer case), or +\code{"subtract"} (for subtracting the conflict tie value from the + congruence tie value in each dyad). In two-mode networks, this must be +\code{"ignore"}, \code{"combine"} (for creating multiplex combinations, + e.g., 1 for positive, 2 for negative, and 3 for mixed), or +\code{subtract} (for subtracting negative from positive ties). In event + lists, this setting is ignored.} + +\item{normalization}{Normalization of edge weights. Valid settings for +one-mode networks are \code{"no"} (for switching off normalization), +\code{"average"} (for average activity normalization), \code{"jaccard"} +(for Jaccard coefficient normalization), and \code{"cosine"} (for +cosine similarity normalization). Valid settings for two-mode networks +are \code{"no"}, \code{"activity"} (for activity normalization), and +\code{"prominence"} (for prominence normalization).} + +\item{duplicates}{Setting for excluding duplicate statements before network +construction. Valid settings are \code{"include"} (for including all +statements in network construction), \code{"document"} (for counting +only one identical statement per document), \code{"week"} (for counting +only one identical statement per calendar week), \code{"month"} (for +counting only one identical statement per calendar month), \code{"year"} +(for counting only one identical statement per calendar year), and +\code{"acrossrange"} (for counting only one identical statement across +the whole time range).} + +\item{start.date}{The start date for network construction in the format +\code{"dd.mm.yyyy"}. All statements before this date will be excluded.} + +\item{stop.date}{The stop date for network construction in the format +\code{"dd.mm.yyyy"}. All statements after this date will be excluded.} + +\item{start.time}{The start time for network construction on the specified +\code{start.date}. All statements before this time on the specified date +will be excluded.} + +\item{stop.time}{The stop time for network construction on the specified +\code{stop.date}. All statements after this time on the specified date +will be excluded.} + +\item{timeWindow}{Possible values are \code{"no"}, \code{"events"}, +\code{"seconds"}, \code{"minutes"}, \code{"hours"}, \code{"days"}, +\code{"weeks"}, \code{"months"}, and \code{"years"}. If \code{"no"} is +selected (= the default setting), no time window will be used. If any of +the time units is selected, a moving time window will be imposed, and +only the statements falling within the time period defined by the window +will be used to create the network. The time window will then be moved +forward by one time unit at a time, and a new network with the new time +boundaries will be created. This is repeated until the end of the overall +time span is reached. All time windows will be saved as separate +networks in a list. The duration of each time window is defined by the +\code{windowSize} argument. For example, this could be used to create a +time window of 6 months which moves forward by one month each time, thus +creating time windows that overlap by five months. If \code{"events"} is +used instead of a natural time unit, the time window will comprise +exactly as many statements as defined in the \code{windowSize} argument. +However, if the start or end statement falls on a date and time where +multiple events happen, those additional events that occur simultaneously +are included because there is no other way to decide which of the +statements should be selected. Therefore the window size is sometimes +extended when the start or end point of a time window is ambiguous in +event time.} + +\item{windowSize}{The number of time units of which a moving time window is +comprised. This can be the number of statement events, the number of days +etc., as defined in the \code{"timeWindow"} argument.} + +\item{excludeValues}{A list of named character vectors that contains entries +which should be excluded during network construction. For example, +\code{list(concept = c("A", "B"), organization = c("org A", "org B"))} +would exclude all statements containing concepts "A" or "B" or +organizations "org A" or "org B" when the network is constructed. This +is irrespective of whether these values appear in \code{variable1}, +\code{variable2}, or the \code{qualifier}. Note that only variables at +the statement level can be used here. There are separate arguments for +excluding statements nested in documents with certain meta-data.} + +\item{excludeAuthors}{A character vector of authors. If a statement is +nested in a document where one of these authors is set in the "Author" +meta-data field, the statement is excluded from network construction.} + +\item{excludeSources}{A character vector of sources. If a statement is +nested in a document where one of these sources is set in the "Source" +meta-data field, the statement is excluded from network construction.} + +\item{excludeSections}{A character vector of sections. If a statement is +nested in a document where one of these sections is set in the "Section" +meta-data field, the statement is excluded from network construction.} + +\item{excludeTypes}{A character vector of types. If a statement is +nested in a document where one of these types is set in the "Type" +meta-data field, the statement is excluded from network construction.} + +\item{invertValues}{A boolean value indicating whether the entries provided +by the \code{excludeValues} argument should be excluded from network +construction (\code{invertValues = FALSE}) or if they should be the only +values that should be included during network construction +(\code{invertValues = TRUE}).} + +\item{invertAuthors}{A boolean value indicating whether the entries provided +by the \code{excludeAuthors} argument should be excluded from network +construction (\code{invertAuthors = FALSE}) or if they should be the +only values that should be included during network construction +(\code{invertAuthors = TRUE}).} + +\item{invertSources}{A boolean value indicating whether the entries provided +by the \code{excludeSources} argument should be excluded from network +construction (\code{invertSources = FALSE}) or if they should be the +only values that should be included during network construction +(\code{invertSources = TRUE}).} + +\item{invertSections}{A boolean value indicating whether the entries +provided by the \code{excludeSections} argument should be excluded from +network construction (\code{invertSections = FALSE}) or if they should +be the only values that should be included during network construction +(\code{invertSections = TRUE}).} + +\item{invertTypes}{A boolean value indicating whether the entries provided +by the \code{excludeTypes} argument should be excluded from network +construction (\code{invertTypes = FALSE}) or if they should be the +only values that should be included during network construction +(\code{invertTypes = TRUE}).} + +\item{x}{A \code{dna_phaseTransitions} object.} + +\item{...}{Additional arguments. Currently not in use.} + +\item{object}{A \code{"dna_phaseTransitions"} object.} + +\item{plots}{The plots to include in the output list. Can be one or more of +the following: \code{"heatmap"}, \code{"silhouette"}, \code{"mds"}, +\code{"states"}.} +} +\description{ +Detect phase transitions and states in a discourse network. + +Show details of a \code{dna_phaseTransitions} object. +} +\details{ +This function applies the state dynamics methods of Masuda and Holme to a +time window discourse network. It computes temporally overlapping discourse +networks, computes the dissimilarity between all networks, and clusters them. +For the dissimilarity, the sum of absolute edge weight differences and the +Euclidean spectral distance are available. Several clustering techniques can +be applied to identify the different stages and phases from the resulting +distance matrix. + +Print a summary of a \code{dna_phaseTransitions} object, which can be created +using the \link{dna_phaseTransitions} function. +} +\examples{ +\dontrun{ +dna_init() +dna_sample() +dna_openDatabase("sample.dna", coderId = 1, coderPassword = "sample") + +# compute states and phases for sample dataset +results <- dna_phaseTransitions(distanceMethod = "absdiff", + clusterMethods = c("ward", + "pam", + "concor", + "walktrap"), + cores = 1, + k.min = 2, + k.max = 6, + networkType = "onemode", + variable1 = "organization", + variable2 = "concept", + timeWindow = "events", + windowSize = 15) +results +} + +} +\author{ +Philip Leifeld, Kristijan Garic + +Philip Leifeld +} diff --git a/rDNA/rDNA/man/dna_printDetails.Rd b/rDNA/rDNA/man/dna_printDetails.Rd index 94c6b977..a2021f9e 100644 --- a/rDNA/rDNA/man/dna_printDetails.Rd +++ b/rDNA/rDNA/man/dna_printDetails.Rd @@ -27,6 +27,7 @@ dna_printDetails() } \seealso{ Other {rDNA database connections}: +\code{\link{dna_api}()}, \code{\link{dna_closeDatabase}()}, \code{\link{dna_openConnectionProfile}()}, \code{\link{dna_openDatabase}()}, diff --git a/rDNA/rDNA/man/dna_saveConnectionProfile.Rd b/rDNA/rDNA/man/dna_saveConnectionProfile.Rd index ab0b4968..88b0aec7 100644 --- a/rDNA/rDNA/man/dna_saveConnectionProfile.Rd +++ b/rDNA/rDNA/man/dna_saveConnectionProfile.Rd @@ -44,6 +44,7 @@ dna_saveConnectionProfile(file = "my profile.dnc", coderPassword = "sample") } \seealso{ Other {rDNA database connections}: +\code{\link{dna_api}()}, \code{\link{dna_closeDatabase}()}, \code{\link{dna_openConnectionProfile}()}, \code{\link{dna_openDatabase}()}, diff --git a/rDNA/rDNA/man/print.dna_barplot.Rd b/rDNA/rDNA/man/print.dna_barplot.Rd deleted file mode 100644 index a5d33c72..00000000 --- a/rDNA/rDNA/man/print.dna_barplot.Rd +++ /dev/null @@ -1,36 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/rDNA.R -\name{print.dna_barplot} -\alias{print.dna_barplot} -\title{Print a \code{dna_barplot} object} -\usage{ -\method{print}{dna_barplot}(x, trim = 30, attr = TRUE, ...) -} -\arguments{ -\item{x}{A \code{dna_barplot} object, as returned by the -\code{\link{dna_barplot}} function.} - -\item{trim}{Number of maximum characters to display in entity labels. -Entities with more characters are truncated, and the last character is -replaced by an asterisk (\code{*}).} - -\item{attr}{Display attributes, such as the name of the variable and the -levels of the qualifier variable if available.} - -\item{...}{Additional arguments. Currently not in use.} -} -\description{ -Show details of a \code{dna_barplot} object. -} -\details{ -Print the data frame returned by the \code{\link{dna_barplot}} function. -} -\seealso{ -Other {rDNA barplots}: -\code{\link{autoplot.dna_barplot}()}, -\code{\link{dna_barplot}()} -} -\author{ -Philip Leifeld -} -\concept{{rDNA barplots}} diff --git a/rDNA/rDNA/tests/testthat/test-backbone.R b/rDNA/rDNA/tests/testthat/test-backbone.R new file mode 100644 index 00000000..290cd09b --- /dev/null +++ b/rDNA/rDNA/tests/testthat/test-backbone.R @@ -0,0 +1,260 @@ +context("backbone") + +test_that("Penalized backbone works", { + testthat::skip_on_cran() + testthat::skip_on_ci() + samp <- dna_sample() + dna_init() + dna_openDatabase(samp, coderId = 1, coderPassword = "sample") + b <- dna_backbone(method = "penalty", + penalty = 3.5, + iterations = 10000, + variable1 = "organization", + variable2 = "concept", + qualifier = "agreement", + qualifierAggregation = "subtract", + normalization = "average") + output <- capture.output(b) + expect_match(output[1], "Backbone method: penalty") + expect_length(b$backbone, 3) + expect_true(is.character(b$backbone)) + expect_length(b$redundant, 3) + expect_true(is.character(b$redundant)) + expect_true(is.numeric(b$unpenalized_backbone_loss)) + expect_length(b$unpenalized_backbone_loss, 1) + expect_true(b$unpenalized_backbone_loss > 0) + expect_true(is.numeric(b$unpenalized_redundant_loss)) + expect_length(b$unpenalized_redundant_loss, 1) + expect_true(b$unpenalized_redundant_loss > 0) + expect_true("dna_network_onemode" %in% class(b$backbone_network)) + expect_true("dna_network_onemode" %in% class(b$redundant_network)) + expect_true("dna_network_onemode" %in% class(b$full_network)) + expect_equal(b$iterations, 10000) + expect_equal(attributes(b)$method, "penalty") + expect_s3_class(b, "dna_backbone") + expect_equal(dim(b$diagnostics), c(10000, 9)) + expect_false(any(is.na(b$diagnostics))) + dna_closeDatabase() + unlink(samp) +}) + +test_that("Plot method works for backbones with penalty", { + testthat::skip_on_cran() + testthat::skip_on_ci() + samp <- dna_sample() + dna_openDatabase(samp, coderId = 1, coderPassword = "sample") + b <- dna_backbone(method = "penalty", + penalty = 3.5, + iterations = 10000, + variable1 = "organization", + variable2 = "concept", + qualifier = "agreement", + qualifierAggregation = "subtract", + normalization = "average") + expect_no_error(plot(b, ma = 500)) + expect_no_warning(plot(b, ma = 500)) + expect_no_message(plot(b, ma = 500)) + fn <- tempfile() + pdf(fn) + plot(b, ma = 500) + dev.off() + expect_true(file.exists(fn)) + file.remove(fn) + dna_closeDatabase() + unlink(samp) +}) + +test_that("Autoplot method works for backbones with penalty", { + testthat::skip_on_cran() + testthat::skip_on_ci() + samp <- dna_sample() + dna_openDatabase(samp, coderId = 1, coderPassword = "sample") + b <- dna_backbone(method = "penalty", + penalty = 3.5, + iterations = 10000, + variable1 = "organization", + variable2 = "concept", + qualifier = "agreement", + qualifierAggregation = "subtract", + normalization = "average") + skip_if_not_installed("ggplot2") + library("ggplot2") + p <- autoplot(b) + expect_true("list" %in% class(p)) + expect_length(p, 4) + expect_equal(unique(as.character(sapply(p, class))), c("gg", "ggplot")) + dna_closeDatabase() + unlink(samp) +}) + +test_that("Fixed backbone works", { + testthat::skip_on_cran() + testthat::skip_on_ci() + samp <- dna_sample() + dna_openDatabase(samp, coderId = 1, coderPassword = "sample") + b <- dna_backbone(method = "fixed", + backboneSize = 4, + iterations = 2000, + variable1 = "organization", + variable2 = "concept", + qualifier = "agreement", + qualifierAggregation = "subtract", + normalization = "average") + output <- capture.output(b) + expect_match(output[1], "Backbone method: fixed") + expect_length(b$backbone, 4) + expect_true(is.character(b$backbone)) + expect_length(b$redundant, 2) + expect_true(is.character(b$redundant)) + expect_true(is.numeric(b$unpenalized_backbone_loss)) + expect_length(b$unpenalized_backbone_loss, 1) + expect_true(b$unpenalized_backbone_loss > 0) + expect_true(is.numeric(b$unpenalized_redundant_loss)) + expect_length(b$unpenalized_redundant_loss, 1) + expect_true(b$unpenalized_redundant_loss > 0) + expect_true("dna_network_onemode" %in% class(b$backbone_network)) + expect_true("dna_network_onemode" %in% class(b$redundant_network)) + expect_true("dna_network_onemode" %in% class(b$full_network)) + expect_equal(b$iterations, 2000) + expect_equal(attributes(b)$method, "fixed") + expect_s3_class(b, "dna_backbone") + expect_equal(dim(b$diagnostics), c(2000, 9)) + expect_false(any(is.na(b$diagnostics))) + dna_closeDatabase() + unlink(samp) +}) + +test_that("Plot method works for fixed backbone size", { + testthat::skip_on_cran() + testthat::skip_on_ci() + samp <- dna_sample() + dna_openDatabase(samp, coderId = 1, coderPassword = "sample") + b <- dna_backbone(method = "fixed", + backboneSize = 4, + iterations = 2000, + variable1 = "organization", + variable2 = "concept", + qualifier = "agreement", + qualifierAggregation = "subtract", + normalization = "average") + expect_no_error(plot(b, ma = 500)) + expect_no_warning(plot(b, ma = 500)) + expect_no_message(plot(b, ma = 500)) + fn <- tempfile() + pdf(fn) + plot(b, ma = 500) + dev.off() + expect_true(file.exists(fn)) + file.remove(fn) + dna_closeDatabase() + unlink(samp) +}) + +test_that("Autoplot method works for backbones with fixed size", { + testthat::skip_on_cran() + testthat::skip_on_ci() + samp <- dna_sample() + dna_openDatabase(samp, coderId = 1, coderPassword = "sample") + b <- dna_backbone(method = "fixed", + backboneSize = 4, + iterations = 2000, + variable1 = "organization", + variable2 = "concept", + qualifier = "agreement", + qualifierAggregation = "subtract", + normalization = "average") + skip_if_not_installed("ggplot2") + library("ggplot2") + p <- autoplot(b) + expect_true("list" %in% class(p)) + expect_length(p, 4) + expect_equal(unique(as.character(sapply(p, class))), c("gg", "ggplot")) + dna_closeDatabase() + unlink(samp) +}) + +test_that("Nested backbone works", { + testthat::skip_on_cran() + testthat::skip_on_ci() + samp <- dna_sample() + dna_openDatabase(samp, coderId = 1, coderPassword = "sample") + b <- dna_backbone(method = "nested", + variable1 = "organization", + variable2 = "concept", + qualifier = "agreement", + qualifierAggregation = "subtract", + normalization = "average") + output <- capture.output(b) + expect_match(output[1], "Backbone method: nested") + expect_true(is.numeric(b$backboneLoss)) + expect_true(is.numeric(b$redundantLoss)) + expect_true(is.character(b$entity)) + expect_true(is.integer(b$i)) + expect_true(is.numeric(b$statements)) + expect_equal(dim(b), c(6, 5)) + expect_equal(colnames(b), c("i", "entity", "backboneLoss", "redundantLoss", "statements")) + expect_equal(attributes(b)$numStatementsFull, 23) + expect_equal(attributes(b)$method, "nested") + expect_s3_class(b, "dna_backbone") + expect_false(any(is.na(b))) + dna_closeDatabase() + unlink(samp) +}) + +test_that("Plot method works for nested backbone", { + testthat::skip_on_cran() + testthat::skip_on_ci() + samp <- dna_sample() + dna_openDatabase(samp, coderId = 1, coderPassword = "sample") + b <- dna_backbone(method = "nested", + variable1 = "organization", + variable2 = "concept", + qualifier = "agreement", + qualifierAggregation = "subtract", + normalization = "average") + expect_no_error(plot(b, ma = 500)) + expect_no_warning(plot(b, ma = 500)) + expect_no_message(plot(b, ma = 500)) + fn <- tempfile() + pdf(fn) + plot(b, ma = 500) + dev.off() + expect_true(file.exists(fn)) + file.remove(fn) + dna_closeDatabase() + unlink(samp) +}) + +test_that("Autoplot method works for nested backbones", { + testthat::skip_on_cran() + testthat::skip_on_ci() + samp <- dna_sample() + dna_openDatabase(samp, coderId = 1, coderPassword = "sample") + b <- dna_backbone(method = "nested", + variable1 = "organization", + variable2 = "concept", + qualifier = "agreement", + qualifierAggregation = "subtract", + normalization = "average") + skip_if_not_installed("ggplot2") + library("ggplot2") + p <- autoplot(b) + expect_equal(class(p), c("ggraph", "gg", "ggplot")) + dna_closeDatabase() + unlink(samp) +}) + + +test_that("Evaluate backbone solution works", { + testthat::skip_on_cran() + testthat::skip_on_ci() + samp <- dna_sample() + dna_openDatabase(samp, coderId = 1, coderPassword = "sample") + b <- dna_evaluateBackboneSolution( + c("There should be legislation to regulate emissions.", + "Emissions legislation should regulate CO2.") + ) + expect_length(b, 2) + dna_closeDatabase() + unlink(samp) +}) diff --git a/rDNA/rDNA/tests/testthat/test-clustering.R b/rDNA/rDNA/tests/testthat/test-clustering.R new file mode 100644 index 00000000..ad1578f2 --- /dev/null +++ b/rDNA/rDNA/tests/testthat/test-clustering.R @@ -0,0 +1,82 @@ +context("clustering") + +# example 1: compute 12 cluster solutions for one time point +test_that("Example 1 produces expected output", { + testthat::skip_on_cran() + testthat::skip_on_ci() + skip_if_not_installed("igraph", minimum_version = "0.8.1") + skip_if_not_installed("sna", minimum_version = "2.4") + skip_if_not_installed("cluster", minimum_version = "1.12.0") + samp <- dna_sample() + dna_init(samp) + dna_openDatabase(samp, coderId = 1, coderPassword = "sample") + mc1 <- dna_multiclust(variable1 = "organization", + variable2 = "concept", + qualifier = "agreement", + duplicates = "document", + k = 0, + saveObjects = TRUE) + expect_s3_class(mc1, "dna_multiclust") + expect_named(mc1, c("modularity", "max_mod", "memberships", "cl")) + expect_true(length(mc1$modularity) > 0) + expect_true(length(mc1$max_mod) > 0) + expect_true(length(mc1$memberships) > 0) + expect_true(length(mc1$cl) > 0) + dna_closeDatabase() + unlink(samp) +}) + +# example 2: compute only Girvan-Newman edge betweenness with two clusters +test_that("Example 2 produces expected output", { + testthat::skip_on_cran() + testthat::skip_on_ci() + skip_if_not_installed("igraph", minimum_version = "0.8.1") + samp <- dna_sample() + dna_init(samp) + dna_openDatabase(samp, coderId = 1, coderPassword = "sample") + set.seed(12345) + mc2 <- dna_multiclust(k = 2, + single = FALSE, + average = FALSE, + complete = FALSE, + ward = FALSE, + kmeans = FALSE, + pam = FALSE, + equivalence = FALSE, + concor_one = FALSE, + concor_two = FALSE, + louvain = FALSE, + fastgreedy = FALSE, + walktrap = FALSE, + leading_eigen = FALSE, + edge_betweenness = TRUE, + infomap = FALSE, + label_prop = FALSE, + spinglass = FALSE) + expect_s3_class(mc2, "dna_multiclust") + expect_named(mc2, c("modularity", "memberships")) + expect_true(length(mc2$modularity) > 0) + expect_true(length(mc2$memberships) > 0) + dna_closeDatabase() + unlink(samp) +}) + +# example 3: smoothed modularity using time window algorithm +test_that("Example 3 produces expected output", { + testthat::skip_on_cran() + testthat::skip_on_ci() + skip_if_not_installed("igraph", minimum_version = "0.8.1") + skip_if_not_installed("sna", minimum_version = "2.4") + skip_if_not_installed("cluster", minimum_version = "1.12.0") + samp <- dna_sample() + dna_init(samp) + dna_openDatabase(samp, coderId = 1, coderPassword = "sample") + mc3 <- dna_multiclust(k = 2, + timeWindow = "events", + windowSize = 28) + expect_s3_class(mc3, "dna_multiclust") + expect_named(mc3, c("max_mod")) + expect_true(length(mc3$max_mod) > 0) + dna_closeDatabase() + unlink(samp) +}) \ No newline at end of file diff --git a/rDNA/rDNA/tests/testthat/test-data-access.R b/rDNA/rDNA/tests/testthat/test-data-access.R index 3f17e78e..c86a438f 100644 --- a/rDNA/rDNA/tests/testthat/test-data-access.R +++ b/rDNA/rDNA/tests/testthat/test-data-access.R @@ -23,10 +23,10 @@ test_that("DNA can use databases and profiles", { expect_false(dna_openConnectionProfile(file = "profile.dnc", coderPassword = "test")) expect_output(dna_openConnectionProfile(file = "profile.dnc", coderPassword = "sample"), "DNA database: ") unlink("profile.dnc") + unlink("sample.dna") }) test_that("dna_sample works", { - unlink("sample.dna") expect_equal(dna_sample(), paste0(getwd(), "/sample.dna")) expect_true(file.exists(paste0(getwd(), "/sample.dna"))) expect_gt(file.size(paste0(getwd(), "/sample.dna")), 200000) diff --git a/rDNA/rDNA/tests/testthat/test-phasetransitions.R b/rDNA/rDNA/tests/testthat/test-phasetransitions.R new file mode 100644 index 00000000..866ef9e9 --- /dev/null +++ b/rDNA/rDNA/tests/testthat/test-phasetransitions.R @@ -0,0 +1,44 @@ +context("Testing phase transitions") + +# Create a function to set up the database for tests +setup_dna_database <- function() { + dna_init() + samp <- dna_sample() + dna_openDatabase(samp, coderId = 1, coderPassword = "sample") + return(samp) +} + +# Create a function to clean up after tests +cleanup_dna_database <- function(samp) { + dna_closeDatabase() + unlink(samp) +} + +test_that("dna_phaseTransitions produces expected output with default settings", { + testthat::skip_on_cran() + testthat::skip_on_ci() + samp <- setup_dna_database() + result <- dna_phaseTransitions() + + expect_type(result, "dna_phaseTransitions") + expect_true(!is.null(result$states)) + expect_true(!is.null(result$modularity)) + expect_true(!is.null(result$clusterMethod)) + expect_true(!is.null(result$distmat)) + + cleanup_dna_database(samp) +}) + +test_that("autoplot.dna_phaseTransitions produces expected plots", { + testthat::skip_on_cran() + testthat::skip_on_ci() + samp <- setup_dna_database() + phase_trans_obj <- dna_phaseTransitions() + + plots <- autoplot.dna_phaseTransitions(phase_trans_obj) + expect_type(plots, "list") + expect_length(plots, 4) + expect_type(plots[[1]], "ggplot") + + cleanup_dna_database(samp) +})