From fb0487328b6e7dba6c9d84e622fbff953351f422 Mon Sep 17 00:00:00 2001 From: Stephan Hageboeck Date: Thu, 18 Sep 2025 09:41:16 +0200 Subject: [PATCH 1/2] [hist] Use WW comparison when TH1::Chi2Test is invoked with TProfiles. The test can only work if the uncertainties are taken into account correctly, so the Chi2Test function was overridden for all TProfile classes. Furthermore, the function will check if the profiles have the correct error option set. See also the discussion in: https://root-forum.cern.ch/t/chi2test-using-tprofile/64156/ --- hist/hist/inc/TProfile.h | 3 ++- hist/hist/inc/TProfile2D.h | 1 + hist/hist/inc/TProfile3D.h | 1 + hist/hist/src/TProfile.cxx | 28 ++++++++++++++++++++++++++++ hist/hist/src/TProfile2D.cxx | 28 ++++++++++++++++++++++++++++ hist/hist/src/TProfile3D.cxx | 28 ++++++++++++++++++++++++++++ 6 files changed, 88 insertions(+), 1 deletion(-) diff --git a/hist/hist/inc/TProfile.h b/hist/hist/inc/TProfile.h index 2149b1b478aea..e1b00a80bd4b2 100644 --- a/hist/hist/inc/TProfile.h +++ b/hist/hist/inc/TProfile.h @@ -89,7 +89,8 @@ class TProfile : public TH1D { Bool_t Add(const TH1 *h1, const TH1 *h2, Double_t c1=1, Double_t c2=1) override; // *MENU* static void Approximate(Bool_t approx=kTRUE); Int_t BufferEmpty(Int_t action=0) override; - void BuildOptions(Double_t ymin, Double_t ymax, Option_t *option); + void BuildOptions(Double_t ymin, Double_t ymax, Option_t *option); + Double_t Chi2Test(const TH1* h2, Option_t *option = "WW", Double_t *res = nullptr) const override; void Copy(TObject &hnew) const override; Bool_t Divide(TF1 *h1, Double_t c1=1) override; Bool_t Divide(const TH1 *h1) override; diff --git a/hist/hist/inc/TProfile2D.h b/hist/hist/inc/TProfile2D.h index f7a0edc755600..564141207c4f3 100644 --- a/hist/hist/inc/TProfile2D.h +++ b/hist/hist/inc/TProfile2D.h @@ -95,6 +95,7 @@ class TProfile2D : public TH2D { static void Approximate(Bool_t approx=kTRUE); void BuildOptions(Double_t zmin, Double_t zmax, Option_t *option); Int_t BufferEmpty(Int_t action=0) override; + Double_t Chi2Test(const TH1* h2, Option_t *option = "WW", Double_t *res = nullptr) const override; void Copy(TObject &hnew) const override; Bool_t Divide(TF1 *h1, Double_t c1=1) override; Bool_t Divide(const TH1 *h1) override; diff --git a/hist/hist/inc/TProfile3D.h b/hist/hist/inc/TProfile3D.h index 4fe600436ff10..93ce0f310e793 100644 --- a/hist/hist/inc/TProfile3D.h +++ b/hist/hist/inc/TProfile3D.h @@ -100,6 +100,7 @@ class TProfile3D : public TH3D { static void Approximate(Bool_t approx=kTRUE); void BuildOptions(Double_t tmin, Double_t tmax, Option_t *option); Int_t BufferEmpty(Int_t action=0) override; + Double_t Chi2Test(const TH1* h2, Option_t *option = "WW", Double_t *res = nullptr) const override; void Copy(TObject &hnew) const override; Bool_t Divide(TF1 *h1, Double_t c1=1) override; Bool_t Divide(const TH1 *h1) override; diff --git a/hist/hist/src/TProfile.cxx b/hist/hist/src/TProfile.cxx index b12163e53c3b5..355baa9df9f82 100644 --- a/hist/hist/src/TProfile.cxx +++ b/hist/hist/src/TProfile.cxx @@ -416,6 +416,34 @@ Int_t TProfile::BufferFill(Double_t x, Double_t y, Double_t w) return -2; } +//////////////////////////////////////////////////////////////////////////////// +/// Run a Chi2Test between a TProfileD and another histogram. +/// If the argument is also a TProfileD, this calls TH1::Chi2Test() with the option "WW". +/// \see TH1::Chi2Test() + +Double_t TProfile::Chi2Test(const TH1 *h2, Option_t *option, Double_t *res) const +{ + TString opt = option; + opt.ToUpper(); + + if (auto other = dynamic_cast(h2); other) { + if (fErrorMode != kERRORMEAN || other->fErrorMode != kERRORMEAN) { + Error("Chi2Test", "Chi2 tests need TProfiles in 'error of mean' mode."); + return 0; + } + + opt += "WW"; + opt.ReplaceAll("UU", "WW"); + opt.ReplaceAll("UW", "WW"); + } else if (!opt.Contains("WW")) { + Error("Chi2Test", "TProfiles need to be tested with the 'W' option. Either use option 'WW' or use " + "histogram.Chi2Test(, 'UW')"); + return 0; + } + + return TH1::Chi2Test(h2, opt, res); +} + //////////////////////////////////////////////////////////////////////////////// /// Copy a Profile histogram to a new profile histogram. diff --git a/hist/hist/src/TProfile2D.cxx b/hist/hist/src/TProfile2D.cxx index dae1db38f7baa..d15fd3f169bce 100644 --- a/hist/hist/src/TProfile2D.cxx +++ b/hist/hist/src/TProfile2D.cxx @@ -379,6 +379,34 @@ Int_t TProfile2D::BufferFill(Double_t x, Double_t y, Double_t z, Double_t w) return -2; } +//////////////////////////////////////////////////////////////////////////////// +/// Run a Chi2Test between a TProfile2D and another histogram. +/// If the argument is also a TProfile2D, this calls TH1::Chi2Test() with the option "WW". +/// \see TH1::Chi2Test() + +Double_t TProfile2D::Chi2Test(const TH1 *h2, Option_t *option, Double_t *res) const +{ + TString opt = option; + opt.ToUpper(); + + if (auto other = dynamic_cast(h2); other) { + if (fErrorMode != kERRORMEAN || other->fErrorMode != kERRORMEAN) { + Error("Chi2Test", "Chi2 tests need TProfiles in 'error of mean' mode."); + return 0; + } + + opt += "WW"; + opt.ReplaceAll("UU", "WW"); + opt.ReplaceAll("UW", "WW"); + } else if (!opt.Contains("WW")) { + Error("Chi2Test", "TProfiles need to be tested with the 'W' option. Either use option 'WW' or use " + "histogram.Chi2Test(, 'UW')"); + return 0; + } + + return TH1::Chi2Test(h2, opt, res); +} + //////////////////////////////////////////////////////////////////////////////// /// Copy a Profile2D histogram to a new profile2D histogram. diff --git a/hist/hist/src/TProfile3D.cxx b/hist/hist/src/TProfile3D.cxx index bc3fde80da17c..252f1b1e49da7 100644 --- a/hist/hist/src/TProfile3D.cxx +++ b/hist/hist/src/TProfile3D.cxx @@ -343,6 +343,34 @@ Int_t TProfile3D::BufferFill(Double_t x, Double_t y, Double_t z, Double_t t, Dou return -2; } +//////////////////////////////////////////////////////////////////////////////// +/// Run a Chi2Test between a TProfile3D and another histogram. +/// If the argument is also a TProfile3D, this calls TH1::Chi2Test() with the option "WW". +/// \see TH1::Chi2Test() + +Double_t TProfile3D::Chi2Test(const TH1 *h2, Option_t *option, Double_t *res) const +{ + TString opt = option; + opt.ToUpper(); + + if (auto other = dynamic_cast(h2); other) { + if (fErrorMode != kERRORMEAN || other->fErrorMode != kERRORMEAN) { + Error("Chi2Test", "Chi2 tests need TProfiles in 'error of mean' mode."); + return 0; + } + + opt += "WW"; + opt.ReplaceAll("UU", "WW"); + opt.ReplaceAll("UW", "WW"); + } else if (!opt.Contains("WW")) { + Error("Chi2Test", "TProfiles need to be tested with the 'W' option. Either use option 'WW' or use " + "histogram.Chi2Test(, 'UW')"); + return 0; + } + + return TH1::Chi2Test(h2, opt, res); +} + //////////////////////////////////////////////////////////////////////////////// /// Copy a Profile3D histogram to a new profile2D histogram. From 3c61b5bf8e1fd2c2114c299b9b5f20f82e3167ea Mon Sep 17 00:00:00 2001 From: Stephan Hageboeck Date: Fri, 19 Sep 2025 18:00:41 +0200 Subject: [PATCH 2/2] [hist] Add test file for TProfiles, testing Chi2Test(). --- hist/hist/test/CMakeLists.txt | 1 + hist/hist/test/test_TProfile.cxx | 93 ++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 hist/hist/test/test_TProfile.cxx diff --git a/hist/hist/test/CMakeLists.txt b/hist/hist/test/CMakeLists.txt index 9e5838e616ad8..b22647a8c6088 100644 --- a/hist/hist/test/CMakeLists.txt +++ b/hist/hist/test/CMakeLists.txt @@ -29,6 +29,7 @@ ROOT_ADD_GTEST(testTGraphSorting test_TGraph_sorting.cxx LIBRARIES Hist) ROOT_ADD_GTEST(testSpline test_spline.cxx LIBRARIES Hist) ROOT_ADD_GTEST(testTF1Simple test_tf1_simple.cxx LIBRARIES Hist RIO) ROOT_ADD_GTEST(testTF1DrawCopy test_tf1_drawcopy.cxx LIBRARIES Hist) +ROOT_ADD_GTEST(test_TProfile test_TProfile.cxx LIBRARIES Hist) if(fftw3) ROOT_ADD_GTEST(testTF1 test_tf1.cxx LIBRARIES Hist) diff --git a/hist/hist/test/test_TProfile.cxx b/hist/hist/test/test_TProfile.cxx new file mode 100644 index 0000000000000..c08d0d2563608 --- /dev/null +++ b/hist/hist/test/test_TProfile.cxx @@ -0,0 +1,93 @@ +#include "gtest/gtest.h" + +#include "TProfile.h" +#include "TProfile2D.h" +#include "TProfile3D.h" +#include "TRandom.h" + +#include "ROOT/TestSupport.hxx" + +template +void runTest(T const &reference, T const &sameDistr, T const &differentDistr) +{ + EXPECT_EQ(reference.Chi2Test(&sameDistr), reference.Chi2Test(&sameDistr, "WW")); + EXPECT_EQ(reference.Chi2Test(&sameDistr), + reference.Chi2Test(&sameDistr, "P WW UW")); // Need more than just the default option + EXPECT_EQ(reference.Chi2Test(&sameDistr), reference.Chi2Test(&sameDistr, "UW")); + EXPECT_EQ(reference.Chi2Test(&sameDistr), reference.Chi2Test(&sameDistr, "UU")); + EXPECT_EQ(reference.Chi2Test(&sameDistr), reference.Chi2Test(&sameDistr, "P UU")); + + const double probSuccess = reference.Chi2Test(&sameDistr, "P"); + EXPECT_GT(probSuccess, 0.1); + EXPECT_LE(probSuccess, 1.); + const double probFail = reference.Chi2Test(&differentDistr, "P"); + EXPECT_LT(probFail, 0.05); +} + +TEST(TProfile, Chi2Test) +{ + TProfile reference("reference", "reference", 10, 0, 10); + TProfile sameDistr("sameDistr", "sameDistr", 10, 0, 10); + TProfile differentDistr("differentDistr", "differentDistr", 10, 0, 10); + + gRandom->SetSeed(1); + for (unsigned int i = 0; i < 100000; i++) { + const double x = gRandom->Uniform(10.); + reference.Fill(x, gRandom->Gaus(5 + x / 2, 5.)); + sameDistr.Fill(x, gRandom->Gaus(5 + x / 2, 5.)); + differentDistr.Fill(x, gRandom->Gaus(20, 1.)); + } + + runTest(reference, sameDistr, differentDistr); +} + +TEST(TProfile, Chi2TestWithWrongErrors) +{ + TProfile reference("reference", "reference", 10, 0, 10); + reference.Fill(1, 2); + reference.Fill(1, 3); + + for (auto err : {"s", "i", "g"}) { + ROOT::TestSupport::CheckDiagsRAII checkDiag(kError, "TProfile::Chi2Test", "error of mean", false); + + TProfile sameDistr("sameDistr", "sameDistr", 10, 0, 10, err); + sameDistr.Fill(1, 2); + sameDistr.Fill(1, 3); + + reference.Chi2Test(&sameDistr); + } +} + +TEST(TProfile2D, Chi2Test) +{ + TProfile2D reference("reference", "reference", 10, 0, 10, 10, 0, 10); + TProfile2D sameDistr("sameDistr", "sameDistr", 10, 0, 10, 10, 0, 10); + TProfile2D differentDistr("differentDistr", "differentDistr", 10, 0, 10, 10, 0, 10); + + gRandom->SetSeed(1); + for (unsigned int i = 0; i < 50000; i++) { + const double x = gRandom->Uniform(10.); + reference.Fill(x, x + 1., gRandom->Gaus(5 + x / 2, 5.)); + sameDistr.Fill(x, x + 1., gRandom->Gaus(5 + x / 2, 5.)); + differentDistr.Fill(x, x + 1., gRandom->Gaus(20, 1.)); + } + + runTest(reference, sameDistr, differentDistr); +} + +TEST(TProfile3D, Chi2Test) +{ + TProfile3D reference("reference", "reference", 10, 0, 10, 11, 0, 11, 12, 0, 12); + TProfile3D sameDistr("sameDistr", "sameDistr", 10, 0, 10, 11, 0, 11, 12, 0, 12); + TProfile3D differentDistr("differentDistr", "differentDistr", 10, 0, 10, 11, 0, 11, 12, 0, 12); + + gRandom->SetSeed(1); + for (unsigned int i = 0; i < 50000; i++) { + const double x = gRandom->Uniform(10.); + reference.Fill(x, x + 1., x + 2., gRandom->Gaus(5 + x / 2, 5.)); + sameDistr.Fill(x, x + 1., x + 2., gRandom->Gaus(5 + x / 2, 5.)); + differentDistr.Fill(x, x + 1., x + 2., gRandom->Gaus(20, 1.)); + } + + runTest(reference, sameDistr, differentDistr); +} \ No newline at end of file