Input: panel_clean.csv — merged quarterly panel for
listed Taiwanese textile firms (TSE Textiles & Fiber), assembled
from five TEJ Pro exports (balance sheet, income statement, cash-flow
statement, stock returns, per-share book value). Net income =
consolidated total profit after tax; all flow items are single-quarter
figures.
d <- read.csv("panel_clean.csv", )
d$t <- d$year * 4 + d$quarter
d$yq <- paste0(d$year, "Q", d$quarter)
d <- d[order(d$coid, d$t), ]
cat("Observations:", nrow(d), " Firms:", length(unique(d$coid)),
" Period:", min(d$yq), "->", max(d$yq), "\n")
## Observations: 1954 Firms: 42 Period: 2014Q2 -> 2026Q1
# continuous financial ratios
d$roa <- d$ni / d$ta
d$cfo_ta <- d$cfo / d$ta
d$cr <- d$ca / d$cl
d$gm <- (d$rev - d$cogs) / d$rev
d$ato <- d$rev / d$ta
d$ltd_r <- d$ltd / d$ta
d$debt_r <- d$tl / d$ta
d$size <- log(d$ta) # firm size = log(total assets)
d$pb <- d$price / d$bvps # price-to-book
# year-over-year change (lag 4 quarters) via explicit (coid, t-4) merge
lagvars <- c("roa","cr","gm","ato","ltd_r","cs")
lg <- d[, c("coid","t",lagvars)]; lg$t <- lg$t + 4
names(lg)[-(1:2)] <- paste0(lagvars,"_l4")
d <- merge(d, lg, by = c("coid","t"), all.x = TRUE)
d <- d[order(d$coid, d$t), ]
d$d_roa <- d$roa - d$roa_l4
d$d_cr <- d$cr - d$cr_l4
d$d_gm <- d$gm - d$gm_l4
d$d_ato <- d$ato - d$ato_l4
d$d_ltd <- d$ltd_r - d$ltd_r_l4
# 9 Piotroski F-score signals (NA where inputs unavailable)
d$F1 <- as.integer(d$roa > 0)
d$F2 <- as.integer(d$cfo > 0)
d$F3 <- as.integer(d$d_roa > 0)
d$F4 <- as.integer(d$cfo_ta > d$roa)
d$F5 <- as.integer(d$d_ltd < 0)
d$F6 <- as.integer(d$d_cr > 0)
d$F7 <- as.integer(d$cs <= d$cs_l4)
d$F8 <- as.integer(d$d_gm > 0)
d$F9 <- as.integer(d$d_ato > 0)
d$Fscore <- rowSums(d[, paste0("F",1:9)]) # NA if any signal is NA
# future one-quarter return via (coid, t-1) merge
fu <- d[, c("coid","t","ret")]; fu$t <- fu$t - 1
names(fu)[3] <- "fut_ret"
d <- merge(d, fu, by = c("coid","t"), all.x = TRUE)
d <- d[order(d$coid, d$t), ]
# Winner = future return above industry (same-quarter) median
med <- ave(d$fut_ret, d$yq, FUN = function(x) median(x, na.rm = TRUE))
d$Winner <- as.integer(d$fut_ret > med)
# five performance classes = within-quarter quintiles of future return
d$Cat5 <- ave(d$fut_ret, d$yq, FUN = function(x){
out <- rep(NA_integer_, length(x))
ok <- is.finite(x)
if (sum(ok) < 5) return(out)
b <- unique(quantile(x[ok], seq(0,1,.2), na.rm = TRUE))
if (length(b) < 2) return(out)
out[ok] <- as.integer(cut(x[ok], b, include.lowest = TRUE))
out
})
reg <- c("F1","F2","F3","F4","F5","F6","F7","F8","F9","Fscore","fut_ret",
"roa","cfo_ta","d_roa","d_cr","d_gm","d_ato","size","pb","debt_r","Winner")
D <- d[complete.cases(d[, reg]), ]
cat("Analysis sample N =", nrow(D), " (", min(D$yq), "->", max(D$yq), ")\n")
## Analysis sample N = 1691 ( 2015Q2 -> 2025Q3 )
dv <- c("Fscore","roa","cfo_ta","d_roa","d_cr","d_gm","d_ato","fut_ret","size","pb","debt_r")
round(t(sapply(D[,dv], function(x) c(N=length(x), Mean=mean(x), SD=sd(x),
Min=min(x), Median=median(x), Max=max(x)))), 4)
## N Mean SD Min Median Max
## Fscore 1691 5.1508 1.7733 1.0000 5.0000 9.0000
## roa 1691 0.0060 0.0338 -0.2440 0.0039 0.7209
## cfo_ta 1691 0.0107 0.0424 -0.2190 0.0078 0.4340
## d_roa 1691 -0.0003 0.0449 -0.7202 -0.0006 0.9649
## d_cr 1691 0.0104 5.3780 -62.3026 -0.0016 75.1443
## d_gm 1691 -0.0014 0.2797 -4.9803 0.0007 5.2988
## d_ato 1691 -0.0063 0.0557 -0.7021 -0.0031 0.4334
## fut_ret 1691 1.4253 18.3528 -61.0990 -0.3635 250.0007
## size 1691 15.7747 1.3756 10.9641 15.4549 20.3406
## pb 1691 1.4600 1.3640 0.1843 1.0151 16.7255
## debt_r 1691 0.4150 0.1991 0.0329 0.4156 0.9843
cat("\nF-score distribution:\n"); print(table(D$Fscore))
##
## F-score distribution:
##
## 1 2 3 4 5 6 7 8 9
## 25 101 198 271 368 325 246 120 37
cv <- c("Fscore","roa","cfo_ta","d_roa","d_cr","d_gm","d_ato","fut_ret")
round(cor(D[,cv]), 3)
## Fscore roa cfo_ta d_roa d_cr d_gm d_ato fut_ret
## Fscore 1.000 0.272 0.381 0.218 0.009 0.215 0.337 0.053
## roa 0.272 1.000 0.199 0.663 -0.028 0.178 0.117 -0.003
## cfo_ta 0.381 0.199 1.000 0.007 -0.027 0.043 -0.044 0.001
## d_roa 0.218 0.663 0.007 1.000 -0.034 0.189 0.252 0.011
## d_cr 0.009 -0.028 -0.027 -0.034 1.000 -0.223 -0.048 0.013
## d_gm 0.215 0.178 0.043 0.189 -0.223 1.000 0.145 -0.014
## d_ato 0.337 0.117 -0.044 0.252 -0.048 0.145 1.000 -0.005
## fut_ret 0.053 -0.003 0.001 0.011 0.013 -0.014 -0.005 1.000
m0 <- lm(fut_ret ~ Fscore, data = D) # simple
m1 <- lm(fut_ret ~ F1+F2+F3+F4+F5+F6+F7+F8+F9, data = D) # Model 1
m2 <- lm(fut_ret ~ roa+cfo_ta+d_roa+d_cr+d_gm+d_ato, data = D) # Model 2
cat("Simple OLS (FutRet ~ F-score):\n"); print(summary(m0)$coefficients)
## Simple OLS (FutRet ~ F-score):
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -1.41708 1.36982 -1.0345 0.301049
## Fscore 0.55184 0.25147 2.1945 0.028336
cat(sprintf("R2 = %.4f p = %.4f\n\n", summary(m0)$r.squared, summary(m0)$coefficients[2,4]))
## R2 = 0.0028 p = 0.0283
cat("Model 1 (F1-F9): adj R2 = %.4f\n", summary(m1)$adj.r.squared)
## Model 1 (F1-F9): adj R2 = %.4f
## 0.0028117
print(round(summary(m1)$coefficients,4))
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -1.6698 1.6681 -1.0010 0.3170
## F1 0.4451 1.0785 0.4127 0.6799
## F2 -0.5950 1.2973 -0.4586 0.6466
## F3 2.5851 1.0611 2.4363 0.0149
## F4 1.3082 1.2359 1.0585 0.2900
## F5 0.6639 0.9390 0.7071 0.4796
## F6 -0.3346 0.9350 -0.3579 0.7205
## F7 1.4088 1.2691 1.1101 0.2671
## F8 0.7919 1.0098 0.7842 0.4330
## F9 -1.3255 0.9473 -1.3993 0.1619
cat("\nModel 2 (continuous): adj R2 =", round(summary(m2)$adj.r.squared,4), "\n")
##
## Model 2 (continuous): adj R2 = -0.0029
print(round(summary(m2)$coefficients,4))
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 1.4542 0.4703 3.0922 0.0020
## roa -10.6642 18.3522 -0.5811 0.5613
## cfo_ta 2.1008 10.9252 0.1923 0.8475
## d_roa 11.5939 13.9050 0.8338 0.4045
## d_cr 0.0346 0.0853 0.4058 0.6849
## d_gm -0.8168 1.6824 -0.4855 0.6274
## d_ato -2.3718 8.3764 -0.2831 0.7771
acc <- function(m) mean((predict(m,type="response")>.5) == m$model[[1]])
m3a <- glm(Winner ~ F1+F2+F3+F4+F5+F6+F7+F8+F9, data=D, family=binomial)
m3b <- glm(Winner ~ Fscore+size+pb+roa+debt_r, data=D, family=binomial)
m4 <- glm(Winner ~ roa+cfo_ta+d_roa+d_cr+d_gm+d_ato, data=D, family=binomial)
cat(sprintf("Accuracy 3A=%.3f 3B=%.3f 4=%.3f\n\n", acc(m3a), acc(m3b), acc(m4)))
## Accuracy 3A=0.576 3B=0.562 4=0.535
cat("Model 3B (coef / odds ratio / p):\n")
## Model 3B (coef / odds ratio / p):
co <- summary(m3b)$coefficients
print(round(data.frame(Coef=co[,1], OddsRatio=exp(co[,1]), p=co[,4]), 4))
## Coef OddsRatio p
## (Intercept) -0.1877 0.8289 0.7497
## Fscore 0.1733 1.1893 0.0000
## size -0.0349 0.9657 0.3545
## pb -0.0504 0.9509 0.1852
## roa 1.0593 2.8845 0.5176
## debt_r -0.2773 0.7578 0.2997
library(nnet)
Dm <- D[!is.na(D$Cat5), ]; Dm$Cat5 <- factor(Dm$Cat5)
mm <- multinom(Cat5 ~ Fscore+roa+cfo_ta+d_roa+d_gm+d_ato, data=Dm, trace=FALSE)
cat("5-class accuracy =", round(mean(predict(mm)==Dm$Cat5),3), " (baseline 0.20)\n\n")
## 5-class accuracy = 0.253 (baseline 0.20)
cat("Relative Risk Ratios (vs class 1):\n"); print(round(exp(coef(mm)),3))
## Relative Risk Ratios (vs class 1):
## (Intercept) Fscore roa cfo_ta d_roa d_gm d_ato
## 2 0.617 1.097 0.262 0.058 0.391 1.521 0.838
## 3 0.279 1.274 0.037 0.006 1.313 0.986 0.097
## 4 0.184 1.388 0.334 0.007 11.197 0.924 0.111
## 5 0.267 1.297 14.706 0.018 0.615 1.006 0.409
z <- coef(mm)/summary(mm)$standard.errors
cat("\np-values:\n"); print(round((1-pnorm(abs(z)))*2,4))
##
## p-values:
## (Intercept) Fscore roa cfo_ta d_roa d_gm d_ato
## 2 0.067 0.0797 0.6901 0.1533 0.6854 0.1525 0.9080
## 3 0.000 0.0000 0.3484 0.0147 0.9144 0.9627 0.1314
## 4 0.000 0.0000 0.7511 0.0162 0.3936 0.7977 0.1504
## 5 0.000 0.0000 0.3941 0.0388 0.8383 0.9856 0.5488
fit <- lm(fut_ret ~ Fscore, data=D); b<-coef(fit); r2<-summary(fit)$r.squared
plot(jitter(D$Fscore,1.5), D$fut_ret, pch=16, col=rgb(.17,.42,.72,.25),
xlab="Piotroski F-score", ylab="Next-quarter return (%)",
main="F-score vs. Future Stock Return", ylim=c(-70,80))
abline(fit, col="#c0392b", lwd=2.2)
legend("topleft", bty="n", text.col="#c0392b",
legend=sprintf("y = %.3f + %.3f x\nR2 = %.4f (p=%.3f)",
b[1],b[2],r2,summary(fit)$coefficients[2,4]))
f2<-lm(fut_ret~roa,data=D)
plot(D$roa, D$fut_ret, pch=16, col=rgb(.18,.52,.34,.25),
xlab="ROA (quarterly)", ylab="Next-quarter return (%)",
main="ROA vs. Future Stock Return", ylim=c(-70,80))
abline(f2, col="#c0392b", lwd=2.2)
legend("topleft", bty="n", text.col="#c0392b",
legend=sprintf("y = %.2f + %.2f x\nR2 = %.4f", coef(f2)[1],coef(f2)[2],summary(f2)$r.squared))
g <- tapply(D$fut_ret, D$Fscore, mean)
barplot(g, col="#3182ce", xlab="F-score", ylab="Mean next-quarter return (%)",
main="Average Future Return by F-score"); abline(h=0)
D$yqn <- D$year + (D$quarter-1)/4
tr <- tapply(D$Fscore, D$yqn, mean)
plot(as.numeric(names(tr)), tr, type="o", pch=16, col="#6b46c1",
xlab="Year", ylab="Mean F-score", main="Industry Average F-score Over Time")
write.csv(round(summary(m0)$coefficients,4), "out_OLS_simple.csv")
write.csv(round(summary(m1)$coefficients,4), "out_OLS_model1.csv")
write.csv(round(data.frame(OR=exp(co[,1]),p=co[,4]),4), "out_logit_3B.csv")
write.csv(round(exp(coef(mm)),4), "out_multinom_RRR.csv")
cat("Result tables exported.\n")
## Result tables exported.