1
0
Fork
You've already forked charinet
0
A package for working with character interaction networks in R
  • R 100%
2024年10月18日 16:51:02 -03:00
man remove plotting functions from the package 2024年10月18日 16:04:12 -03:00
R remove plotting functions from the package 2024年10月18日 16:04:12 -03:00
tests remove plotting functions from the package 2024年10月18日 16:04:12 -03:00
.gitignore update examples to work with new movienetdata version 2021年08月27日 13:18:05 +01:00
.Rbuildignore remove unneeded project files 2024年10月18日 11:35:07 -03:00
DESCRIPTION remove plotting functions from the package 2024年10月18日 16:04:12 -03:00
LICENSE.md update the license info 2024年10月17日 15:59:26 -03:00
NAMESPACE remove plotting functions from the package 2024年10月18日 16:04:12 -03:00
README.md better adj example in readme 2024年10月18日 16:51:02 -03:00
README.Rmd better adj example in readme 2024年10月18日 16:51:02 -03:00

charinet

R CMD CHECK result CRAN status Codeberg badge

This package provides a handful of tools for working with character interaction network data in R.

Contents

  1. Installation
  2. Data structure
  3. Demo
  4. Get help
  5. Package checks

Below are an important note on the data structure the package is designed to work with and a full demo of the key functions. The demo will make use of the data in the movienetdata package, which contains character interaction data from a number of popular films.

If you have any questions or issues that aren’t answered by this walkthrough, please contact me or raise an issue.

Installation

You can install the latest version of charinet using your preferred method of installing R packages from git repo URLs, e.g.:

# Using the `pak` package:
pak::pkg_install("git::https://codeberg.org/pjphd/charinet.git")
# Using the `remotes` package:
remotes::install_git("https://codeberg.org/pjphd/charinet.git")

Data structure

Key to using the package is understanding the expected format of the character interaction data. Most of the package functionality is based on the understanding that character interactions are stored in a time-ordered multicast event list. This means a data structure wherein each row is a discrete interaction, with row r occuring after row r-1 and before r+1 in the narrative text. Because interactions are generally multicast (they only have one sender, but can and often do have multiple receivers), the data structure should contain a column indicating the ID of the sender (e.g. for dialogue data, this would be the speaking character), immediately followed by columns containing dummy variables for whether each character is the recipient of a given line of dialogue.

An example of this is shown below:

eventID sender Character 1 Character 2 Character 3 Character 4
1 1 0 1 0 0
2 2 1 0 0 0
3 1 0 1 1 1
4 3 1 1 0 0

In this example, there are four discrete interactions: Character 1 addresses Character 2, then Character 2 addresses Character 1, then Character 1 addresses Characters 2, 3 and 4 simultaneously, then Character 3 addresses Characters 1 and 2. The utility of this structure is that it clearly reflects the multicast nature of the events as there is only one row per event, and thus does not ask us to impose a pseudo-dyadic structure upon the interactions. For data which already exists in a dyadic format (with "from" and "to" columns for each event), the helper function dyadic_to_multicast (outlined below) converts this to the multicast format.

It is worth making this structure absolutely clear because many of the functions in the package depend on it. Many functions take an argument sender_col, which specifies the number of the column indicating sender ID numbers. In the example above, this would be sender_col = 2, as the sender ID column is the second column. The function will then expect the dummy for character 1 to be in column sender_col + 1, the dummy for character 2 to be in column sender_col + 2, etc. Where there are other event-level variables of interest (for example, a scene ID variable, or a variable containing the text of what was said in an interaction), these should therefore be included as columns prior to the column indicating the sender IDs.

Demo

library(charinet)

Loading ready-made data:

First, let’s read in the character interaction network data for Iron Man (2008).

iron_man <- movienetdata::mcu01_ironman

This data is a list containing three elements:

names(iron_man)
#> [1] "event_list" "node_list" "adjacency"

The data look like so (only the first few columns are shown here for clarity):

