Selectively Mapping over elements in a List

23

8

I am using the following code to easily generate a row of images of all eight planets of our Solar System:

Labeled[# // Text, AstronomicalData[#, "Image"]] & /@ 
  AstronomicalData["Planet"] // Row

I like the above code because it is concise and gives nice output. However, if I wanted to do something similar for the moons in our Solar System, I run into the problem that only some moons have images. If I run the following line of code, I get a somewhat sloppy output with Missing Image errors embedded.

Labeled[# // Text, AstronomicalData[#, "Image"]] & /@ 
  AstronomicalData["PlanetaryMoon"] // Row

I was wondering if there is a simple way to Map over a list of data conditionally? That is, if an element in a list does not meet a specific condition then skip over it. Note that I'm looking to avoid an explicit loop with an If construct. I really don't mind using a loop with an If statement, but I was curious if there was a concise idiom that anyone could shed some light on.

Alfred Fazio

Posted 2012-02-03T04:14:57.100

Reputation: 1 115

2This is a beautiful illustration of many ways to solve a problem in Mathematica! – Cassini – 2012-02-03T04:52:08.980

There is a lot of truly excellent feedback on this page. I learned something new from each person's answer. Thank you everyone for contributing. – Alfred Fazio – 2012-02-03T05:06:11.623

Also, thank you rcollyer for cleaning up my post :) – Alfred Fazio – 2012-02-03T05:07:34.867

The more I look at these answers the more I see just how much experience you guys have. Seriously a dizzying array of different approaches. I've learned SOO much more looking up the strange code I've seen here than what I originally came here to learn :D Thanks again everyone! – Alfred Fazio – 2012-02-03T05:18:22.937

You're welcome, and welcome to the community. – rcollyer – 2012-02-03T14:10:38.620

Answers

33

Here's an option which only passes moons with images to the Labeled function:

Labeled[# // Text, AstronomicalData[#, "Image"]] & /@ 
  Select[AstronomicalData["PlanetaryMoon"], 
   ImageQ[AstronomicalData[#, "Image"]] &] // Row

enter image description here

Cassini

Posted 2012-02-03T04:14:57.100

Reputation: 5 128

2This was EXACTLY what I was looking for. Your example uses purely functional programming, gets straight to the point, and avoids any unsightly workarounds. Thank you!! – Alfred Fazio – 2012-02-03T05:03:55.680

15

This is absurd in how it looks, but in this case I don't think skipping over the missing images is necessary. Instead, we are going to redefine what Missing means by attaching an upvalue to it using UpSetDelayed (^:=), as follows

Block[{Missing},
  Labeled[_, _Missing] ^:= Sequence[];
  Labeled[# // Text, AstronomicalData[#, "Image"]]
] & /@ AstronomicalData["PlanetaryMoon"] // Row

and returns what you expect. Block allows you to temporarily redefine what a Symbol means, and in this case, I set Missing["NotAvailable"] to return a Sequence[]. An empty sequence has the special property that it will truncate a list it is found in, for example

{a, b, Sequence[], c}

returns

{a, b, c}

So, by redefining Missing to return an empty Sequence, the code shortens the list for you.

Also, here's a purely functional approach that does not require accessing the database more than twice: once for the list of the moons and once for the image.

labelledMoon[name_String, img_?ImageQ] := Labeled[Text@name, img]
labelledMoon[_, _Missing] := Sequence[]
labelledMoon[#, AstronomicalData[#, "Image"]] & /@ 
  AstronomicalData["PlanetaryMoon"] // Row

This employs the feature of an empty Sequence to automatically shorten the list. Here's a self contained version:

With[{name=#, img = AstronomicalData[#, "Image"]},
  If[Head[img]=!=Missing, 
     Labeled[Text@name, img],
     Unevaluated[Sequence[]]
  ]]& /@  AstronomicalData["PlanetaryMoon"] // Row

which gives the same result. Note, this required the use of Unevaluated to ensure that the empty Sequence was inserted properly into the list.

A functional alternative to Unevaluated[Sequence[]] is to use a SlotSequence. As ##&[1,2,3] returns Sequence[1,2,3], then an empty Sequence can be returned via ##&[]. Here's the shortened version:

With[{name=#, img = AstronomicalData[#, "Image"]},
  If[Head[img]=!=Missing, Labeled[Text@name, img], ##&[]]
]& /@  AstronomicalData["PlanetaryMoon"] // Row

All 4 versions of the code output:

enter image description here

rcollyer

Posted 2012-02-03T04:14:57.100

Reputation: 32 561

15

You will "test" if Missing["NotAvailable"] is returned every time anyway. So a "concise" approach is to filter them out at the end:

Cases[Labeled[# // Text, AstronomicalData[#, "Image"]] & /@ 
   AstronomicalData["PlanetaryMoon"], _[_, _Image]] // Row

I'd be curious to see a more "concise" piece of code. Alternatively instead of using exact pattern _[_, _Image] you could just use _Image and specify all levels in expression to search for that pattern:

Cases[Labeled[# // Text, AstronomicalData[#, "Image"]] & /@ 
   AstronomicalData["PlanetaryMoon"], _Image, Infinity] // Row

But this is a few characters longer ;-)

Vitaliy Kaurov

Posted 2012-02-03T04:14:57.100

Reputation: 66 672

3Or complementarily, DeleteCases[(* stuff *), _Missing]... – J. M.'s ennui – 2012-02-03T04:41:44.193

Thank you for posting this, Vitaliy. I've learned a lot from your example. I would say that this answer comes in second place only because it requires more computation than the answer provided by David Skulsky. Mr. Skulsky's answer avoids running Map on elements unless they meet the criteria. In your answer all elements are computed and then filtered. Either way, this is a great answer. Thank you! – Alfred Fazio – 2012-02-03T05:24:33.037

1+1 for Vitaly's answer because his approach only calls AstronomicalData[#,"Image"] once for each moon, whereas my approach calls it twice. – Cassini – 2012-02-03T05:32:31.083

@J.M. I was rather referring to _Missing part - it won't delete because Labeled wrapped around _Missing. You cracked me up with the "idiom" joke ;) – Vitaliy Kaurov – 2012-02-03T05:33:40.740

Still, the pattern can be changed accordingly. Or, use DeleteCases[] before any further processing's done. :) Anyway, at least you got my intent. – J. M.'s ennui – 2012-02-03T05:48:02.667

14

In situations like your, where you don't know the number of elements in your result in advance, two functions become very handy: Reap and Sow. The approach is to Sow an element you want in your list deep inside the code and to Reap them at the outside.

To get all primes below 11 you can do

Reap[Table[If[PrimeQ[i], Sow[i]], {i, 10}]]

(*
  {{Null, 2, 3, Null, 5, Null, 7, Null, Null, Null}, {{2, 3, 5, 7}}}
*)

Note that Reap gives you two lists, the first one is your original Table result which contains Null when a number is not prime and the last list contains only the Sowed elements. Therefore, your moons are extracted by

Row[Last[Last[Reap[With[{img = AstronomicalData[#, "Image"]},
       If[Head[img] =!= Missing,
        Sow[Labeled[# // Text, img]]]] & /@ 
     AstronomicalData["PlanetaryMoon"]]]]]

halirutan

Posted 2012-02-03T04:14:57.100

Reputation: 109 574

13

Here is yet another approach:

Cases[
  Thread[ AstronomicalData["PlanetaryMoon", #]& /@ {"Name", "Image"} ],
  {n_, i_Image} :> Labeled[Text@n, i]
] // Row

Mr.Wizard

Posted 2012-02-03T04:14:57.100

Reputation: 259 163

8

In version 10, the GeneralUtilities context provides a MapIf function that does exactly this:

<<GeneralUtilities`
MapIf[f, EvenQ, Range@10]
(* {1, f[2], 3, f[4], 5, f[6], 7, f[8], 9, f[10]} *)

As with Map, MapIf can also be used in an operator form with MapIf[f, test].

rm -rf

Posted 2012-02-03T04:14:57.100

Reputation: 85 395

6

For shorter lists you can use patterns. For example you want to apply a function only to the even numbers of a list:

n = RandomInteger[{0, 9}, 8]
{5, 2, 1, 6, 8, 5, 7, 7}
n /. i_?EvenQ -> f[i]
{5, f[2], 1, f[6], f[8], 5, 7, 7}

In your case, you would have to replace the EvenQ by something like a HasImageQ, and f by a function that determines how matching entries have to be displayed.

You may also want to have a look at Cases, which is capable of doing a similar thing.

David

Posted 2012-02-03T04:14:57.100

Reputation: 14 421

5

Seems like I'm a year late to this fun filled party, but here's my take:

names = AstronomicalData["PlanetaryMoon"];
data = {names, AstronomicalData[#, "Image"] & /@ names};
MapThread[Labeled[Text@#1, #2] &, data] /. Labeled[_, _Missing] -> Sequence[] // Row

RunnyKine

Posted 2012-02-03T04:14:57.100

Reputation: 32 260

For what it's worth you don't need pattern names; Labeled[_, _Missing] will work. – Mr.Wizard – 2013-04-15T00:42:30.773

@Mr.Wizard, Ah yes, since I don't refer to them again. Edited to include your suggestion, thanks. – RunnyKine – 2013-04-15T00:45:24.583

1Also, if you are open to funny looking patterns: DeleteCases[MapThread[Labeled[Text@#, #2] &, data], _~_~_Missing] // Row :-) – Mr.Wizard – 2013-04-15T00:46:17.773

@Mr.Wizard, you lost me there, you'll have to explain _~_~_Missing to me. – RunnyKine – 2013-04-15T00:48:41.170

It's equivalent to _[_, _Missing] but in infix form, and without the spacing needed to make that form readable. I don't recommend it in practice! For another humorous pattern see this. Frightening, isn't it? :o)

– Mr.Wizard – 2013-04-15T00:51:26.923

@Mr.Wizard, Thanks for explaining. The pattern from the answer you linked is definitely scary :-) – RunnyKine – 2013-04-15T00:56:22.480