From d6841188ce345abc91de13abacd976154f613819 Mon Sep 17 00:00:00 2001 From: Matthieu Gomez Date: Mon, 31 Aug 2026 17:33:51 -0400 Subject: [PATCH 1/2] Improve results display - Show dependent variable in the header (closes #237) - Mark IV models in the title - Format the F-test p-value like coefficient p-values - Add thousands separator to number of obs - Move Converged next to Iterations in the fixed-effect block - Widen the table when a header pair does not fit in half of it --- README.md | 9 +++++---- src/FixedEffectModel.jl | 34 +++++++++++++++++++++------------- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index fa3c9528..999f5363 100755 --- a/README.md +++ b/README.md @@ -17,13 +17,14 @@ The objective of the package is similar to the Stata command [`reghdfe`](https:/ using DataFrames, RDatasets, FixedEffectModels df = dataset("plm", "Cigar") reg(df, @formula(Sales ~ NDI + fe(State) + fe(Year)), Vcov.cluster(:State), weights = :Pop) -# FixedEffectModel +# FixedEffectModel # ========================================================================= -# Number of obs: 1380 Converged: true +# Dependent variable: Sales Number of obs: 1,380 # dof (model): 1 dof (residuals): 45 -# R²: 0.803 R² adjusted: 0.798 -# F-statistic: 13.3382 P-value: 0.001 +# R²: 0.803 R² adjusted: 0.791 +# F-statistic: 13.3382 P-value: 0.0007 # R² within: 0.139 Iterations: 5 +# Converged: true # ========================================================================= # Estimate Std. Error t-stat Pr(>|t|) Lower 95% Upper 95% # ───────────────────────────────────────────────────────────────────────── diff --git a/src/FixedEffectModel.jl b/src/FixedEffectModel.jl index ee125f13..6a8c8c5d 100644 --- a/src/FixedEffectModel.jl +++ b/src/FixedEffectModel.jl @@ -299,37 +299,38 @@ end ## ############################################################################## +import StatsBase: NoQuote, PValue + function top(m::FixedEffectModel) out = [ - "Number of obs" sprint(show, nobs(m), context = :compact => true); - "Converged" m.converged; + "Dependent variable" string(responsename(m)); + "Number of obs" replace(string(nobs(m)), r"(?<=\d)(?=(\d{3})+$)" => ","); "dof (model)" sprint(show, dof(m), context = :compact => true); "dof (residuals)" sprint(show, dof_residual(m), context = :compact => true); "R²" @sprintf("%.3f",r2(m)); "R² adjusted" @sprintf("%.3f",adjr2(m)); "F-statistic" sprint(show, m.F, context = :compact => true); - "P-value" @sprintf("%.3f",m.p); + "P-value" sprint(show, PValue(m.p)); ] if has_iv(m) - out = vcat(out, + out = vcat(out, [ "F-statistic (first stage)" sprint(show, m.F_kp, context = :compact => true); - "P-value (first stage)" @sprintf("%.3f",m.p_kp); + "P-value (first stage)" sprint(show, PValue(m.p_kp)); ]) end if has_fe(m) - out = vcat(out, + out = vcat(out, [ "R² within" @sprintf("%.3f",m.r2_within); "Iterations" sprint(show, m.iterations, context = :compact => true); + "Converged" m.converged; ]) end return out end - -import StatsBase: NoQuote, PValue function Base.show(io::IO, m::FixedEffectModel) ct = coeftable(m) #copied from show(iio,cf::Coeftable) @@ -353,22 +354,29 @@ function Base.show(io::IO, m::FixedEffectModel) totwidth = sum(sum.(A)) + 2 * (length(A) - 1) - #intert my stuff which requires totwidth - ctitle = string(typeof(m)) - halfwidth = div(totwidth - length(ctitle), 2) - print(io, " " ^ halfwidth * ctitle * " " ^ halfwidth) + #insert my stuff which requires totwidth ctop = top(m) for i in 1:size(ctop, 1) ctop[i, 1] = ctop[i, 1] * ":" end + # widen the table if a label/value pair does not fit in half of it: + # lpad would otherwise glue the value to its label and shift the row + needed = maximum(length(ctop[i, 1]) + 1 + length(string(ctop[i, 2])) for i in 1:size(ctop, 1)) + totwidth = max(totwidth, 2 * needed + 2) + ctitle = string(typeof(m)) + if has_iv(m) + ctitle *= " (IV)" + end + halfwidth = div(totwidth - length(ctitle), 2) + print(io, " " ^ halfwidth, ctitle) println(io, '\n', repeat('=', totwidth)) halfwidth = div(totwidth, 2) - 1 interwidth = 2 + mod(totwidth, 2) for i in 1:(div(size(ctop, 1) - 1, 2)+1) print(io, ctop[2*i-1, 1]) print(io, lpad(ctop[2*i-1, 2], halfwidth - length(ctop[2*i-1, 1]))) - print(io, " " ^interwidth) if size(ctop, 1) >= 2*i + print(io, " " ^interwidth) print(io, ctop[2*i, 1]) print(io, lpad(ctop[2*i, 2], halfwidth - length(ctop[2*i, 1]))) end From a20ce1d261a3c1b19882df33c790c19f54d0fe13 Mon Sep 17 00:00:00 2001 From: Matthieu Gomez Date: Mon, 31 Aug 2026 17:40:10 -0400 Subject: [PATCH 2/2] Show a warning on non-convergence instead of a Converged row --- README.md | 1 - src/FixedEffectModel.jl | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 999f5363..891a0419 100755 --- a/README.md +++ b/README.md @@ -24,7 +24,6 @@ reg(df, @formula(Sales ~ NDI + fe(State) + fe(Year)), Vcov.cluster(:State), weig # R²: 0.803 R² adjusted: 0.791 # F-statistic: 13.3382 P-value: 0.0007 # R² within: 0.139 Iterations: 5 -# Converged: true # ========================================================================= # Estimate Std. Error t-stat Pr(>|t|) Lower 95% Upper 95% # ───────────────────────────────────────────────────────────────────────── diff --git a/src/FixedEffectModel.jl b/src/FixedEffectModel.jl index 6a8c8c5d..0faa2685 100644 --- a/src/FixedEffectModel.jl +++ b/src/FixedEffectModel.jl @@ -324,7 +324,6 @@ function top(m::FixedEffectModel) [ "R² within" @sprintf("%.3f",m.r2_within); "Iterations" sprint(show, m.iterations, context = :compact => true); - "Converged" m.converged; ]) end return out @@ -385,6 +384,9 @@ function Base.show(io::IO, m::FixedEffectModel) # rest of coeftable code println(io, repeat('=', totwidth)) + if has_fe(m) && !m.converged + println(io, "Warning: demeaning did not converge; estimates may be unreliable") + end print(io, repeat(' ', sum(A[1]))) for j in 1:length(colnms) print(io, " ", lpad(colnms[j], sum(A[j+1])))