head(iron_man$event_list[ , 1:6])
#> eventID sceneID speakerID Tony.Stark..1. Jimmy..2. James.Rhodes..3.
#> [1,] 1 1 1 0 1 0
#> [2,] 2 1 2 1 0 0
#> [3,] 3 1 1 0 1 0
#> [4,] 4 1 14 1 0 0
#> [5,] 5 1 1 0 0 0
#> [6,] 6 1 14 1 0 0
iron_man$node_list
#> char_ID char_name char_female nlines linesin
#> 1 1 Tony Stark 0 353 368
#> 2 2 Jimmy 0 5 11
#> 3 3 James Rhodes 0 68 64
#> 4 4 Obadiah Stane 0 80 62
#> 5 5 Christine Everhart 1 25 28
#> 6 6 Happy Hogan 0 4 7
#> 7 7 JARVIS 0 38 40
#> 8 8 Pepper Potts 1 123 135
#> 9 9 General Gabriel 0 1 2
#> 10 10 Ho Yinsen 0 60 54
#> 11 11 Abu Bakaar 0 8 3
#> 12 12 Raza 0 15 12
#> 13 13 Phil Coulson 0 24 25
#> 14 14 Ramirez 1 2 5
#> 15 15 Major Allen 0 4 2
#> 16 16 William Riva 0 4 3
#> 17 17 Pratt 0 3 6
#> 18 18 Nick Fury 0 3 2

Reading in data from a PDF of a screenplay:

One of the main tools of this package is a screenplay parser function screenplay_to_events() which will extract character interactions from a PDF of a well-formatted screenplay.

This function uses regular expressions, and is thus heavily dependent on the screenplay following conventional screenplay formatting standards. Behind the scenes, it tags each line of the screenplay based on whether it matches regular expressions corresponding to the different identifiable elements of a screenplay such as scene boundaries, character names and dialogue.

Some caveats: many of these regular expressions depend on indentation, so the function won’t work on screenplays that do not use consistent indentation patterns (or which do not indent at all). The function is also likely to fail if the PDF is watermarked, as this interferes with the ability to detect text clearly in the PDF. Finally, the presence of conventional annotations such as "(V. O.)" and "(O. S.)" are used to help identify which elements are the lines corresponding to speaking character names, and so the parser may not work if there are no such annotations in the screenplay (this would be unusual but worth mentioning).

To show the function in action, we’ll use it to read in the screenplay for From Russia with Love (1963), downloaded from here.

frwl_events <- screenplay_to_events(pdf_file = path_to_frwl_pdf, 
 window = 5)

When converting the tagged screenplay into a multicast event list, the parser uses the relatively easily-identified speaking characters to populate the sender ID column. However, it is not possible to accurately capture the receiving characters of a line of dialogue from a screenplay, so the parser uses proximity to determine who the ‘receivers’ of an interaction are. The window argument specifies the size of the context window for this proximity. For a given line of dialogue t in scene s, the parser will look for any other characters who spoke within window number of lines of t within the same scene s, and assign them as recipients of line t.

Some helper functions

The package has a few helper functions for extracting useful information from the data. To make our newly extracted From Russia with Love data more analogous to the ready-made Iron Man data, we can generate a nodelist and an adjacency matrix from frwl_events.

frwl_nodes <- nodes_from_events(event_list = frwl_events, 
 sender_col = 3, 
 receivers = FALSE)

Note that sender_col is set to 3, because the sender ID column in frwl_events is the third column. The receivers = FALSE argument tells the function that our data doesn’t contain real receivers (we’re only approximating), so there’s no need to include a variable in the node list for the number of times each character is the recipient of a line of dialogue. When the input data does contain truly directed events, receivers = TRUE will add this variable to the nodelist.

# Peek at the nodelist, ranked by number of lines spoken
head(frwl_nodes[order(frwl_nodes$nlines, decreasing = TRUE), ])
#> charID char_name nlines
#> 19 19 BOND 305
#> 28 28 KERIM 103
#> 14 14 TANIA 87
#> 51 51 GRANT 71
#> 10 10 KLEB 45
#> 9 9 BLOFELD 27

And now we can aggregate the events into an adjacency matrix format:

frwl_adj <- adj_from_events(event_list = frwl_events)

