20
$\begingroup$

This might seem like an overly simple question, but I need to specify custom plot tick marks as integers (no trailing decimal point) if they are approximately integers, but not if they are not. Using Rationalize on all the tick values won't work because I don't want ticks in the form of $\frac{3}{2}$.

Consider:

roundif = If[Chop[# - Floor[#]] == 0, Rationalize[#], #] & 

Some tests to show it works as intended:

roundif /@ {-1., -1, 0, 0.5,  1500, 1501., 1501.2}

(* {-1, -1, 0, 0.5, 1500, 1501, 1501.2} *)

roundif /@ Range[-3, 3, 0.5]

(*  {-3, -2.5, -2, -1.5, -1, -0.5, 0, 0.5, 1, 1.5, 2, 2.5, 3} *)

(Of course, I could make it a normal SetDelayed function and make its Attributes include Listable.)

Timing seems to be linear in the length of the list and the number of times it is performed.

testdata = Range[-30, 30, 0.5];   
Do[roundif /@ testdata, {10000}]; // AbsoluteTiming    
{5.8656000, Null}

Is this the most efficient way to do this? Have I missed some subtlety?

$\endgroup$
2
  • 3
    $\begingroup$ Is this likely to be a bottle neck in your application? $\endgroup$
    – Ajasja
    Jun 26, 2012 at 8:21
  • $\begingroup$ @Ajasja, well, no, I'm more concerned that I've missed some subtle edge case. $\endgroup$
    – Verbeia
    Jun 26, 2012 at 8:26

5 Answers 5

21
$\begingroup$

Stan Wagon presents a little utility function in his book Mathematica in Action called IntegerChop[]. Here's a slightly wrinkled version:

IntegerChop = With[{r = Round[#]}, r + Chop[# - r]] &;

You might wish to do comparisons yourself (the computer I am using does not have Mathematica).


Here are some benchmarks:

Do[roundif /@ testdata, {10000}]; // AbsoluteTiming  
(* ==> {4.7382710, Null} *)

Do[IntegerChop /@ testdata, {10000}]; // AbsoluteTiming
(* ==> {4.6512660, Null} *)

So basically no difference.

$\endgroup$
2
  • 5
    $\begingroup$ @Ajasja Since Round and Chop are listable, IntegerChop also is, so you could do IntegerChop[testdata], which might be a bit faster $\endgroup$
    – rm -rf
    Jun 26, 2012 at 15:05
  • $\begingroup$ @R.M quite right, and on my tests, mapping it (/@) is an order of magnitude slower than just using the function (@) or IntegerChop[data]. $\endgroup$
    – Verbeia
    Jun 27, 2012 at 10:09
13
$\begingroup$

EDIT

As Chris pointed out Floor used this way fails for cases where the value is slightly less than a whole number, whereas Round works. I shall edit the remainder of my answer to correct this oversight.


If you are using x == 0 you shouldn't need Chop since it is already making a numeric comparison:

If[# - Round[#] == 0, Round[#], #] &

Or simply:

If[Round@# == #, Round@#, #] &

The code from Jerry's answer is actually faster with Function rather than With as there is less overhead:

With[{r = Round[#]}, r + Chop[# - r]] & /@ Range[0, 1*^6, 0.5]; // Timing

# + Chop[#2 - #]&[Round@#, #] & /@ Range[0, 1*^6, 0.5];        // Timing

{3.697, Null}

{2.683, Null}

Either form is built from Listable functions and is therefore listable, able to be applied directly to the list without Map:

With[{r = Round[#]}, r + Chop[# - r]] & @ Range[0, 1*^6, 0.5]; // Timing

# + Chop[#2 - #]&[Round@#, #] & @ Range[0, 1*^6, 0.5];        // Timing

{0.499, Null}

{0.483, Null}

$\endgroup$
3
  • $\begingroup$ I hadn't even read the last part of your answer, and missed the simplest and fastest solution, and started compiling. Shameful +1!! $\endgroup$
    – Rojo
    Jun 26, 2012 at 14:35
  • $\begingroup$ @Rojo Thanks, and sorry. :-o I'll try to make the best part of my answers more visible in the future. $\endgroup$
    – Mr.Wizard
    Jun 27, 2012 at 1:54
  • 2
    $\begingroup$ Is your edited function significantly different from IntegerChop? $\endgroup$
    – user1066
    Jun 27, 2012 at 16:51
10
$\begingroup$

Some of the above don't work in some cases due to machine approximation, e.g.

x = 6250*0.292

1825.

If[# - ⌊#⌋ == 0, Round@#, #] &[x]

1825.

Chop[# - ⌊#⌋] + ⌊#⌋ &[x]

1825.

IntegerPart@# + Chop@FractionalPart@# &[x]

1825.

But Stan Wagon's method works:

With[{r = Round[#]}, r + Chop[# - r]] &[x]

1825

$\endgroup$
2
  • 2
    $\begingroup$ Good catch. I didn't think this through very well as my version misses values that are slightly less than a whole number. $\endgroup$
    – Mr.Wizard
    Jun 27, 2012 at 9:43
  • 2
    $\begingroup$ Very well spotted, and shows the flaw in my original version as well. $\endgroup$
    – Verbeia
    Jun 27, 2012 at 10:10
7
$\begingroup$

Ok, if you want it faster still, and your close to integer numbers are machine-size integers - here are two equivalent implementations - in Mathematica compiled to C, and Java. It is an interesting problem to compare performance, we will observe that Java code is speed-equivalent to C code here, modulo small extra time needed for data transfer.

The idea is to obtain a list of integers from those numbers close to ones, and a list of their positions (this is close in spirit to what @Rojo did in his now deleted answer). But then, I will create a copy of the original list and modify it in-place with Part. So, our top-level function is then

ClearAll[roundClose];
roundClose[data_, f_] :=
  Module[{copy = data},
   (copy[[#[[2]]]] = #[[1]]) &[ f[copy]];
   copy];

where f is a function which returns a list {ints, positions}.

Using Compile

Here is a function using Compile:

fn = 
  Compile[{{data, _Real, 1}},
    Module[{i = 1, ctr = 0, ints = Table[0, {Length[data]}], 
        pos =  Table[0, {Length[data]}]},
      Do[
        If[data[[i]] == Floor[data[[i]]],
          ints[[++ctr]] = Round[data[[i]]];
          pos[[ctr]] = i
        ],
        {i, Length[data]}
      ];
      {Take[ints, ctr], Take[pos, ctr]}
    ],
    CompilationTarget -> "C",
    RuntimeOptions -> "Speed"]

Using Java

Assuming that we have the Java reloader loaded, we compile this class:

JCompileLoad@
"import java.util.Arrays;

public class RoundCloseToInteger{
    public static int [][] roundClose(double [] nums ){
        int[] resultNums = new int[nums.length];
        int[] resultPos = new int[nums.length];
        int ctr = 0;        
        for(int i=0;i<nums.length;i++){
            double num = nums[i];           
            if(num ==((double)Math.floor(num))) {               
                 resultNums[ctr]=(int)Math.round(num);
                 resultPos[ctr++]=i+1;          
            }
        }
        resultNums = Arrays.copyOf(resultNums,ctr);
        resultPos = Arrays.copyOf(resultPos,ctr);
        return new int[][]{resultNums,resultPos};       
    }
 }"

The function to be used is then RoundCloseToInteger`roundClose.

Benchmarks

Here is a test data:

ld = Range[0, 1*^6, 0.5];

Testing now:

(r1 = Chop[# - Floor@#] + Floor@# &@ld);//AbsoluteTiming
(r2 = roundClose[ld,RoundCloseToInteger`roundClose]);//AbsoluteTiming
(r3 = roundClose[ld,fn]);//AbsoluteTiming

(*
   {1.6113282,Null}
   {0.5341797,Null}
   {0.4873047,Null}
*)

r1==r2==r3

(*  True  *)

Remarks

We get roughly the same 3x speed-up with both compiled to C and Java versions, w.r.t. the code of @Mr.Wizard. The reason for it is that, for such light-weight operations as Floor or Round, the time scales linearly with the numbers of runs through the list, which is 3 for the code of @Mr.Wizard and only 1 for the present code.

We can see that for such long lists, Java code is pretty much speed-equivalent to C code generated by Compile. One can experiment with data transfer and confirm that the timing difference is of the same order as needed to transfer back and forth the data. However, the smaller the size of the list, the more overhead will be encoutered for java calls, in proportion to the total running time.

I included the Java solution because it is a good and simple case study to see the relative speed on (Compile-generated) C vs Java, in a simple setting. To my mind, this shows that Java is a viable alternative. One advantage of Java is that it is cross-platform, meaning that once you compiled a given class on some machine, you can bring it to a computer not equipped with C compiler, and it will run there, and also, you won't face a compilation to C overhead.

$\endgroup$
7
  • $\begingroup$ +1. Btw, do you see a better way to do the simple following operation than what I did with Extract? Given 3 lists of equal length, build an equal sized fourth list such that the third is an indicator of whether to take the element from the first or second list. e.g l1={a, b, c};l2={aa, bb, cc}; indicator={1, 2, 1}; output is {a, bb, c} $\endgroup$
    – Rojo
    Jun 26, 2012 at 23:19
  • $\begingroup$ @Rojo Thanks. How about Unitize[indicator - 2]*l1 + Unitize[indicator - 1]*l2? $\endgroup$ Jun 26, 2012 at 23:28
  • $\begingroup$ Thanks! That was the kind of code I had the hunch I wasn't seing $\endgroup$
    – Rojo
    Jun 26, 2012 at 23:31
  • $\begingroup$ Great answer; +1! I was recently surprised to find that scimark2 gives significantly higher benchmark numbers running under Java 7 than the C version of the same code does after being compiled with gcc 4.6.1 using optimized switches. It looks like the days of Java (and other JVM languages such as Scala or Clojure) being slower than C are now just a memory. $\endgroup$ Jun 28, 2012 at 23:49
  • $\begingroup$ @OleksandrR. Thanks! As to Java vs C, I think the reasons may include better optimizations possible for HotSpot JIT (because it has all the run-time information), and also modern garbage collectors may be superior to manual memory management for complex programs. A similar (in terms of optimization) situation happens with Python used with the PyPy project, since they are able to optimize globally, while C compilers can not optimize across the libraries boudaries. $\endgroup$ Jun 29, 2012 at 9:02
7
$\begingroup$

I like this for readability:

roundif = IntegerPart@# + Chop@FractionalPart@# &

It's also listable and fairly fast.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.