Objective. Build two independent, cleaned datasets
of listed common stocks in Taiwan, one from the Taiwan Stock
Exchange (TWSE) ISIN lookup page and one from the FinMind API. Each
dataset has four columns: stock_id, name,
listing_date, industry. ETFs, funds, preferred
stocks and other non-common-stock securities are excluded. The two
sources are then compared and exported to CSV.
Different data providers classify securities differently and update
at different times. This report therefore (1) inspects each source
before filtering, (2) records why every record is kept,
excluded or flagged, and (3) never invents values: if a field cannot be
verified (for example FinMind’s listing date), it stays
NA.
| Source | Location | What it provides |
|---|---|---|
| TWSE ISIN lookup | https://isin.twse.com.tw/isin/e_C_public.jsp?strMode=2 |
HTML table of listed securities: code + name, ISIN, listing date,
market, industry, CFI code, remarks. strMode=2 = listed
(上市) securities. |
FinMind TaiwanStockInfo |
https://api.finmindtrade.com/api/v4/data |
JSON: industry_category, stock_id,
stock_name, type (twse / tpex / …),
date. |
Important: FinMind’s date column is the
date the record was updated, not the official listing
date. It is not used as listing_date.
Run once in the Console (not inside the .Rmd):
install.packages(c("rvest", "httr", "jsonlite", "dplyr", "stringr", "readr", "knitr", "tibble"))
Common error: “package ‘x’ is not available” means a typo or an old R version; update R and retry.
Loaded inside the .Rmd:
library(rvest)
library(httr)
library(jsonlite)
library(dplyr)
library(stringr)
library(readr)
library(knitr)
library(tibble)
What: functions shared by both sources. Why: identical cleaning and checks make the comparison fair. Where to run: inside the .Rmd. Expected output: none (they only define functions).
final_cols <- c("stock_id", "name", "listing_date", "industry")
# A common stock on TWSE has a 4-digit code that does not start with 0
# (ETFs such as 0050 start with 0; preferred shares carry a letter suffix, e.g. 2881A).
valid_common_id <- function(x) str_detect(x, "^[1-9][0-9]{3}$")
# Standardise to the four required columns, types and order.
standardize_stock_table <- function(df) {
df |>
transmute(
stock_id = str_squish(as.character(stock_id)),
name = str_squish(as.character(name)),
listing_date = as.Date(listing_date),
industry = na_if(str_squish(as.character(industry)), "")
) |>
select(all_of(final_cols))
}
# Validation checks returned as a table.
validate_stock_table <- function(df, source_name) {
tibble(
source = source_name,
check = c("Columns correct and in order",
"All stock_id are valid 4-digit common codes",
"No empty names",
"listing_date is Date class (NA allowed)",
"Missing listing_date (count)",
"Missing industry (count)",
"Duplicate stock_id (count)"),
result = c(identical(names(df), final_cols),
all(valid_common_id(df$stock_id)),
all(!is.na(df$name) & df$name != ""),
inherits(df$listing_date, "Date"),
sum(is.na(df$listing_date)),
sum(is.na(df$industry)),
sum(duplicated(df$stock_id)))
)
}
What: sends an HTTP GET request and reads the HTML.
Why: we need the raw page before parsing. Expected
output: HTTP status 200 and a few lines of readable text.
Common errors: timeout (retry, check internet); garbled Chinese
(change twse_encoding to "UTF-8" and re-run).
Where: inside the .Rmd.
twse_url <- "https://isin.twse.com.tw/isin/e_C_public.jsp?strMode=2"
twse_encoding <- "Big5" # use "UTF-8" if text looks garbled
twse_cache <- "twse_isin_strMode2.html" # saved in your project folder
if (!file.exists(twse_cache)) {
twse_resp <- tryCatch(
RETRY("GET", twse_url,
user_agent("Mozilla/5.0 (university assignment)"),
timeout(300),
write_disk(twse_cache, overwrite = TRUE),
times = 3, pause_min = 5, terminate_on = c(403, 404)),
error = function(e) {
unlink(twse_cache)
stop("TWSE download failed: ", conditionMessage(e))
}
)
stop_for_status(twse_resp)
cat("HTTP status:", status_code(twse_resp), "\n")
} else {
cat("Using cached file:", twse_cache, "\n")
}
## Using cached file: twse_isin_strMode2.html
twse_page <- read_html(twse_cache, encoding = twse_encoding)
cat("Tables found:", length(html_elements(twse_page, "table")), "\n")
## Tables found: 2
What: shows the first rows of every <tr>.
Why: the page has section-heading rows (for example “Stock”,
“ETF”) mixed with data rows, and html_table() can misalign
them. We must see this before parsing.
all_rows <- html_elements(twse_page, "tr")
row_cells <- lapply(all_rows, function(r) str_squish(html_text(html_elements(r, "td, th"))))
cat("Total <tr> rows:", length(row_cells), "\n")
## Total <tr> rows: 38321
cat("Cells per row (distribution):\n")
## Cells per row (distribution):
print(table(lengths(row_cells)))
##
## 1 7
## 8 38313
head(Filter(function(x) length(x) > 0, row_cells), 6)
## [[1]]
## [1] "Security Code & Security Name" "ISIN Code"
## [3] "Date Listed" "Market"
## [5] "Industrial Group" "CFICode"
## [7] "Remarks"
##
## [[2]]
## [1] "Stocks"
##
## [[3]]
## [1] "1101 TCC" "TW0001101004" "1962/02/09" "TWSE LISTED" "Cement"
## [6] "ESVUFR" ""
##
## [[4]]
## [1] "1102 ACC" "TW0001102002" "1962/06/08" "TWSE LISTED" "Cement"
## [6] "ESVUFR" ""
##
## [[5]]
## [1] "1103 CHC" "TW0001103000" "1969/11/14" "TWSE LISTED" "Cement"
## [6] "ESVUFR" ""
##
## [[6]]
## [1] "1104 UCC" "TW0001104008" "1971/02/01" "TWSE LISTED" "Cement"
## [6] "ESVUFR" ""
What: walks through rows. A one-cell row is a
section heading (saved as section); the
row containing “ISIN” is the header; other full-width
rows are data. Why: the section heading is our
evidence for the security type. Expected output: a table with
the original column names plus section.
current_section <- NA_character_
header <- NULL
data_rows <- list()
data_sections <- character()
for (cells in row_cells) {
n <- length(cells)
if (n == 0) next
if (n == 1) { current_section <- cells[1]; next }
if (is.null(header)) {
if (any(str_detect(cells, regex("ISIN", ignore_case = TRUE)))) header <- cells
next
}
if (n == length(header)) {
data_rows[[length(data_rows) + 1]] <- cells
data_sections <- c(data_sections, current_section)
}
}
stopifnot(!is.null(header), length(data_rows) > 0)
twse_raw <- as.data.frame(do.call(rbind, data_rows), stringsAsFactors = FALSE)
names(twse_raw) <- header
twse_raw$section <- data_sections
twse_raw <- as_tibble(twse_raw)
cat("Columns found:\n"); print(names(twse_raw))
## Columns found:
## [1] "Security Code & Security Name" "ISIN Code"
## [3] "Date Listed" "Market"
## [5] "Industrial Group" "CFICode"
## [7] "Remarks" "section"
cat("\nRows per section heading:\n"); print(count(twse_raw, section, sort = TRUE))
##
## Rows per section heading:
## # A tibble: 7 × 2
## section n
## <chr> <int>
## 1 Listing Warrants 36927
## 2 Stocks 1085
## 3 ETF 242
## 4 Preferred Stocks 27
## 5 ETN 15
## 6 TDR 10
## 7 Real Estate Investment Trust (REIT) 6
head(twse_raw)
Common error: “Expected exactly one column matching …”. Look at the printed column names above and adjust the pattern.
cat("Column names found on the page:\n")
## Column names found on the page:
print(names(twse_raw))
## [1] "Security Code & Security Name" "ISIN Code"
## [3] "Date Listed" "Market"
## [5] "Industrial Group" "CFICode"
## [7] "Remarks" "section"
share_match <- function(x, pattern) {
x <- str_squish(x); x <- x[!is.na(x) & x != ""]
if (length(x) == 0) return(0)
mean(str_detect(x, pattern))
}
find_col_by_content <- function(df, pattern, label) {
shares <- sapply(df, share_match, pattern = pattern)
best <- names(shares)[which.max(shares)]
if (max(shares) < 0.5) stop("Could not find the ", label, " column by content.")
best
}
col_codename <- find_col_by_content(twse_raw, "^[0-9A-Z]{4,6}\\s+\\S", "code and name")
col_listing <- find_col_by_content(twse_raw, "^[0-9]{4}/[0-9]{2}/[0-9]{2}$", "listing date")
col_cfi <- find_col_by_content(twse_raw, "^[A-Z]{6}$", "CFI code")
find_col_by_name <- function(df, pattern, label) {
hits <- names(df)[str_detect(names(df), regex(pattern, ignore_case = TRUE))]
if (length(hits) != 1) stop("Cannot identify the ", label,
" column by name. Names are: ", paste(names(df), collapse = " | "))
hits
}
col_industry <- find_col_by_name(twse_raw, "industr|產業|sector", "industry")
col_market <- find_col_by_name(twse_raw, "market|市場", "market")
data.frame(role = c("code+name", "listing date", "CFI", "industry", "market"),
column = c(col_codename, col_listing, col_cfi, col_industry, col_market))
What: code and name sit in one cell separated by a space
(often a full-width space). We split at the first space. Dates like
1962/02/09 become real Date values; failed
conversions become NA (not guessed).
twse_parsed <- twse_raw |>
mutate(
code_name_parts = str_match(.data[[col_codename]], "^(\\S+)\\s+(.*)$"),
stock_id = code_name_parts[, 2],
name = code_name_parts[, 3],
listing_date = as.Date(str_replace_all(.data[[col_listing]], "/", "-"), format = "%Y-%m-%d"),
industry = .data[[col_industry]],
cfi_code = str_squish(.data[[col_cfi]]),
market = .data[[col_market]]
) |>
select(stock_id, name, listing_date, industry, cfi_code, market, section)
cat("Rows where code/name could not be split:", sum(is.na(twse_parsed$stock_id)), "\n")
## Rows where code/name could not be split: 0
head(twse_parsed)
Three independent pieces of evidence from the TWSE page:
ES... = equity,
common/ordinary shares; EP... = preferred; other prefixes
are funds, depositary receipts, etc.A record is common only if all three agree. If the
section says stock but the CFI code is missing, it is
ambiguous and is not silently kept: it is
exported for manual review.
stock_section_pattern <- regex("^stock|^equit|股票", ignore_case = TRUE)
twse_classified <- twse_parsed |>
mutate(
is_stock_section = str_detect(coalesce(section, ""), stock_section_pattern) &
!str_detect(coalesce(section, ""), regex("preferred|特別股|warrant|認購|認售|depositary|TDR|ETF|fund|beneficiary|受益", ignore_case = TRUE)),
cfi_common = str_detect(coalesce(cfi_code, ""), "^ES"),
cfi_missing = is.na(cfi_code) | cfi_code == "",
id_ok = valid_common_id(coalesce(stock_id, "")),
classification = case_when(
is.na(stock_id) ~ "excluded: unparseable code/name",
is_stock_section & cfi_common & id_ok ~ "common",
is_stock_section & cfi_missing & id_ok ~ "ambiguous",
str_detect(coalesce(cfi_code, ""), "^EP") ~ "excluded: preferred stock (CFI EP)",
str_detect(stock_id, "^[0-9]{4}[A-Z]$") ~ "excluded: preferred stock (ID suffix)",
!is_stock_section ~ "excluded: non-stock section (ETF/fund/warrant/TDR etc.)",
!id_ok ~ "excluded: not a common-stock code format",
TRUE ~ "ambiguous"
)
)
kable(count(twse_classified, classification, sort = TRUE), caption = "TWSE classification results")
| classification | n |
|---|---|
| excluded: non-stock section (ETF/fund/warrant/TDR etc.) | 37200 |
| common | 1085 |
| excluded: preferred stock (CFI EP) | 26 |
| excluded: preferred stock (ID suffix) | 1 |
twse_before <- nrow(twse_parsed)
twse_candidates <- twse_classified |> filter(classification == "common")
twse_ambiguous <- twse_classified |> filter(classification == "ambiguous")
twse_excluded_n <- sum(str_starts(twse_classified$classification, "excluded"))
twse_dups_n <- sum(duplicated(twse_candidates$stock_id))
twse_clean <- twse_candidates |>
standardize_stock_table() |>
distinct(stock_id, .keep_all = TRUE) |>
arrange(stock_id)
cat("Parsed rows:", twse_before, "| Excluded by type:", twse_excluded_n,
"| Ambiguous (manual review):", nrow(twse_ambiguous),
"| Duplicates removed:", twse_dups_n, "| Final:", nrow(twse_clean), "\n")
## Parsed rows: 38312 | Excluded by type: 37227 | Ambiguous (manual review): 0 | Duplicates removed: 0 | Final: 1085
head(twse_clean, 10)
Decision on missing values: records with a missing industry or listing date are kept (flagged in the validation table), because a missing attribute does not change whether the security is a common stock.
What: requests the TaiwanStockInfo dataset.
Why: it is FinMind’s security master list. Optional: a
free FinMind token raises the rate limit. Set it once in the Console
with Sys.setenv(FINMIND_TOKEN = "your_token"); the code
works without it. Common errors: HTTP 402 = rate limit reached
(add a token or wait); status not 200 in the JSON = API
error message is printed.
fm_token <- Sys.getenv("FINMIND_TOKEN")
fm_query <- list(dataset = "TaiwanStockInfo")
if (nzchar(fm_token)) fm_query$token <- fm_token
fm_resp <- GET("https://api.finmindtrade.com/api/v4/data", query = fm_query, timeout(60))
cat("HTTP status:", status_code(fm_resp), "\n")
## HTTP status: 200
if (http_error(fm_resp)) stop("FinMind request failed: HTTP ", status_code(fm_resp))
fm_json <- fromJSON(content(fm_resp, as = "text", encoding = "UTF-8"), flatten = TRUE)
if (!identical(as.integer(fm_json$status), 200L)) stop("FinMind API error: ", fm_json$msg)
fm_raw <- as_tibble(fm_json$data)
cat("Rows:", nrow(fm_raw), "\nColumns:", paste(names(fm_raw), collapse = ", "), "\n")
## Rows: 4333
## Columns: industry_category, stock_id, stock_name, type, date
head(fm_raw)
Why: we decide the filter from the actual
values of type and industry_category.
kable(count(fm_raw, type, sort = TRUE), caption = "FinMind: values of `type`")
| type | n |
|---|---|
| twse | 2402 |
| tpex | 1378 |
| emerging | 553 |
kable(count(fm_raw, industry_category, sort = TRUE), caption = "FinMind: values of `industry_category`")
| industry_category | n |
|---|---|
| 電子工業 | 545 |
| 生技醫療業 | 303 |
| 半導體業 | 294 |
| ETF | 271 |
| 電子零組件業 | 262 |
| 其他 | 194 |
| 光電業 | 178 |
| 電腦及週邊設備業 | 148 |
| 電機機械 | 141 |
| 上櫃ETF | 125 |
| 上櫃指數股票型基金(ETF) | 125 |
| 通信網路業 | 119 |
| 化學生技醫療 | 105 |
| 建材營造 | 93 |
| 其他電子業 | 81 |
| 觀光餐旅 | 76 |
| 資訊服務業 | 72 |
| 綠能環保 | 71 |
| 金融保險 | 70 |
| 紡織纖維 | 62 |
| 其他電子類 | 61 |
| 化學工業 | 60 |
| 鋼鐵工業 | 56 |
| 電子通路業 | 51 |
| 食品工業 | 49 |
| 汽車工業 | 47 |
| 觀光事業 | 45 |
| 航運業 | 40 |
| 文化創意業 | 39 |
| 貿易百貨 | 39 |
| 數位雲端 | 38 |
| 創新板股票 | 36 |
| 存託憑證 | 36 |
| 所有證券 | 36 |
| Index | 30 |
| 塑膠工業 | 29 |
| ETN | 28 |
| 數位雲端類 | 27 |
| 居家生活類 | 25 |
| 運動休閒 | 23 |
| 電器電纜 | 23 |
| 創新版股票 | 21 |
| 指數投資證券(ETN) | 20 |
| 綠能環保類 | 20 |
| 居家生活 | 18 |
| 金融業 | 16 |
| 油電燃氣業 | 13 |
| 橡膠工業 | 12 |
| 運動休閒類 | 10 |
| 電子商務業 | 10 |
| 受益證券 | 8 |
| 水泥工業 | 8 |
| 造紙工業 | 8 |
| 玻璃陶瓷 | 5 |
| 農業科技 | 5 |
| 農業科技業 | 4 |
| 大盤 | 2 |
Note on listing_date:
TaiwanStockInfo has a date column, but it is a
data update date, so it is not used.
listing_date is set to NA for FinMind. See the
optional supplement in the Comparison section.
non_common_industry <- regex("ETF|ETN|Index|大盤|基金|存託憑證|受益|Depositary|Beneficiary|Warrant|權證",
ignore_case = TRUE)
fm_before <- nrow(fm_raw)
fm_classified <- fm_raw |>
mutate(
stock_id = str_squish(stock_id),
name = str_squish(stock_name),
industry = na_if(str_squish(industry_category), ""),
classification = case_when(
type != "twse" ~ "excluded: not TWSE-listed (e.g. TPEx/emerging)",
str_detect(stock_id, "^[0-9]{4}[A-Z]$") ~ "excluded: preferred stock (ID suffix)",
str_detect(coalesce(industry, ""), non_common_industry) ~ "excluded: ETF/fund/index/DR/other (industry label)",
!valid_common_id(stock_id) ~ "excluded: not a common-stock code format",
is.na(industry) ~ "ambiguous",
TRUE ~ "common"
)
)
kable(count(fm_classified, classification, sort = TRUE), caption = "FinMind classification results")
| classification | n |
|---|---|
| common | 1988 |
| excluded: not TWSE-listed (e.g. TPEx/emerging) | 1931 |
| excluded: ETF/fund/index/DR/other (industry label) | 374 |
| excluded: preferred stock (ID suffix) | 39 |
| excluded: not a common-stock code format | 1 |
fm_ambiguous <- fm_classified |> filter(classification == "ambiguous")
fm_excluded_n <- sum(str_starts(fm_classified$classification, "excluded"))
fm_candidates <- fm_classified |> filter(classification == "common")
fm_dups_n <- sum(duplicated(fm_candidates$stock_id))
finmind_clean <- fm_candidates |>
mutate(listing_date = as.Date(NA)) |> # not provided by TaiwanStockInfo; left as NA on purpose
standardize_stock_table() |>
distinct(stock_id, .keep_all = TRUE) |>
arrange(stock_id)
cat("Raw rows:", fm_before, "| Excluded by type:", fm_excluded_n,
"| Ambiguous:", nrow(fm_ambiguous), "| Duplicates removed:", fm_dups_n,
"| Final:", nrow(finmind_clean), "\n")
## Raw rows: 4333 | Excluded by type: 2345 | Ambiguous: 0 | Duplicates removed: 773 | Final: 1215
head(finmind_clean, 10)
Why two filters (type and industry label)?
FinMind mixes TWSE, TPEx, emerging-board securities, ETFs and index
series in one table. Any record that passes the ID format but has no
industry is flagged ambiguous rather than assumed to be a
common stock.
validation_table <- bind_rows(
validate_stock_table(twse_clean, "TWSE"),
validate_stock_table(finmind_clean, "FinMind")
) |> mutate(result = as.character(result))
kable(validation_table)
| source | check | result |
|---|---|---|
| TWSE | Columns correct and in order | 1 |
| TWSE | All stock_id are valid 4-digit common codes | 1 |
| TWSE | No empty names | 1 |
| TWSE | listing_date is Date class (NA allowed) | 1 |
| TWSE | Missing listing_date (count) | 0 |
| TWSE | Missing industry (count) | 0 |
| TWSE | Duplicate stock_id (count) | 0 |
| FinMind | Columns correct and in order | 1 |
| FinMind | All stock_id are valid 4-digit common codes | 1 |
| FinMind | No empty names | 1 |
| FinMind | listing_date is Date class (NA allowed) | 1 |
| FinMind | Missing listing_date (count) | 1215 |
| FinMind | Missing industry (count) | 0 |
| FinMind | Duplicate stock_id (count) | 0 |
# Confirm no excluded security types remain
cat("TWSE: any ID starting with 0 or containing letters? ",
any(!valid_common_id(twse_clean$stock_id)), "\n")
## TWSE: any ID starting with 0 or containing letters? FALSE
cat("FinMind: any ID starting with 0 or containing letters? ",
any(!valid_common_id(finmind_clean$stock_id)), "\n")
## FinMind: any ID starting with 0 or containing letters? FALSE
# Duplicate investigation (should be zero rows)
twse_clean |> count(stock_id) |> filter(n > 1)
finmind_clean |> count(stock_id) |> filter(n > 1)
only_twse <- anti_join(twse_clean, finmind_clean, by = "stock_id")
only_finmind <- anti_join(finmind_clean, twse_clean, by = "stock_id")
in_both <- inner_join(twse_clean, finmind_clean, by = "stock_id", suffix = c("_twse", "_finmind"))
comparison <- tibble(
metric = c("Records before cleaning (parsed/raw)",
"Records excluded by security type",
"Ambiguous records withheld for manual review",
"Duplicate records removed",
"Records after cleaning",
"Missing listing_date",
"Missing industry"),
TWSE = c(twse_before, twse_excluded_n, nrow(twse_ambiguous), twse_dups_n,
nrow(twse_clean), sum(is.na(twse_clean$listing_date)), sum(is.na(twse_clean$industry))),
FinMind = c(fm_before, fm_excluded_n, nrow(fm_ambiguous), fm_dups_n,
nrow(finmind_clean), sum(is.na(finmind_clean$listing_date)), sum(is.na(finmind_clean$industry)))
) |> mutate(across(c(TWSE, FinMind), as.character))
overlap <- tibble(
metric = c("Stock IDs in both datasets", "Stock IDs only in TWSE", "Stock IDs only in FinMind"),
TWSE = as.character(c(nrow(in_both), nrow(only_twse), NA)),
FinMind = as.character(c(nrow(in_both), NA, nrow(only_finmind)))
)
comparison_all <- bind_rows(comparison, overlap)
kable(comparison_all, caption = "TWSE vs FinMind comparison")
| metric | TWSE | FinMind |
|---|---|---|
| Records before cleaning (parsed/raw) | 38312 | 4333 |
| Records excluded by security type | 37227 | 2345 |
| Ambiguous records withheld for manual review | 0 | 0 |
| Duplicate records removed | 0 | 773 |
| Records after cleaning | 1085 | 1215 |
| Missing listing_date | 0 | 1215 |
| Missing industry | 0 | 0 |
| Stock IDs in both datasets | 1085 | 1085 |
| Stock IDs only in TWSE | 0 | NA |
| Stock IDs only in FinMind | NA | 130 |
cat("IDs only in TWSE (first 30):\n"); print(head(only_twse$stock_id, 30))
## IDs only in TWSE (first 30):
## character(0)
cat("IDs only in FinMind (first 30):\n"); print(head(only_finmind$stock_id, 30))
## IDs only in FinMind (first 30):
## [1] "1107" "1230" "1262" "1311" "1469" "1507" "1520" "1523" "1592" "1601"
## [11] "1606" "1613" "1701" "1704" "1715" "1716" "1724" "1729" "1902" "2311"
## [21] "2315" "2325" "2336" "2341" "2350" "2358" "2361" "2381" "2384" "2391"
FinMind does not supply listing dates. For stock IDs that appear in
both datasets, the TWSE official date can be attached
in a separate column so the origin is explicit. The
main FinMind file keeps NA.
finmind_supplemented <- finmind_clean |>
left_join(select(twse_clean, stock_id, listing_date_twse = listing_date), by = "stock_id") |>
mutate(listing_date_source = if_else(is.na(listing_date_twse), NA_character_, "TWSE ISIN (supplementary)"))
cat("FinMind IDs that can be supplemented from TWSE:", sum(!is.na(finmind_supplemented$listing_date_twse)), "\n")
## FinMind IDs that can be supplemented from TWSE: 1085
Use the tables above to write your own interpretation. Points to discuss:
industry_category. Industry
names may differ in language and granularity, so only stock IDs are
compared.NA by design.ambiguous.Two independent datasets of listed common stocks were produced with
the same four columns, validated with the same checks, compared with
dplyr joins, and exported. Every inclusion and exclusion is
backed by evidence from the source, and missing information is preserved
as NA rather than invented.
cat("Working directory:", getwd(), "\n")
## Working directory: /Users/kvn.andretty/Downloads
write_csv(twse_clean, "twse_listed_common_stocks.csv", na = "")
write_csv(finmind_clean, "finmind_listed_common_stocks.csv", na = "")
write_csv(comparison_all, "twse_finmind_comparison.csv", na = "")
# Extra files for transparency
write_csv(twse_ambiguous |> select(stock_id, name, listing_date, industry, cfi_code, section),
"twse_ambiguous_for_review.csv", na = "")
write_csv(select(fm_ambiguous, stock_id, name, industry, type),
"finmind_ambiguous_for_review.csv", na = "")
write_csv(finmind_supplemented, "finmind_with_twse_listing_date_supplement.csv", na = "")
out_files <- c("twse_listed_common_stocks.csv", "finmind_listed_common_stocks.csv",
"twse_finmind_comparison.csv", "twse_ambiguous_for_review.csv",
"finmind_ambiguous_for_review.csv", "finmind_with_twse_listing_date_supplement.csv")
kable(tibble(file = out_files, exists = file.exists(out_files),
size_kb = round(file.size(out_files) / 1024, 1)))
| file | exists | size_kb |
|---|---|---|
| twse_listed_common_stocks.csv | TRUE | 48.2 |
| finmind_listed_common_stocks.csv | TRUE | 33.1 |
| twse_finmind_comparison.csv | TRUE | 0.4 |
| twse_ambiguous_for_review.csv | TRUE | 0.1 |
| finmind_ambiguous_for_review.csv | TRUE | 0.0 |
| finmind_with_twse_listing_date_supplement.csv | TRUE | 72.6 |
write_csv() always writes UTF-8. If Excel shows garbled
Chinese, import the file as UTF-8 (Data > From Text/CSV), or replace
write_csv with write_excel_csv, which adds a
UTF-8 byte-order mark.