Which looks like so (filtering to characters who speak more than 10 times, which helps reduce the noise picked up by the scraper):

frwl_maj_chars <- which(frwl_nodes$nlines > 10)
frwl_adj[frwl_maj_chars, frwl_maj_chars]
#> MORZENY KRONSTEEN BLOFELD KLEB TANIA BOND 'M' BOOTHROYD KERIM GRANT
#> MORZENY 0 0 0 8 6 9 0 0 0 0
#> KRONSTEEN 0 0 11 8 0 0 0 0 0 0
#> BLOFELD 0 17 0 24 1 1 0 0 0 0
#> KLEB 7 6 9 0 28 6 0 0 0 0
#> TANIA 1 0 1 15 0 71 0 0 13 1
#> BOND 3 0 4 10 123 0 12 6 83 58
#> 'M' 0 0 0 0 0 14 0 7 0 0
#> BOOTHROYD 0 0 0 0 0 11 9 0 0 0
#> KERIM 0 0 0 0 14 98 0 0 0 0
#> GRANT 0 0 0 0 3 71 0 0 0 0

Activity heatmaps

The package contains a function activity_heatmap() for calculating character-by-story-chunk activity heatmaps. Story chunks can be any division of the event list into chunks - either by pre-defined divisions such as scenes or chapters, or by arbitrary division into a number of evenly-sized chunks.

First, we’ll generate a heatmap for Iron Man which looks at character activity (lines spoken) in each scene.

iron_man_hm <- activity_heatmap(event_list = iron_man$event_list, 
 char_names = iron_man$node_list$char_name, 
 story_chunk_col = 2, 
 sender_col = 3)

Here, we specified that the column containing the story chunk IDs (in our case the "sceneID" column) is the second column, and the speaker IDs are in the third column.

To illustrate another approach, we can generate a heatmap for From Russia with Love which divides the narrative into 10 chunks.

frwl_hm <- activity_heatmap(event_list = frwl_events, 
 n_chunks = 10, 
 sender_col = 3)

Narrative centrality

Finally, the package implements the narrative centrality measure for character interaction network data introduced in Jones, Quinn and Koskinen (2020). The measure can calculate the speaking or spoken-to centrality scores of network actors in a time-ordered multicast event list. In either case, all characters begin with the same non-zero score. In the speaking mode, when a character speaks, their score increases by a fraction of the score of the character they speak to (when addressing multiple recipients, the speaker’s score increases by a fraction of the mean of the addressees’ scores). In the spoken-to mode, when a character is spoken to, their score increases by a fraction of the score of the character speaking to them.

The package implements the measure like so:

iron_man_scores <- narrative_centrality(event_list = iron_man$event_list, 
 char_names = iron_man$node_list$char_name,
 mode = "both", 
 wp = 0.01,
 normalised = TRUE, 
 sender_col = 3)

Here mode is used to specify either "in", "out", or "both". For mode = "in", only spoken-to scores are returned; for mode = "out", only speaking scores are returned; for mode = "both", a list containing both is returned.

names(iron_man_scores)
#> [1] "out_scores" "in_scores"

The wp argument is used to specify the fraction used when increasing character scores. The higher the value, the more sensitive the measure is as characters can overtake other characters’ scores after fewer interactions. The default value of 0.01 means that when characters interact, their scores increase by 1% of the score of those with whom they interact.

Finally, the character scores are normalised (normalised = TRUE) by dividing each characters’ score at time t by the sum of all character scores at time t, such that scores become each character’s relative share of the overall centrality.

Reshaping event lists between dyadic and multicast formats

As noted earlier, the package expects a specific format that is not commonly found in the wild. Thus, the package provides two more helper functions for converting between dyadic and multicast event lists. In the dyadic format, each row is a dyadic interaction, with a "from" (sender) column and a "to" (receiver) column. There may also be a "time" or "event" column which indexes the sequence of events.

Sometimes, it may be useful to convert the multicast event list to this dyadic format, for example so that it can be used with another package’s functions which expect an edge-list style data structure.

This can be done like so:

iron_man_dyadic <- multicast_to_dyadic(iron_man$event_list, 
 sender_col = 3, 
 force_seq = TRUE)
