5  Data Analysis

Part 3: 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() and summarise(); 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.

Continue in the same project

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:

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")

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_summary

Track 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_summary

Financial 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_holdings

A 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

  1. Use distinct() to inspect the industry labels.
  2. Use count() to find the most frequently occurring industries across the five annual snapshots.
  3. Calculate one row per year containing total USD market value.
  4. Filter the combined data to 2024 and create an industry comparison.
  5. 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
Want more control?

The official dplyr reference contains additional summary and ranking tools. Add one only when a reporting question requires it.