I was trying to find out coordinates of the intersection of two curves in R. The input data are coordinates of empirical points from the two curves. My solution is to use the function curve_intersect(). I need to do this for 2000 replications (i.e., 2000 pairs of curves). So I put the data in two lists. Each list contains 1000 data frames with x & y coordinates of one curve in each data frame.
Here is my data: data
Below are the code that I used.
threshold_or1 <- map2_df(recall_or1_4, precision_or1_4,
                         ~curve_intersect(.x, .y, empirical = TRUE, domain = NULL))
# recall_or_4 is a list of 2000 data frames. Each data frame 
# |contains coordinates from curve #1. 
# precision_or_4 is a list of 2000 data frames. Each data frame 
# |contains coordinates from curve #2.
I got this error message below.
Error in uniroot(function(x) curve1_f(x) - curve2_f(x), c(min(curve1$x),  : f() values at end points not of opposite sign
Since the function curve_intersect() can be successfully applied to some individual data frames from the two lists. I ran the following code in order to see exactly which pair of data frames made the process fail.
test <- for (i in 1:2000){
            curve_intersect(recall_or1_4[[i]], precision_or1_4[[i]], empirical = TRUE, domain = NULL)
            print(paste("i=",i))}
Then, I got the following message, which means that the process ran successfully until it reaches the data pair #460. So I checked that individual data pair.
[1] "i= 457"
[1] "i= 458"
[1] "i= 459"
Error in uniroot(function(x) curve1_f(x) - curve2_f(x), c(min(curve1$x),  : f() values at end points not of opposite sign
I plotted data pairs #460.
test1 <- precision_or1_4[[460]] %>% mutate(statistics = 'precision')
test2 <- recall_or1_4[[460]] %>% mutate(statistics = 'recall')
test3 <- rbind(test1, test2)
test3 <- test3 %>% mutate(statistics = as.factor(statistics))
curve_test3 <- ggplot(test3, aes(x = x, y = y))+
        geom_line(aes(colour = statistics))
curve_test3
Find coordinates of the intersection point
I then went to modify the source code of curve_intersect(). The original source code is
    curve_intersect <- function(curve1, curve2, empirical=TRUE, domain=NULL) {
        if (!empirical & missing(domain)) {
                stop("'domain' must be provided with non-empirical curves")
        }
        
        if (!empirical & (length(domain) != 2 | !is.numeric(domain))) {
                stop("'domain' must be a two-value numeric vector, like c(0, 10)")
        }
        
        if (empirical) {
                # Approximate the functional form of both curves
                curve1_f <- approxfun(curve1$x, curve1$y, rule = 2)
                curve2_f <- approxfun(curve2$x, curve2$y, rule = 2)
                
                # Calculate the intersection of curve 1 and curve 2 along the x-axis
                point_x <- uniroot(function(x) curve1_f(x) - curve2_f(x),
                                   c(min(curve1$x), max(curve1$x)))$root
                
                # Find where point_x is in curve 2
                point_y <- curve2_f(point_x)
        } else {
                # Calculate the intersection of curve 1 and curve 2 along the x-axis
                # within the given domain
                point_x <- uniroot(function(x) curve1(x) - curve2(x), domain)$root
                
                # Find where point_x is in curve 2
                point_y <- curve2(point_x)
        }
        
        return(list(x = point_x, y = point_y))
}
I modified the uniroot() part from the third if statement. Instead of using c(min(curve1$x), max(curve1$x)) as an argument of uniroot(), I used lower = -100000000, upper = 100000000. The modified function is
curve_intersect_tq <- function(curve1, curve2, empirical=TRUE, domain=NULL) {
        if (!empirical & missing(domain)) {
                stop("'domain' must be provided with non-empirical curves")
        }
        
        if (!empirical & (length(domain) != 2 | !is.numeric(domain))) {
                stop("'domain' must be a two-value numeric vector, like c(0, 10)")
        }
        
        if (empirical) {
                # Approximate the functional form of both curves
                curve1_f <- approxfun(curve1$x, curve1$y, rule = 2)
                curve2_f <- approxfun(curve2$x, curve2$y, rule = 2)
                
                # Calculate the intersection of curve 1 and curve 2 along the x-axis
                point_x <- uniroot(function(x) curve1_f(x) - curve2_f(x),
                                   lower = -100000000, upper = 100000000)$root
                
                # Find where point_x is in curve 2
                point_y <- curve2_f(point_x)
        } else {
                # Calculate the intersection of curve 1 and curve 2 along the x-axis
                # within the given domain
                point_x <- uniroot(function(x) curve1(x) - curve2(x), domain)$root
                
                # Find where point_x is in curve 2
                point_y <- curve2(point_x)
        }
        
        return(list(x = point_x, y = point_y))
}
I tried to change the values of lower =, upper = arguments. It did not work. I got the same error message as shown below.
curve_intersect_tq(recall_or1_4[[460]], precision_or1_4[[460]], empirical = TRUE, domain = NULL)
Error in uniroot(function(x) curve1_f(x) - curve2_f(x), c(min(curve1$x),  : 
  f() values at end points not of opposite sign
I also tried to use possibly(fun, NA) from the tidyverse package hoping that the process can run even with an error message. It did not work when I used
(1) possibly(curve_intersect(), NA) or
(2) possibly(uniroot(), NA)
The same error message appeared.
Why do I have the error message? What could be possible solutions? Thanks in advance.
                        
Might be a little late to the party, but here's why your code still fails and what you could do, depending on what you want to get out of your analysis:
First of all, the reason why your code fails, even after adaptation, is that you are merely telling
unirootto search a wider window inx. However, the underlying curves will never intersect - there just isn't anycurve1_f(x) - curve2_f(x) == 0to be found.From the doc of
uniroot:In the original
curve_intersectimplementation,unirootis searching the x-interval that is defined in your data (that's thec(min(curve1$x), max(curve1$x))). In your alteration, you're telling it to search in the x interval[-100000000, 100000000]. You could as well have setextendInt = "yes", but it wouldn't change anything.The problem doesn't lie in the search interval, it lies with
approxfun!approxfunmerely helps you by interpolating empirical data between points. Outside of the data you pass in, the returned function wouldn't know what to do.approxfunallows you to specify explicit values forywhich should be returned outside the empirically defined window (with its paramsyleft/yright) or lets you set arulefor each side.In the code you posted above,
rule = 2decides that "the value at the closest data extreme is used". So,approxfundoes not extrapolate the data you pass in. It only extends the known.We can plot how
curve1_fandcurve2_fwill extend outside the empirically defined x-interval into infinity:So, now to what you can do to get your code to not crash:
(spoiler: it pretty much depends on what you're trying to accomplish with your project)
If you don't want to make any assumptions, I'd suggest you wrap your mapped function in a
tryCatchstatement and let it fail where the out-of-the-box solution doesn't give you any results. Let's run this for the part of your list that previously made the whole thing crash:Now, there is just a NA row when
curve_intersectisn't able to give you a result.In this case, we'll use a custom
curve_intersect-function. Let's wrap the problematicunirootcall in atryCatchand if no root can be found, we'll fit almfor each curve and letunirootfind an intersection on the fitted linears.That might or might not make sense in the light of your experiment, so I'll let you be the judge here. And obviously you can use other models than the simplistic
lmif your data is more complex than that...Just to visualize this approach vs the default:
You see, where
approxfun"fails", withlmwe make that assumption that we can extrapolate linearly and find an intersection aroundx = 1.27outside of your observed frame.To go for that second approach and include an extrapolation with
lmin our search, you could throw together something like this:(here, too, only the third
ifis edited.)For your problematic list elements, this now tries to extrapolate with the naively fitted
lmmodel:Hope this helps a little in understanding and fixing your issue :)