head(iron_man_dyadic)
#> time eventID sceneID from to
#> [1,] 1 1 1 1 2
#> [2,] 2 1 1 1 14
#> [3,] 3 1 1 1 17
#> [4,] 4 2 1 2 1
#> [5,] 5 3 1 1 2
#> [6,] 6 4 1 14 1

Note that the force_seq argument, if set to TRUE, will force events with multiple receivers into a false sequence, so that all events in the dyadic event list have a different event ID. In the Iron Man example above, this can be seen in the difference between the time variable and the eventID variable - the time variable computed by the function treats every dyadic event as happening subsequent to the previous one. When force_seq = FALSE, these multicast events will still be split into the dyadic components, but the computed time variable will indicate that these dyadic events are simultaneous (in the above example the time variable would be identical to the eventID variable).

The more common case would be to convert this kind of dyadic format into the multicast format expected by charinet. This is done like so:

iron_man_multicast <- dyadic_to_multicast(iron_man_dyadic, 
 sender_col = 4, 
 receiver_col = 5, 
 time_col = 2, 
 char_names = iron_man$node_list$char_name)
iron_man_multicast[1:6, 1:6]
#> eventID from Tony Stark Jimmy James Rhodes Obadiah Stane
#> [1,] 1 1 0 1 0 0
#> [2,] 2 2 1 0 0 0
#> [3,] 3 1 0 1 0 0
#> [4,] 4 14 1 0 0 0
#> [5,] 5 1 0 0 0 0
#> [6,] 6 14 1 0 0 0

Note that we specify both the "from" (sender_col) and "to" (receiver_col) column indices, as well as a time_col indicating the column to be used to collect the timing of events. In this case, we specified eventID (column 2), because this is the column that captures the fact that some of the dyadic interactions are simultaneous (they are actually part of a multicast interaction). If we had specified time_col = 1, every event in the multicast event list would have exactly 1 sender and 1 receiver, preserving the false sequence we forced when creating iron_man_dyadic. When a time_col is specified, the function will collect these dyadic interactions spread across multiple rows in the dyadic event list and place them in the same row in the multicast event list. Thus, iron_man_multicast is equivalent to the iron_man$event_list we started with:

identical(rowSums(iron_man_multicast[ , 2:ncol(iron_man_multicast)]), 
 rowSums(iron_man$event_list[ , 3:ncol(iron_man$event_list)]))
#> [1] TRUE

Get help

For more on using the package, see the documentation:

?charinet

All the functions are also documented, so you should be able to find help for how to use everything; otherwise, get in touch.

Package checks

Note that the following checks are run in R on my local machine when the README is built and their results embedded here via rmarkdown and knitr.

Cloud computing has real environmental costs and using CI for simple checks like these on small packages is a waste of resources in my opinion. Instead, I’m just rebuilding the README before each push to keep the results of the checks up-to-date. Every little helps!

Code coverage:

covr::package_coverage()
#> charinet Coverage: 76.88%
#> R/heatmap.R: 51.16%
#> R/helpers.R: 62.07%
#> R/narrative_centrality.R: 70.45%
#> R/read_data.R: 90.91%
#> R/parse_screenplay.R: 95.97%

R CMD CHECK results:

devtools::check(quiet = TRUE)
#> i Loading charinet
#> ── R CMD check results ──────────────────────────────── charinet 0.0.2.9009 ────
#> Duration: 11s
#> 
#> 0 errors ✔ | 0 warnings ✔ | 0 notes ✔

These checks were run in the following environment:

cbind("Session information" = sessioninfo::platform_info()[c(
 "version", "system", "os", "date"
)])
#> Session information 
#> version "R version 4.4.1 (2024年06月14日)"
#> system "x86_64, linux-gnu" 
#> os "Fedora Linux 40 (KDE Plasma)"
#> date "2024年10月18日"

References

Jones, P., E. Quinn and J. Koskinen. 2020. "Measuring centrality in film narratives using dynamic character interaction networks." Social Networks 63: 21-37. DOI: 10.1016/j.socnet.202003003.