library(tidyverse)
gpfg_2021 <- read_csv("data/gpfg_2021.csv")
gpfg_2022 <- read_csv("data/gpfg_2022.csv")
gpfg_2023 <- read_csv("data/gpfg_2023.csv")
gpfg_2024 <- read_csv("data/gpfg_2024.csv")
gpfg_2025 <- read_csv("data/gpfg_2025.csv")5 Data Analysis
The previous chapters cleaned one annual file. We will now import several files with the same structure, combine them, learn what the resulting dataset contains, and then use it to answer reporting questions.
5.1 Learning objectives
By the end of this chapter, you should be able to:
- import several annual CSV files;
- combine tables with the same columns using
bind_rows(); - inspect the time period, categories, frequencies, and missing values;
- distinguish description, ranking, comparison, relationship, and trend questions;
- create summary tables with
group_by()andsummarise(); and - turn a calculated result into a carefully worded finding.
5.2 From one annual file to five
Chapter 4 produced gpfg_2025.csv. It describes holdings at one point in time. That is enough for a 2025 ranking, but it cannot tell us whether a value rose or fell. A trend question requires observations from several dates.
For the main analysis, we will use five consecutive annual snapshots from 2021 through 2025. Five files are enough to practise importing and combining without turning the lesson into ten repeated lines of code.
Open djr.Rproj and create 05-data-analysis.Rmd. Keep the gpfg_2025.csv created in Chapter 4. Download gpfg_2021.csv, gpfg_2022.csv, gpfg_2023.csv, and gpfg_2024.csv into the same data folder. A ready-made gpfg_2025.csv is also available for anyone beginning with this chapter.
5.3 Import the annual files
Start by loading the tidyverse and importing each CSV. The code repeats the read_csv() pattern from Chapter 3:
The object names record which annual snapshot each table contains.
Before combining files, compare their structures:
glimpse(gpfg_2021)Rows: 9,338
Columns: 11
$ year <dbl> 2021, 2021, 2021, 2021, 2021, 2021, 2021, 2021, …
$ report_date <date> 2021-12-31, 2021-12-31, 2021-12-31, 2021-12-31,…
$ region <chr> "Oceania", "Oceania", "Oceania", "Oceania", "Oce…
$ country <chr> "Australia", "Australia", "Australia", "Australi…
$ company <chr> "29Metals Ltd", "A2B Australia Ltd", "ALS Ltd", …
$ industry <chr> "Basic Materials", "Financials", "Industrials", …
$ market_value_nok <dbl> 161952543, 6362419, 482302084, 13286938, 1039279…
$ market_value_usd <dbl> 18365506, 721502, 54693318, 1506746, 11785490, 1…
$ voting_pct <dbl> 1.71, 0.62, 1.19, 0.48, 0.49, 1.60, 0.43, 1.16, …
$ ownership_pct <dbl> 1.71, 0.62, 1.19, 0.48, 0.49, 1.60, 0.43, 1.16, …
$ incorporation_country <chr> "Australia", "Australia", "Australia", "Australi…
glimpse(gpfg_2025)Rows: 7,201
Columns: 11
$ year <dbl> 2025, 2025, 2025, 2025, 2025, 2025, 2025, 2025, …
$ report_date <date> 2025-12-31, 2025-12-31, 2025-12-31, 2025-12-31,…
$ region <chr> "Oceania", "Oceania", "Oceania", "Oceania", "Oce…
$ country <chr> "Australia", "Australia", "Australia", "Australi…
$ company <chr> "3P Learning Ltd", "ALS Ltd", "AMP Ltd", "ANZ Gr…
$ industry <chr> "Consumer Discretionary", "Industrials", "Financ…
$ market_value_nok <dbl> 34140465, 1044536415, 393367522, 16360856591, 13…
$ market_value_usd <dbl> 3384651, 103554273, 38998054, 1621998602, 131849…
$ voting_pct <dbl> 3.00, 1.39, 1.27, 2.22, 1.68, 1.28, 1.17, 3.23, …
$ ownership_pct <dbl> 3.00, 1.39, 1.27, 2.22, 1.68, 1.28, 1.17, 3.23, …
$ incorporation_country <chr> "Australia", "Australia", "Australia", "Australi…
The column names and types should match. Each file already contains a year column, so the observations will remain distinguishable after combination.
5.4 Combine the annual rows
The five tables describe the same variables but different annual observations. bind_rows() places their rows underneath one another:
gpfg <- bind_rows(
gpfg_2021,
gpfg_2022,
gpfg_2023,
gpfg_2024,
gpfg_2025
)This is different from a join. bind_rows() makes the table longer by adding observations. Chapter 8 will use a join to add variables from another source by matching a shared key.
Check that every expected year appears:
gpfg |>
count(year)The result should contain one row for each year from 2021 through 2025. The record count differs by year, which is itself something to investigate before interpreting a trend.
Save the combined table so later notebooks can begin from this completed step:
write_csv(gpfg, "data/gpfg_5_years.csv")The repository also provides a ready-made gpfg_5_years.csv for learners who begin at a visualization chapter.
5.5 Know the dataset before asking questions
Before calculating a finding, learn what the table contains. This prevents us from asking a question that the data cannot answer.
What does one row represent?
glimpse(gpfg)Rows: 43,285
Columns: 11
$ year <dbl> 2021, 2021, 2021, 2021, 2021, 2021, 2021, 2021, …
$ report_date <date> 2021-12-31, 2021-12-31, 2021-12-31, 2021-12-31,…
$ region <chr> "Oceania", "Oceania", "Oceania", "Oceania", "Oce…
$ country <chr> "Australia", "Australia", "Australia", "Australi…
$ company <chr> "29Metals Ltd", "A2B Australia Ltd", "ALS Ltd", …
$ industry <chr> "Basic Materials", "Financials", "Industrials", …
$ market_value_nok <dbl> 161952543, 6362419, 482302084, 13286938, 1039279…
$ market_value_usd <dbl> 18365506, 721502, 54693318, 1506746, 11785490, 1…
$ voting_pct <dbl> 1.71, 0.62, 1.19, 0.48, 0.49, 1.60, 0.43, 1.16, …
$ ownership_pct <dbl> 1.71, 0.62, 1.19, 0.48, 0.49, 1.60, 0.43, 1.16, …
$ incorporation_country <chr> "Australia", "Australia", "Australia", "Australi…
One row represents one equity holding reported by NBIM in one year-end snapshot. The same company can appear in several years, so a row is not a unique company across the whole period. It is also not a purchase, sale, profit, or payment received by the company.
Which years are present?
The earlier count(year) result establishes both the available years and the number of records in each year. For trend work, check that the dates are consecutive and that no year is unexpectedly absent.
Which category values exist?
distinct() keeps each observed country value once. arrange() places the values in alphabetical order so they are easier to inspect:
gpfg |>
distinct(country) |>
arrange(country)The result shows the labels used by NBIM. It does not count how often they appear.
We can inspect the shorter region list in the same way:
gpfg |>
distinct(region) |>
arrange(region)How frequently does each value appear?
count(country) returns the number of holding records associated with each investment market across the five snapshots:
gpfg |>
count(country) |>
arrange(desc(n)) |>
slice_head(n = 10)desc(n) requests descending order. A high frequency means many rows, not necessarily a high total market value. Count and amount answer different questions.
Are important values missing?
Reuse the missing-value check from Chapter 4:
gpfg |>
filter(is.na(company) | is.na(market_value_nok))The | symbol means “or,” so the code keeps a row if either field is missing. No rows should appear. This does not prove the entire dataset is correct, but it confirms that these two fields are complete in the supplied annual files.
5.6 Types of questions in data journalism
Once we understand the dataset, we can ask questions it is capable of answering:
| Question type | What it asks | GPFG example |
|---|---|---|
| Trend | How did a measure change over time? | How did total reported market value change from 2021 to 2025? |
| Description | How much is there, or what is typical? | What was the median holding value in 2025? |
| Ranking | Which observations are largest or smallest? | Which individual holdings were largest in 2025? |
| Comparison | How do categories differ? | Which investment markets had the largest 2025 totals? |
| Relationship | Do two measures vary together? | Are larger holdings associated with higher ownership percentages? |
A good question names the measure, group, and period. We begin with a trend because combining the annual files has made this question possible.
5.7 Ask a trend question
How did the total reported market value of the equity holdings change from 2021 through 2025?
group_by(year) forms one group for each annual snapshot. summarise() then reduces each group to one result row:
annual_summary <- gpfg |>
group_by(year) |>
summarise(
holding_records = n(),
market_value_nok = sum(market_value_nok)
) |>
arrange(year)
annual_summaryTrack the changing row meaning:
- before
summarise(), one row represents one holding in one year; - after
summarise(), one row represents one annual snapshot.
Inside summarise(), n() counts the rows in each year group, while sum() adds the market values in that group.
The table describes changes in nominal year-end value. Those changes can reflect security prices, exchange rates, purchases, sales, and changes in coverage. They are not automatically investment returns.
5.8 Ask a descriptive question
For questions about one year, use the 2025 object imported earlier:
What were the total and typical values of individual holdings in 2025?
holding_summary <- gpfg_2025 |>
summarise(
total_market_value_nok = sum(market_value_nok),
average_market_value_nok = mean(market_value_nok),
median_market_value_nok = median(market_value_nok),
smallest_market_value_nok = min(market_value_nok),
largest_market_value_nok = max(market_value_nok)
)
holding_summaryFinancial values are often uneven. Compare the mean and median before calling either one “typical”; a small number of very large holdings can pull the mean upward.
5.9 Ask a ranking question
Which ten individual holdings had the largest reported market values in 2025?
select() keeps the columns needed for the question. arrange() sorts the rows, and slice_head() keeps the first ten:
largest_holdings <- gpfg_2025 |>
select(company, country, industry, market_value_nok) |>
arrange(desc(market_value_nok)) |>
slice_head(n = 10)
largest_holdingsA ranking identifies large observations; it does not explain why they are large. Verify important rows against the source before publication.
5.10 Ask a comparison question
Which investment markets accounted for the largest total reported market values in 2025?
country_summary <- gpfg_2025 |>
group_by(country) |>
summarise(market_value_nok = sum(market_value_nok)) |>
mutate(share_percent = market_value_nok / sum(market_value_nok) * 100) |>
arrange(desc(market_value_nok))
country_summary |>
slice_head(n = 10)One row now represents one investment market. The market with the most records is not necessarily the market with the largest total value.
5.11 Questions for visualization
Some questions are easier to examine visually:
- the annual table can become a line chart;
- the country ranking can become an ordered bar chart;
- holding values can be examined as a distribution; and
- market value and ownership percentage can be compared in a scatterplot.
Chapters 6 and 7 will reuse these questions and calculations. The chart will come after the analysis table, not replace it.
5.12 From a result to a finding
Compare these statements:
- Too broad: “The United States received the most Norwegian investment.”
- Better: “The United States had the largest reported market value in NBIM’s 2025 year-end equity-holdings file.”
The second statement matches the evidence. Explanation requires additional reporting beyond the calculated table.
5.13 Practice
- Use
distinct()to inspect the industry labels. - Use
count()to find the most frequently occurring industries across the five annual snapshots. - Calculate one row per year containing total USD market value.
- Filter the combined data to 2024 and create an industry comparison.
- Write one supported finding and one explanatory question requiring more evidence.
5.14 Takeaways
| Function or idea | What it does |
|---|---|
read_csv() |
Imports each annual CSV |
bind_rows() |
Places observations from similar tables underneath one another |
count() |
Shows category frequencies or records per year |
distinct() |
Shows the unique values present in a column |
arrange() |
Orders rows |
filter() |
Keeps rows for a selected condition or period |
group_by() + summarise() |
Produces one summary row per group |
select() |
Keeps the columns required for a question |
mutate() |
Adds a derived measure such as percentage share |
write_csv() |
Saves the combined table for later chapters |
The official dplyr reference contains additional summary and ranking tools. Add one only when a reporting question requires it.