1

Preface

Though this problem references the pandas and numpy packages I believe a solution does not require working knowledge of either of these packages.

Setup

I wish to create a dictionary of lambda functions to pass to the formatter argument of the pandas function pandas.DataFrame.to_latex.

I would like the dictionary of lambda functions to format floats to a number of digits as specified by a list.

Example

What I would like to achieve may best be seen by example. Let's set up some floats we'd like to format:

import numpy as np
import pandas as pd

y = np.array([[0.12345, 0.12345, 0.12345]])
colnames = ['col1', 'col2', 'col3']
df = pd.DataFrame(y, columns=colnames)

#print(df)
#      col1     col2     col3
#0  0.12345  0.12345  0.12345

Excellent, now I'd like to format column col1 to show 1 digit after the decimal point. Similarly, I'd like to format col2 to show 2 digits after the decimal point and col3 to display 3 digits. Let's set up a list with this intention:

digits = [1, 2, 3]

From this list we shall create a dict of lambda functions to format the columns, and test the functions after creation.

fun = {}

for id, col in enumerate(['col1', 'col2', 'col3']):
    fun[col] = lambda x : '{{:.{}f}}'.format(digits[id]).format(x)
    print(fun[col](0.12345))
    # Prints 0.1, 0.12, 0.123 (looks to have worked!)

In the code above printing on creation of each entry appears I have achieved what I wished to. However, looking again I see I was mistaken

print(fun['col1'](0.12345)) # Prints 0.123
print(fun['col2'](0.12345)) # Prints 0.123
print(fun['col3'](0.12345)) # Prints 0.123

I understand that these functions all format the float the same as digits[id] = 3 after the loop.

I would like to alter how I create the lambda functions such that we instead observe:

print(fun['col1'](0.12345)) # Prints 0.1
print(fun['col2'](0.12345)) # Prints 0.12
print(fun['col3'](0.12345)) # Prints 0.123

Is it possible to do this? I imagine a solution may involve use of eval but I can't figure it out.

Obvious solution

Outside of the context of pd.DataFrame.to_latex, we could create a single lambda function which takes two arguments and formats floats as desired:

fun = lambda x, digit : '{{:.{}f}}'.format(digit).format(x)

print(fun(0.12345, digits[0])) # Prints 0.1
print(fun(0.12345, digits[1])) # Prints 0.12
print(fun(0.12345, digits[2])) # Prints 0.123

However, as far as I understand the formatter functions passed to pd.DataFrame.to_latex may only take a single argument and so such a solution would not be viable.

jwalton
  • 5,286
  • 1
  • 18
  • 36
  • I guess that the title of your question is misleading. Probably you would like to know more about closures, so you will find several answers to your question for example [here](https://stackoverflow.com/questions/233673/how-do-lexical-closures-work) – JoergVanAken Feb 06 '19 at 11:00
  • @JoergVanAken You're right, I didn't know about closures and struggled to articulate because of this. Your link helped me understand and was very informative. – jwalton Feb 06 '19 at 11:37

1 Answers1

0

You need to bind the value of float resolution at the time of looping, otherwise the 3 lambda closures refers to the same id variable and all of them get the last value assigned to id.

A possible solution using functools.partial

def col_resolution(resolution, x):
    return '{{:.{}f}}'.format(resolution).format(x)

for id, col in enumerate(['col1', 'col2', 'col3']):
    fun[col] = partial(col_resolution, digits[id])

    # one line with lambda
    # fun[col] = partial(lambda res, x : '{{:.{}f}}'.format(res).format(x), digits[id])
attdona
  • 17,196
  • 7
  • 49
  • 60
  • Excellent. This works as desired. I also found ```fun[col] = lambda x, digits=digits[id] : '{{:.{}f}}'.format(digits).format(x)``` binds ```digits[id]``` during the loop. – jwalton Feb 06 '19 at 11:49