Tutorial #1. Data Management and Data Wrangling
Introduction to programming for data analysis
M1 Economics / Intro to Programming / tutorial-1
project-m1/
data/
input/ data_choices.csv
scripts/
Disclaimer
This HTML page was generated with Claude Sonnet 5 based on exercises we developed from a LaTeX document.
If you would prefer to work with a more traditional PDF version, you can download it here: Download the PDF.
Part of the content was created by Pierre Michel (AMSE) and Morgan Raux (AMSE) who kindly shared their work.
💡 About the code chunks in this document
The code chunks in Part 1 describe a fictional project folder (project-m1) that a classmate sent you — they illustrate path concepts but are not meant to run on your own machine, so they are shown as non-executed chunks. From Part 2 onward, the chunks are real, executable R chunks: as long as you follow the tutorial’s setup instructions (creating the folders and downloading the described files), they will run exactly as written.
Part 1 — Mastering absolute and relative file paths in R
You have just received a project directory named project-m1 from one of your classmates for an econometrics and quantitative finance assignment. This directory contains all the folders necessary to analyze investment choices using statistical models.
Reminder — Absolute vs. Relative Paths
- Absolute Path: the complete address of a file starting from the root of the operating system (e.g.,
C:/Users/Name/Documents/...or/home/user/...). It is rigid and works only on your specific machine. - Relative Path: the address of a file relative to your current working directory. It uses the single dot
.(current folder) and the double dot..(move up to the parent folder). This is the key to reproducibility.
The complete directory tree looks like this:
project-m1/
data/
input/
data_choices.csv
df_train.csv
df_validation.csv
output/
my_nn.rda
references/
Peterson_et_al_2021_Science.pdf
scripts/
project_utility.Rproj
r/
neural_network.R
ebook/
prospec_utility.qmd
Q1. The starting point.
Scenario. You launch RStudio by double-clicking on the project file project_utility.Rproj located in the scripts folder from your file explorer. RStudio automatically sets your working directory to the folder containing this .Rproj file.
You open a brand new, empty script. You want to import the contents of the data_choices.csv file.
- What is your current Working Directory?
- Provide an R command using the
readrpackage to load this file into an object namedlotteries.
Remember: opening an .Rproj file sets the working directory to the folder that contains it — not the folder of whatever script you open afterward.
data_choices.csv sits in exactly the same folder as the .Rproj file. So the relative path is… just the filename.
The current working directory is the scripts/ folder, because opening an .Rproj file defaults the root directory to its parent folder. Since the data file is located in the exact same folder as the project file, the relative path is direct:
Q2. Moving up and down.
Scenario. Still in the same RStudio session (your working directory has not changed), you now want to import the training data file named df_train.csv.
Provide the R command to load this file and store the result in an object named df_train.
From scripts/, you first need to go up one level to exit scripts and land in project-m1: ..
Then enter the data folder (../data) and the input subfolder (../data/input) to target the file.
Q3. The deep directory trap.
Scenario. You open the script neural_network.R located deep inside the scripts/r/ subfolder. Note that your RStudio session is still actively attached to the project_utility.Rproj file opened in Question 1.
You want to save your final model into the data/output/ folder under the name my_nn.rda. What relative path structure should you pass to the save(..., file = ...) function? Justify your choice.
Classic trap: does the working directory change when you open a script that lives deeper in the folder tree?
No — as long as your RStudio session is tied to an .Rproj file, the working directory never changes. It stays locked onto scripts/, regardless of which script you’re editing or where it’s stored.
Q4. Reproducibility.
A teammate sends you their script to read the bibliographic reference file. Inside, you spot the following line of code:
- What type of file path is this?
- Why will this code inevitably throw an error when you run it on your machine?
- Provide a clean, collaborative fix using a relative path instead.
Look at the prefix C:/ — what does it correspond to, and does that folder exist on your own machine?
It’s an absolute path tied to a specific user’s environment. To fix it, start again from your working directory (scripts/) and go up into references/.
Part 2 — Does money bring happiness?
This exercise introduces a study of the relationship between life satisfaction and standard of living across countries. The analysis is conducted at the national level, using self-reported measures of overall life satisfaction and per capita GDP as a proxy for standard of living. The central hypothesis is that higher per capita GDP is positively associated with higher levels of life satisfaction.
Objectives of the exercise
- Retrieve the data to test the hypothesis from Eurostat.
- Prepare the data for further analysis in R.
Setup. Prepare a project for the first tutorial (see the recap if you need a refresher on how to do so). Adopt a tutorial-1/ root containing data/{raw,tmp,out}, scripts/, figs/, tables/, references/, a README.md and a project.Rproj. Working within an RStudio project sets your active working directory to the project’s root folder (tutorial-1/).
Q1. Import the per capita GDP dataset in R (the “Real GDP per capita” dataset, ref: sdg_08_10, downloaded as data/raw/sdg_08_10.csv). Adapt the following instruction:
You are working from the project root directory (tutorial-1/) — navigate down into the subfolders.
Rows: 1027 Columns: 19
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (13): STRUCTURE, STRUCTURE_ID, STRUCTURE_NAME, freq, Time frequency, uni...
dbl (2): TIME_PERIOD, OBS_VALUE
lgl (4): Time, Observation value, CONF_STATUS, Confidentiality status (flag)
ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
# A tibble: 1,027 × 19
STRUCTURE STRUCTURE_ID STRUCTURE_NAME freq `Time frequency` unit
<chr> <chr> <chr> <chr> <chr> <chr>
1 dataflow ESTAT:SDG_08_10(1.0) Real GDP per cap… A Annual CLV2…
2 dataflow ESTAT:SDG_08_10(1.0) Real GDP per cap… A Annual CLV2…
3 dataflow ESTAT:SDG_08_10(1.0) Real GDP per cap… A Annual CLV2…
4 dataflow ESTAT:SDG_08_10(1.0) Real GDP per cap… A Annual CLV2…
5 dataflow ESTAT:SDG_08_10(1.0) Real GDP per cap… A Annual CLV2…
6 dataflow ESTAT:SDG_08_10(1.0) Real GDP per cap… A Annual CLV2…
7 dataflow ESTAT:SDG_08_10(1.0) Real GDP per cap… A Annual CLV2…
8 dataflow ESTAT:SDG_08_10(1.0) Real GDP per cap… A Annual CLV2…
9 dataflow ESTAT:SDG_08_10(1.0) Real GDP per cap… A Annual CLV2…
10 dataflow ESTAT:SDG_08_10(1.0) Real GDP per cap… A Annual CLV2…
# ℹ 1,017 more rows
# ℹ 13 more variables: `Unit of measure` <chr>, na_item <chr>,
# `National accounts indicator (ESA 2010)` <chr>, geo <chr>,
# `Geopolitical entity (reporting)` <chr>, TIME_PERIOD <dbl>, Time <lgl>,
# OBS_VALUE <dbl>, `Observation value` <lgl>, OBS_FLAG <chr>,
# `Observation status (Flag) V2 structure` <chr>, CONF_STATUS <lgl>,
# `Confidentiality status (flag)` <lgl>
Q2. What does the following instruction do?
The View() function opens a spreadsheet-style data viewer inside RStudio. This allows you to visually inspect the rows and columns of your data frame natively. Be careful: the number of displayed columns is limited to 50; click the little arrows at the top of the viewer pane to see the next 50.
Q3. Which column(s) of gdp can be used to identify a unique observation?
Think of a Eurostat panel: one spatial dimension, one temporal dimension.
Typically, Eurostat panels use a combination of the spatial identifier column (geo) and the temporal identifier column (TIME_PERIOD) to form a unique composite primary key per observation row.
Q4. Import the life satisfaction dataset in R (the “Overall life satisfaction by sex, age and educational attainment” dataset, ref: ilc_pw01, saved as data/raw/ilc_pw01.csv). Adapt the following instruction:
Rows: 248 Columns: 27
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (20): STRUCTURE, STRUCTURE_ID, STRUCTURE_NAME, freq, Time frequency, sta...
dbl (2): TIME_PERIOD, OBS_VALUE
lgl (5): sex, Time, Observation value, CONF_STATUS, Confidentiality status ...
ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
# A tibble: 248 × 27
STRUCTURE STRUCTURE_ID STRUCTURE_NAME freq `Time frequency` statinfo
<chr> <chr> <chr> <chr> <chr> <chr>
1 dataflow ESTAT:ILC_PW01$DEFA… Overall life … A Annual AVG
2 dataflow ESTAT:ILC_PW01$DEFA… Overall life … A Annual AVG
3 dataflow ESTAT:ILC_PW01$DEFA… Overall life … A Annual AVG
4 dataflow ESTAT:ILC_PW01$DEFA… Overall life … A Annual AVG
5 dataflow ESTAT:ILC_PW01$DEFA… Overall life … A Annual AVG
6 dataflow ESTAT:ILC_PW01$DEFA… Overall life … A Annual AVG
7 dataflow ESTAT:ILC_PW01$DEFA… Overall life … A Annual AVG
8 dataflow ESTAT:ILC_PW01$DEFA… Overall life … A Annual AVG
9 dataflow ESTAT:ILC_PW01$DEFA… Overall life … A Annual AVG
10 dataflow ESTAT:ILC_PW01$DEFA… Overall life … A Annual AVG
# ℹ 238 more rows
# ℹ 21 more variables: `Statistical information` <chr>, unit <chr>,
# `Unit of measure` <chr>, isced11 <chr>,
# `International Standard Classification of Education (ISCED 2011)` <chr>,
# life_sat <chr>, `Life satisfaction` <chr>, sex <lgl>, Sex <chr>, age <chr>,
# `Age class` <chr>, geo <chr>, `Geopolitical entity (reporting)` <chr>,
# TIME_PERIOD <dbl>, Time <lgl>, OBS_VALUE <dbl>, …
Q5. What does the following instruction do?
It subsets the data frame, keeping only three columns: geographic region (geo), time period (TIME_PERIOD), and the target observation values (OBS_VALUE). All other columns are dropped.
Q6. What does the following instruction do?
It renames the explicit dataset columns to clean up feature definitions (geo becomes country, TIME_PERIOD becomes year, and OBS_VALUE becomes life_satisf) to make future analysis code clearer.
life_satisf <- life_satisf |>
rename(
country = geo,
year = TIME_PERIOD,
life_satisf = OBS_VALUE
)
life_satisf# A tibble: 248 × 3
country year life_satisf
<chr> <dbl> <dbl>
1 AL 2018 5.5
2 AL 2021 5.7
3 AL 2022 6
4 AL 2023 6.3
5 AT 2013 7.8
6 AT 2018 8
7 AT 2021 8
8 AT 2022 7.9
9 AT 2023 7.7
10 AT 2024 7.6
# ℹ 238 more rows
Q7. What does the following code do?
Q8. What does the following code do?
This represents a pipeline sequence for the gdp object: it focuses on three columns (dropping irrelevant attributes) via select(), cleans up their names using rename(), and exports the resulting data frame to disk at data/out/gdp.csv.
gdp <- gdp |>
select(geo, TIME_PERIOD, OBS_VALUE) |>
rename(
country = geo,
year = TIME_PERIOD,
gdp = OBS_VALUE
) |>
write_csv(file = "data/out/gdp.csv")
gdp# A tibble: 1,027 × 3
country year gdp
<chr> <dbl> <dbl>
1 AL 2000 2120
2 AL 2001 2330
3 AL 2002 2440
4 AL 2003 2580
5 AL 2004 2730
6 AL 2005 2880
7 AL 2006 3080
8 AL 2007 3300
9 AL 2008 3550
10 AL 2009 3680
# ℹ 1,017 more rows
Q9. If you look closely at the country column of gdp, you will notice that some values do not correspond to a single country, but rather to a group of countries (e.g., the whole European Union, or the Euro area). Identify these aggregated observations, and remove them from gdp so that only individual countries remain.
Use distinct(country) to list the unique values taken by country. Among them, you should spot codes such as EU27_2020 or EA20: these are Eurostat’s codes for the European Union (27 members, as of 2020) and the Euro area (20 members), not for individual countries.
Individual countries are always identified by a two-letter ISO code (e.g., FR, DE, IT). Aggregated groups of countries have longer codes. The base R function nchar() returns the number of characters in a character string — combine it with filter().
# A tibble: 41 × 1
country
<chr>
1 AL
2 AT
3 BA
4 BE
5 BG
6 CH
7 CY
8 CZ
9 DE
10 DK
11 EA19
12 EA20
13 EA21
14 EE
15 EL
16 ES
17 EU27_2020
18 FI
19 FR
20 HR
21 HU
22 IE
23 IS
24 IT
25 LT
26 LU
27 LV
28 MD
29 ME
30 MK
31 MT
32 NL
33 NO
34 PL
35 PT
36 RO
37 RS
38 SE
39 SI
40 SK
41 TR
# Individual countries have a 2-character ISO code;
# groups of countries (e.g. EU27_2020, EA20) have longer codes.
gdp <- gdp |>
filter(nchar(country) == 2)
gdp# A tibble: 923 × 3
country year gdp
<chr> <dbl> <dbl>
1 AL 2000 2120
2 AL 2001 2330
3 AL 2002 2440
4 AL 2003 2580
5 AL 2004 2730
6 AL 2005 2880
7 AL 2006 3080
8 AL 2007 3300
9 AL 2008 3550
10 AL 2009 3680
# ℹ 913 more rows
Q10. In the dataset gdp, keep only the rows where the year is 2023. Then, sort these rows so that countries with higher values of per capita GDP appear first (i.e., arrange in descending order).
Use filter() for the condition, and arrange(desc(...)) to sort in descending order.
Q11. In the dataset gdp, keep only the rows where the country is France.
Q12. Adapt the following instruction to join the two datasets gdp and lifesat, using the inner_join() function.
Join on the common relational primary keys shared by both tables.
# A tibble: 231 × 4
country year gdp life_satisf
<chr> <dbl> <dbl> <dbl>
1 AL 2018 4730 5.5
2 AL 2021 5190 5.7
3 AL 2022 5500 6
4 AT 2013 43080 7.8
5 AT 2018 45140 8
6 AT 2021 44580 8
7 AT 2022 46430 7.9
8 AT 2023 45670 7.7
9 AT 2024 45140 7.6
10 AT 2025 45390 7.7
# ℹ 221 more rows
Q13. In the dataset combined_data, group the rows by country using group_by(). Then, for each country, compute the average per capita GDP and the average life satisfaction using summarise() and mean(). Lastly, create a new column that is a dummy variable: it should take the value 1 if the country’s average per capita GDP is greater than $35,000, and 0 otherwise. Use mutate() with ifelse() for this step.
group_by(country), then summarise() with two calls to mean(..., na.rm = TRUE).
For the dummy variable, use mutate() with ifelse(condition, 1, 0).
summary_data <- combined_data |>
group_by(country) |>
summarise(
mean_gdp = mean(gdp, na.rm = TRUE),
mean_sat = mean(life_satisf, na.rm = TRUE)
) |>
mutate(high_gdp_dummy = ifelse(mean_gdp > 35000, 1, 0))
summary_data# A tibble: 35 × 4
country mean_gdp mean_sat high_gdp_dummy
<chr> <dbl> <dbl> <dbl>
1 AL 5140 5.73 0
2 AT 45061. 7.81 1
3 BE 42960 7.59 1
4 BG 10250 5.7 0
5 CH 80392. 7.9 1
6 CY 27323. 7.04 0
7 CZ 21310 7.31 0
8 DE 43381. 7.03 1
9 DK 56116. 7.67 1
10 EE 21277. 7.07 0
# ℹ 25 more rows
Q14. Export in a CSV file the dataset combined_data in your out data folder. Name the file gdp-lifesat.csv.
Q15. If you replace inner_join() with left_join() in Q11, what would change? Explain the behavior of these two functions.
Think about what happens to rows that have no match in the other table.
An inner_join() returns only rows where a matching key is found in both datasets. If a country/year pair is missing in either dataset, it disappears. A left_join() retains all entries from the primary reference frame on the left-hand side (gdp). If matching indices are absent on the right-hand side (life_satisf), the metrics are filled with NA without dropping rows. See the {dplyr} cheat sheet for more details.
Q16. Download an additional dataset from Eurostat. For example, the unemployment rate (ref teilm020). Add the values to the combined_data frame.
Assuming the file has been processed similarly and matches our clean structure schema:
Part 3 — Introduction to conditional expressions
In programming, conditional expressions allow your code to make decisions. They execute different blocks of instructions depending on whether a specific condition evaluates to TRUE or FALSE. In this section, we explore the two primary ways to handle conditions in R: the ifelse() function — ideal for creating new columns inside a data frame (a vectorized approach) — and the standard if ... else statement, used to control the flow of a script or custom functions (a procedural approach).
💡 Reminder — Logical Operators (Boolean Calculus)
Let A and B represent logical statements or conditions. R evaluates each statement to see if it is true for a given row or observation, returning either TRUE or FALSE.
For example, statement A could be “Is the country France?” (country == "FR"), and statement B could be “Is the year 2023?” (year == 2023).
We can combine or invert these statements using three core operators:
- AND (
&): returns TRUE only if both statements are true simultaneously. - OR (
|): returns TRUE if at least one of the statements is true. - NOT (
!): inverts the statement (TRUE becomes FALSE, and vice versa).
Quick reference truth table
| A | B | A & B (AND) | A | B (OR) | !A (NOT A) |
|---|---|---|---|---|
| TRUE | TRUE | TRUE | TRUE | FALSE |
| TRUE | FALSE | FALSE | TRUE | FALSE |
| FALSE | TRUE | FALSE | TRUE | TRUE |
| FALSE | FALSE | FALSE | FALSE | TRUE |
Examples in data filtering
💡 Advanced Logical Operators and Set Membership
When datasets grow, writing multiple conditions using & and | can become difficult. We can use set membership operators and logical distribution rules to simplify our code.
The matching operator: %in%
The %in% operator tests if an element matches any value within a specified group or vector. It acts as an efficient shorthand for multiple OR statements. For example, instead of joining “is the country France?” and “is the country Italy?” with an OR operator, you can check if the country belongs to the set c("FR", "IT").
Distributing NOT across conditions: De Morgan’s Laws
When you apply a ! (NOT) operator to an expression inside brackets, the internal logical operators flip:
!(A & B)is equivalent to!A | !B(“not both” means it is either not A, or not B).!(A | B)is equivalent to!A & !B(“not either” means it is not A, and simultaneously not B).
Examples in data filtering
# --- Using %in% instead of multiple OR lines ---
# Long way:
tb |> filter(country == "FR" | country == "IT" | country == "DE")
# Short, elegant way:
tb |> filter(country %in% c("FR", "IT", "DE"))
# --- Excluding an entire group (Not In) ---
tb |> filter(!(country %in% c("FR", "IT", "DE")))
# --- Understanding bracket negation ---
# Exclude rows where it is France in the year 2023
tb |> filter(!(country == "FR" & year == 2023))
# ...equivalent to:
tb |> filter(country != "FR" | year != 2023)
# Exclude rows that are either France OR Italy
tb |> filter(!(country == "FR" | country == "IT"))
# ...equivalent to:
tb |> filter(country != "FR" & country != "IT")Q17. The vectorized ifelse() function.
We want to classify observations into two broad categories based on their reported life satisfaction. If the score in the life_satisf column is strictly greater than 7, the new row variable should display "High". Otherwise, it should display "Moderate or Low".
Complete the following R code to create this new categorical variable named happiness_status:
ifelse(condition, value_if_true, value_if_false) works line-by-line across vectors or columns — fill in the three arguments in order.
This continues our example from Part 2 — tb below is our combined_data object, renamed for consistency with Tutorials #2 and #3:
tb <- combined_data
tb <- tb |>
mutate(
happiness_status = ifelse(
life_satisf > 7, "High", "Moderate or Low"
)
)
tb |> select(country, year, life_satisf, happiness_status)# A tibble: 231 × 4
country year life_satisf happiness_status
<chr> <dbl> <dbl> <chr>
1 AL 2018 5.5 Moderate or Low
2 AL 2021 5.7 Moderate or Low
3 AL 2022 6 Moderate or Low
4 AT 2013 7.8 High
5 AT 2018 8 High
6 AT 2021 8 High
7 AT 2022 7.9 High
8 AT 2023 7.7 High
9 AT 2024 7.6 High
10 AT 2025 7.7 High
# ℹ 221 more rows
Q18. The flow-control if / else block.
Imagine you are writing an automated data pipeline script. You want R to print a warning message directly in the console if the overall mean per capita GDP of your clean sample dataset surpasses a specific baseline threshold (e.g., $40,000).
Analyze the code script below and explain exactly what message will be printed to the console if the variable mean_gdp happens to be exactly 42,500:
Is 42,500 strictly greater than 40,000?
Since the value stored in mean_gdp (42,500) is strictly greater than 40,000, the scalar condition evaluates to TRUE. R immediately executes the first code block and prints:
mean_gdp <- 42500
if (mean_gdp > 40000) {
print("Warning: Global average living standards are high.")
} else {
print("Global average living standards remain below the critical threshold.")
}[1] "Warning: Global average living standards are high."
The secondary fallback instruction path following else is completely bypassed.
Q19. A typical error.
A student attempts to alter individual row values inside their data frame using a standard procedural if block like this:
R executes this but throws a clear warning message in the console stating: “the condition has length > 1 and only the first element will be used”.
Explain conceptually why standard if blocks fail when applied to complete data frame tables, and why using a vectorized tool like ifelse() from Q16 is mandatory here.
Does tb$life_satisf > 7 return a single logical value, or several?
The standard procedural if (condition) statement in base R is engineered to evaluate only a single, individual TRUE/FALSE item (a scalar value) to choose a track for the whole script. However, tb$life_satisf > 7 does not return one single answer: it creates a vector containing many TRUE/FALSE elements (one for every country/year row). The procedural if statement looks only at the very first row element to decide, and ignores the rest of the dataset. To mutate data frames row-by-row based on attributes, we must always use a vectorized alternative like ifelse() or dplyr::case_when().
Below, we reproduce the exact message R raises if you try to run this code as written (using tryCatch() only so this demonstration doesn’t stop the rendering of the document — depending on your R version, this shows up as a warning or, in recent versions of R, as a hard error):
Part 4 — Solving a quadratic equation with an R function
Objective
The objective of this exercise is to write an R function that solves a quadratic equation of the form
\[ ax^2 + bx + c = 0, \]
where \(a\), \(b\), and \(c\) are real numbers and \(a \neq 0\).
The final objective is to create a function that takes a, b, and c as mandatory arguments and returns the real roots of the polynomial. We will also add optional arguments to make the function more flexible.
💡 Style guide
When writing functions, it is good practice to follow a consistent style guide. A common coding style improves readability, makes code easier to review and maintain, and helps others understand your work more quickly. In the R community, the Tidyverse Style Guide is a widely adopted convention that provides recommendations for function names, argument formatting, indentation, comments, and other aspects of code organization.
Q1. Reminders. Recall the discriminant of a quadratic polynomial. Write its formula in terms of \(a\), \(b\), and \(c\). Then explain how the sign of the discriminant determines the number of real roots.
The discriminant of the quadratic polynomial
\[ ax^2 + bx + c = 0 \]
is
\[ \Delta = b^2 - 4ac. \]
The sign of \(\Delta\) determines the number of real roots:
- If \(\Delta > 0\), the polynomial has two distinct real roots.
- If \(\Delta = 0\), the polynomial has one repeated real root.
- If \(\Delta < 0\), the polynomial has no real root.
When \(\Delta \geq 0\), the real roots are given by
\[ x_1 = \frac{-b - \sqrt{\Delta}}{2a} \]
and
\[ x_2 = \frac{-b + \sqrt{\Delta}}{2a}. \]
If \(\Delta = 0\), these two formulas give the same root.
Q2. A first function. Write an R function called quadratic_discriminant. This function should take three mandatory numeric arguments, a, b, and c, and return the discriminant of the polynomial.
The body of the function only needs one line: assign b^2 - 4 * a * c to a variable, then return that variable (or simply let it be the last evaluated expression).
A possible solution is:
#' Discriminant of a quadratic polynomial
#'
#' @param a The coefficient of \eqn{x^2}, \eqn{a \ne 0}.
#' @param b The linear coefficent.
#' @param c The constant term.
#'
#' @description Computes the discriminant of the quadratic polynomial
#' \deqn{ax^2 + bx + c = 0,}
#' i.e., \deqn{\Delta = b^2 - 4ac.}
#' @returns The quadratic discriminant.
quadratic_discriminant <- function(a, b, c) {
delta <- b^2 - 4 * a * c
delta
}For example:
The arguments a, b, and c are mandatory because the discriminant cannot be computed without the three coefficients of the polynomial.
Q3. Solve a quadratic equation. Write an R function called solve_quadratic. This function should take three mandatory arguments, a, b, and c. It should return:
- two real roots if \(\Delta > 0\);
- one real root if \(\Delta = 0\);
NAif \(\Delta < 0\).
The function should also check that \(a \neq 0\). If \(a = 0\), the function should stop and return an error message.
Use stop("your message") as the very first instruction inside an if (a == 0) { ... } block, before computing anything else.
Reuse the discriminant formula from Q2, then branch with if (delta > 0) { ... } else if (delta == 0) { ... } else { ... }, returning the appropriate value with return() in each branch.
A possible solution is:
#' Compute the real roots of a quadratic equation
#'
#' @param a The coefficient of \eqn{x^2}, \eqn{a \ne 0}.
#' @param b The linear coefficient.
#' @param c The constant term.
#'
#' @description Solve a quadratic equation of the form
#' \deqn{ax^2 + bx + c = 0,}
#' where \eqn{a \ne 0}. Complex solutions are ignored in this function.
#' @returns Depending on the discriminant \eqn{\Delta}, returns:
#' * If \eqn{\Delta > 0}: the two real roots,
#' * If \eqn{\Delta = 0}: the real root,
#' * If \eqn{\Delta < 0}: returns `NA`.
#' @md
solve_quadratic <- function(a, b, c) {
if (a == 0) {
stop("The coefficient a must be different from 0.")
}
delta <- b^2 - 4 * a * c
if (delta > 0) {
x1 <- (-b - sqrt(delta)) / (2 * a)
x2 <- (-b + sqrt(delta)) / (2 * a)
return(c(x1, x2))
} else if (delta == 0) {
x <- -b / (2 * a)
return(x)
} else {
# Delta < 0
return(NA)
}
}For example:
Another example:
because the polynomial has no real root.
Q4. Optional arguments. Add two optional arguments to the function:
digits, an integer with default valueNULL;verbose, a boolean with default valueFALSE.
If digits is not NULL, the function should round the roots to the number of decimal places specified by the user. If verbose = TRUE, the function should display (in the console) the value of the discriminant before returning the roots.
Add digits = NULL and verbose = FALSE as extra parameters in the function’s signature — assigning them a default value is exactly what makes them optional.
Print the discriminant with cat("Discriminant:", delta, "\n") right after computing it, guarded by if (verbose == TRUE). Round with round(roots, digits), guarded by if (!is.null(digits)), just before returning.
A possible final version is:
#' Compute the real roots of a quadratic equation
#'
#' @param a The coefficient of \eqn{x^2}, \eqn{a \ne 0}.
#' @param b The linear coefficient.
#' @param c The constant term.
#' @param digits Optional integer specifying the number of decimal places.
#' Defaults to `NULL`, in which case no rounding is performed.
#' @param verbose Logical. If `TRUE`, prints the value of the discriminant.
#' Defaults to `FALSE`.
#'
#' @description Solve a quadratic equation of the form
#' \deqn{ax^2 + bx + c = 0,}
#' where \eqn{a \ne 0}. Complex solutions are ignored in this function.
#' @returns Depending on the discriminant \eqn{\Delta}, returns:
#' * If \eqn{\Delta > 0}: the two real roots,
#' * If \eqn{\Delta = 0}: the real root,
#' * If \eqn{\Delta < 0}: returns `NA`.
#' @md
solve_quadratic <- function(a,
b,
c,
digits = NULL,
verbose = FALSE) {
if (a == 0) {
stop("The coefficient a must be different from 0.")
}
delta <- b^2 - 4 * a * c
if (verbose == TRUE) {
cat("Discriminant:", delta, "\n")
}
if (delta > 0) {
x1 <- (-b - sqrt(delta)) / (2 * a)
x2 <- (-b + sqrt(delta)) / (2 * a)
roots <- base::c(x1, x2)
} else if (delta == 0) {
roots <- -b / (2 * a)
} else {
return(NA)
}
if (!is.null(digits)) {
roots <- round(roots, digits)
}
roots
}For example:
returns the two real roots without rounding.
With the optional argument digits:
the function returns the roots rounded to two decimal places.
With the optional argument verbose:
the function first prints the discriminant and then returns the rounded roots.
In this final function, a, b, and c are mandatory arguments. The arguments digits and verbose are optional because they have default values.
Introduction to Programming for Data Analysis — Master 1 in Economics