1. Load packages
library(dplyr) # data cleaning
library(tidyr) # drop_na()
library(knitr) # kable() tables
2. Read the data file
The file is a tab-separated text file saved in
UTF-16 encoding, so a normal read.csv()
would not read it correctly. Put the CSV in the same folder as
this .Rmd file and check the file name below.
# File name (must be in the same folder as this .Rmd)
data_file <- "20261019021822.csv"
if (!file.exists(data_file)) {
stop("File not found: ", data_file,
"\nPut it in the same folder as this .Rmd. Current folder: ", getwd())
}
raw <- read.delim(
data_file,
sep = "\t", # columns are separated by tabs
fileEncoding = "UTF-16", # the file is UTF-16
colClasses = "character", # read everything as text first (IDs like "0050" stay safe)
quote = "", # company names may contain quote characters
check.names = FALSE # keep the original column names
)
# Structure of the raw file
dim(raw)
## [1] 135022 6
names(raw)
## [1] "CO_ID"
## [2] "Date"
## [3] "Quarter"
## [4] "Market Value of Common Stocks at Quarter-End"
## [5] "Total equity"
## [6] "Net operating income (loss)"
head(raw)
3. Clean the data
clean <- raw |>
# Give the long column names simple names
rename(
market_value = `Market Value of Common Stocks at Quarter-End`,
total_equity = `Total equity`,
net_income = `Net operating income (loss)`
) |>
mutate(
# Remove extra spaces everywhere
across(everything(), trimws),
# CO_ID looks like "2330 TSMC": split it into the ID and the company name
stock_id = sub(" .*$", "", CO_ID), # text before the first space
name = sub("^\\S+\\s+", "", CO_ID), # text after the first space
# In this file a missing value is written as "." -> turn it into NA
market_value = na_if(market_value, "."),
total_equity = na_if(total_equity, "."),
net_income = na_if(net_income, "."),
# Convert text to numbers
market_value = as.numeric(market_value),
total_equity = as.numeric(total_equity),
net_income = as.numeric(net_income),
Quarter = as.integer(Quarter),
# Date looks like 201003 (year + month). Convert to a real date (first day of month)
date = as.Date(paste0(Date, "01"), format = "%Y%m%d"),
year = as.integer(format(date, "%Y"))
) |>
select(stock_id, name, date, year, Quarter, market_value, total_equity, net_income)
n_before <- nrow(clean)
firms_before <- n_distinct(clean$stock_id)
Rows before filtering: 135022 (3002
different IDs)
4. Keep only regular (common) stocks
The file also contains items that are not ordinary stocks, such as
industry indexes (IDs starting with M), securities firms
with IDs like 0009A0, and foreign depositary receipts (IDs
starting with 91). We keep only 4-digit IDs that start with
1-9 and are not in the 91xx depositary-receipt range.
is_common <- grepl("^[1-9][0-9]{3}$", clean$stock_id) & # exactly 4 digits, not starting with 0
!grepl("^91", clean$stock_id) # remove 91xx depositary receipts
# Examples of what is removed (to check the filter)
kable(head(distinct(clean[!is_common, ], stock_id, name), 15),
caption = "Examples of removed IDs")
Examples of removed IDs
| 000930 |
H.N.S |
| 000116 |
JihSun SCL |
| 000960 |
FBS |
| 000980 |
Yuanta |
| 0009A0 |
SinoPac |
| 9102 |
Eastern Asia Technology |
| 911609 |
Yangzijiang Shipbuilding (Hold |
| 911616 |
Dukang Distillers Holdings Ltd |
| 911626 |
Multi Sports Holdings Ltd. |
| 9105 |
Cal-Comp Electronics (Thai) |
| 910579 |
Oceanus Group Limited |
| 911602 |
Hwa Fong Rubber (Thailand) Pub |
| 911606 |
Super Group Ltd. |
| 911610 |
United Envirotech Ltd. |
| 911612 |
Hu An Cable Holdings Ltd. |
stocks <- clean[is_common, ]
# Remove duplicated stock-quarter rows (if any)
stocks <- distinct(stocks, stock_id, date, .keep_all = TRUE)
cat("Rows after filtering:", nrow(stocks), "\n")
## Rows after filtering: 128817
cat("Firms after filtering:", n_distinct(stocks$stock_id), "\n")
## Firms after filtering: 2875
cat("Period:", format(min(stocks$date)), "to", format(max(stocks$date)), "\n")
## Period: 2010-03-01 to 2026-06-01
5. Missing values
missing_table <- data.frame(
variable = c("market_value", "total_equity", "net_income"),
missing = c(sum(is.na(stocks$market_value)),
sum(is.na(stocks$total_equity)),
sum(is.na(stocks$net_income)))
)
missing_table$percent <- round(100 * missing_table$missing / nrow(stocks), 2)
kable(missing_table, caption = "Missing values in the cleaned data")
Missing values in the cleaned data
| market_value |
9111 |
7.07 |
| total_equity |
5 |
0.00 |
| net_income |
2957 |
2.30 |
6. Summary statistics
Values are in the original units of the file (as provided by the data
source).
kable(summary(stocks[, c("market_value", "total_equity", "net_income")]),
caption = "Summary of the numeric variables")
Summary of the numeric variables
|
Min. :2.494e+04 |
Min. : -51575825 |
Min. :-38720187 |
|
1st Qu.:1.424e+06 |
1st Qu.: 862031 |
1st Qu.: -5761 |
|
Median :3.366e+06 |
Median : 1986152 |
Median : 31610 |
|
Mean :2.461e+07 |
Mean : 13941534 |
Mean : 347663 |
|
3rd Qu.:9.446e+06 |
3rd Qu.: 5773830 |
3rd Qu.: 128717 |
|
Max. :6.250e+10 |
Max. :6474470981 |
Max. :766602651 |
|
NAs :9111 |
NAs :5 |
NAs :2957 |
7. First 20 rows
kable(head(stocks, 20), caption = "First 20 rows of the cleaned data")
First 20 rows of the cleaned data
| 1101 |
TCC |
2010-03-01 |
2010 |
1 |
98600641 |
102157504 |
2152334 |
| 1104 |
UCC |
2010-03-01 |
2010 |
1 |
9813229 |
12373734 |
-102741 |
| 1108 |
Lucky Cement |
2010-03-01 |
2010 |
1 |
3290520 |
4324808 |
-15334 |
| 1102 |
ACC |
2010-03-01 |
2010 |
1 |
92557785 |
87422478 |
762094 |
| 1109 |
HSINGTA |
2010-03-01 |
2010 |
1 |
4273150 |
6911221 |
12543 |
| 1103 |
CHC |
2010-03-01 |
2010 |
1 |
11234775 |
18019997 |
-13640 |
| 1201 |
WEI CHUAN |
2010-03-01 |
2010 |
1 |
18749597 |
5829315 |
218832 |
| 1107 |
Chien Tai Cement |
2010-03-01 |
2010 |
1 |
590386 |
-1153228 |
-39860 |
| 1203 |
Ve Wong |
2010-03-01 |
2010 |
1 |
5844446 |
5173583 |
181553 |
| 1110 |
SE CEMENT |
2010-03-01 |
2010 |
1 |
6549400 |
8299983 |
19820 |
| 1216 |
Uni-President |
2010-03-01 |
2010 |
1 |
140891010 |
109689846 |
5043903 |
| 1210 |
GREATWALL |
2010-03-01 |
2010 |
1 |
15381807 |
14625557 |
231197 |
| 1218 |
Taisun |
2010-03-01 |
2010 |
1 |
4099110 |
3719501 |
16107 |
| 1213 |
Oceanic Bev. |
2010-03-01 |
2010 |
1 |
607405 |
601241 |
7572 |
| 1225 |
FOPCO |
2010-03-01 |
2010 |
1 |
2538120 |
2342779 |
57715 |
| 1215 |
CPE |
2010-03-01 |
2010 |
1 |
3364377 |
3570297 |
105255 |
| 1227 |
SFC |
2010-03-01 |
2010 |
1 |
18706334 |
6551674 |
823567 |
| 1217 |
AGV |
2010-03-01 |
2010 |
1 |
5775188 |
5025287 |
16628 |
| 1229 |
LHIHC |
2010-03-01 |
2010 |
1 |
12583701 |
17145372 |
40550 |
| 1219 |
FWUSOW |
2010-03-01 |
2010 |
1 |
3456268 |
3674478 |
35802 |
8. Number of firms in each quarter
firms_per_quarter <- stocks |>
group_by(date) |>
summarise(number_of_firms = n_distinct(stock_id), .groups = "drop")
kable(tail(firms_per_quarter, 12), caption = "Last 12 quarters")
Last 12 quarters
| 2023-09-01 |
1953 |
| 2023-12-01 |
2343 |
| 2024-03-01 |
1997 |
| 2024-06-01 |
2420 |
| 2024-09-01 |
2024 |
| 2024-12-01 |
2412 |
| 2025-03-01 |
2058 |
| 2025-06-01 |
2455 |
| 2025-09-01 |
2016 |
| 2025-12-01 |
2444 |
| 2026-03-01 |
2046 |
| 2026-06-01 |
2435 |
plot(firms_per_quarter$date, firms_per_quarter$number_of_firms,
type = "l", xlab = "Quarter", ylab = "Number of firms",
main = "Number of firms in the data over time")

9. Save the cleaned data
write.csv(stocks, "quarterly_data_clean.csv",
row.names = FALSE, fileEncoding = "UTF-8")
cat("Saved", nrow(stocks), "rows to", file.path(getwd(), "quarterly_data_clean.csv"), "\n")
## Saved 128817 rows to D:/079/quarterly_data_clean.csv