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. 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: 84 Columns: 6
ββ Column specification ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Delimiter: ","
chr (4): freq, unit, na_item, geo
dbl (2): TIME_PERIOD, OBS_VALUE
βΉ 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: 84 Γ 6
freq unit na_item geo TIME_PERIOD OBS_VALUE
<chr> <chr> <chr> <chr> <dbl> <dbl>
1 A CLV10_EUR_HAB B1GQ FR 2010 32548.
2 A CLV10_EUR_HAB B1GQ IT 2010 26774.
3 A CLV10_EUR_HAB B1GQ DE 2010 38145.
4 A CLV10_EUR_HAB B1GQ ES 2010 24253.
5 A CLV10_EUR_HAB B1GQ NL 2010 42162.
6 A CLV10_EUR_HAB B1GQ PT 2010 19958.
7 A CLV10_EUR_HAB B1GQ FR 2011 33245.
8 A CLV10_EUR_HAB B1GQ IT 2011 27502.
9 A CLV10_EUR_HAB B1GQ DE 2011 39567.
10 A CLV10_EUR_HAB B1GQ ES 2011 24455.
# βΉ 74 more rows
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: 84 Columns: 6
ββ Column specification ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Delimiter: ","
chr (4): freq, unit, indic_wb, geo
dbl (2): TIME_PERIOD, OBS_VALUE
βΉ 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: 84 Γ 6
freq unit indic_wb geo TIME_PERIOD OBS_VALUE
<chr> <chr> <chr> <chr> <dbl> <dbl>
1 A RTG LIFESAT FR 2010 6.72
2 A RTG LIFESAT IT 2010 6.59
3 A RTG LIFESAT DE 2010 7.17
4 A RTG LIFESAT ES 2010 6.67
5 A RTG LIFESAT NL 2010 7.94
6 A RTG LIFESAT PT 2010 6.42
7 A RTG LIFESAT FR 2011 7.13
8 A RTG LIFESAT IT 2011 6.45
9 A RTG LIFESAT DE 2011 7.32
10 A RTG LIFESAT ES 2011 6.93
# βΉ 74 more rows
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.
# A tibble: 84 Γ 3
geo TIME_PERIOD OBS_VALUE
<chr> <dbl> <dbl>
1 FR 2010 6.72
2 IT 2010 6.59
3 DE 2010 7.17
4 ES 2010 6.67
5 NL 2010 7.94
6 PT 2010 6.42
7 FR 2011 7.13
8 IT 2011 6.45
9 DE 2011 7.32
10 ES 2011 6.93
# βΉ 74 more rows
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: 84 Γ 3
country year life_satisf
<chr> <dbl> <dbl>
1 FR 2010 6.72
2 IT 2010 6.59
3 DE 2010 7.17
4 ES 2010 6.67
5 NL 2010 7.94
6 PT 2010 6.42
7 FR 2011 7.13
8 IT 2011 6.45
9 DE 2011 7.32
10 ES 2011 6.93
# βΉ 74 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: 84 Γ 3
country year gdp
<chr> <dbl> <dbl>
1 FR 2010 32548.
2 IT 2010 26774.
3 DE 2010 38145.
4 ES 2010 24253.
5 NL 2010 42162.
6 PT 2010 19958.
7 FR 2011 33245.
8 IT 2011 27502.
9 DE 2011 39567.
10 ES 2011 24455.
# βΉ 74 more rows
Q9. 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.
Q10. In the dataset gdp, keep only the rows where the country is France.
# A tibble: 14 Γ 3
country year gdp
<chr> <dbl> <dbl>
1 FR 2010 32548.
2 FR 2011 33245.
3 FR 2012 32724.
4 FR 2013 32944.
5 FR 2014 35318.
6 FR 2015 35382.
7 FR 2016 35526.
8 FR 2017 36783.
9 FR 2018 36947.
10 FR 2019 37796.
11 FR 2020 38253.
12 FR 2021 39174.
13 FR 2022 39929.
14 FR 2023 39966.
Q11. 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: 84 Γ 4
country year gdp life_satisf
<chr> <dbl> <dbl> <dbl>
1 FR 2010 32548. 6.72
2 IT 2010 26774. 6.59
3 DE 2010 38145. 7.17
4 ES 2010 24253. 6.67
5 NL 2010 42162. 7.94
6 PT 2010 19958. 6.42
7 FR 2011 33245. 7.13
8 IT 2011 27502. 6.45
9 DE 2011 39567. 7.32
10 ES 2011 24455. 6.93
# βΉ 74 more rows
Q12. 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: 6 Γ 4
country mean_gdp mean_sat high_gdp_dummy
<chr> <dbl> <dbl> <dbl>
1 DE 43009. 7.34 1
2 ES 27114. 6.87 0
3 FR 36181. 6.96 1
4 IT 30487. 6.59 0
5 NL 47439. 7.91 1
6 PT 22656. 6.41 0
Q13. Export in a CSV file the dataset combined_data in your out data folder. Name the file gdp-lifesat.csv.
Q14. 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.
Q15. 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")Q16. 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: 84 Γ 4
country year life_satisf happiness_status
<chr> <dbl> <dbl> <chr>
1 FR 2010 6.72 Moderate or Low
2 IT 2010 6.59 Moderate or Low
3 DE 2010 7.17 High
4 ES 2010 6.67 Moderate or Low
5 NL 2010 7.94 High
6 PT 2010 6.42 Moderate or Low
7 FR 2011 7.13 High
8 IT 2011 6.45 Moderate or Low
9 DE 2011 7.32 High
10 ES 2011 6.93 Moderate or Low
# βΉ 74 more rows
Q17. 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.
Q18. 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