Skip to content

Commit

Permalink
Now cld4py can add letters to seaborn/matplotlib figures
Browse files Browse the repository at this point in the history
  • Loading branch information
timyerg committed Dec 7, 2024
1 parent 517b3eb commit 8472938
Show file tree
Hide file tree
Showing 18 changed files with 172 additions and 120 deletions.
2 changes: 2 additions & 0 deletions cld4py/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .cld4py import assign_letters
from .cld4py import plot_letters
Binary file added cld4py/__pycache__/__init__.cpython-310.pyc
Binary file not shown.
Binary file added cld4py/__pycache__/__init__.cpython-38.pyc
Binary file not shown.
Binary file added cld4py/__pycache__/__init__.cpython-39.pyc
Binary file not shown.
Binary file added cld4py/__pycache__/assign_letters.cpython-310.pyc
Binary file not shown.
Binary file added cld4py/__pycache__/assign_letters.cpython-38.pyc
Binary file not shown.
Binary file added cld4py/__pycache__/cld4py.cpython-310.pyc
Binary file not shown.
Binary file added cld4py/__pycache__/cld4py.cpython-38.pyc
Binary file not shown.
Binary file added cld4py/__pycache__/cld4py.cpython-39.pyc
Binary file not shown.
Binary file added cld4py/__pycache__/cld4py_main.cpython-310.pyc
Binary file not shown.
168 changes: 168 additions & 0 deletions cld4py/cld4py.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import string
import pandas as pd


def assign_letters(df, G1, G2, P, alpha=.05,
order=None, data=None, vals=None, group=None):
"""
Function to apply compact letter display for pairwise contrasts.
Groups with no significant differences share a letter.
Parameters:
Required:
df - dataframe with contrasts (pairwise comparisons).
G1, G2 - columns in contrasts df with compared groups.
P - column in contrasts df with p-value (adjusted, right?).
Optional:
alpha - sigificance level (default 0.05).
order - None (default), list or ['ascending', 'descending'].
This parameter will define the order of assigned letters.
None - alphabetical order will be applied.
List - order of groups will be defined by that list.
String 'ascending' or 'descending' requires parameters
'data', 'values' and 'group' to order groups by the mean.
data - dataframe with values that were compared to get contrasts.
vals - column in data with compared values.
group - column in data with group information.
"""

#helper function to check p
def is_sign(lv1, lv2, df=df, alpha=alpha):
return df.loc[(df[G1]==lv1)&(df[G2]==lv2)|(df[G1]==lv2)&(df[G2]==lv1),P].iloc[0] < alpha

letters = string.ascii_lowercase
df[P] = df[P].apply(pd.to_numeric)

#define order
if order == None:
order = sorted(set(df[G1].tolist() + df[G2].tolist()))
if order in ['ascending', 'descending']:
asc = order=='ascending'
data[vals] = data[vals].apply(pd.to_numeric)
order = pd.DataFrame(data.groupby(group)[vals].mean()).sort_values(vals, ascending=asc).index.tolist()

#assign letters
draft, sets = {}, []
for i, l1 in enumerate(order):
draft.update({i: {l1}})
for l2 in order:
if l1 != l2 and not any([is_sign(l1,l2)]+[is_sign(l,l2) for l in draft[i]]):
draft[i].add(l2)
[sets.append(v) for v in draft.values() if v not in sets]
cld = pd.DataFrame(columns=['Group', 'Letters'])
for i,l in enumerate(order):
cld.loc[i, ['Group', 'Letters']] = l, ''.join([letters[j] for j,s in enumerate(sets) if l in s])
return cld.set_index('Group')


def plot_letters(cld, data, vals, group, figax,
axis='x', plot='boxplot', pos='upper',
pad=1, c='black', fs=None, lim=0):
"""
Function to plot CLD letters for sns boxplot, violinplot, barplot or swarmplot.
Groups with no significant differences share a letter.
Parameters:
Required:
cld - dataframe or dictionary with groups and letters. If df then groups
should be in the index and letters in the "Letters" column.
data - dataframe with values that were plotted.
vals - column in data with compared values.
group - column in data with group information.
figax - matplotlib or sns figure or ax with plot.
Optional:
axis - axis with plotted groups: "x" or "y" (default "x").
plot - plot type: "boxplot", "violinplot", "barplot" or "swarmplot"
(default "boxplot").
pos - letters position: "upper", "lower", "top" or "bottom"
(default: "upper"). If axis = "y", "upper" and "top" will be
plotted on the right side, "lower" and "bottom" - on the left.
pad - distance (% of data range) to the plotted group object (default 1).
c - color of letters.
fs - fontsize of letters.
lim - increase axes limits, expressed in "pad" (see above) values (default 0)
"""

#import
import seaborn as sns
import matplotlib.pyplot as plt
from matplotlib.cbook import boxplot_stats

types = ['boxplot', 'violinplot', 'barplot', 'swarmplot']
labels = figax.get_xticklabels() if axis=="x" else figax.get_yticklabels()
step = (data[vals].max() - data[vals].min())/100 * pad

#check parameters
if axis not in "xy":
raise ValueError('axis should by either "x" or "y"')
if plot not in types:
raise ValueError(f'"{plot}" is not in the list of supported types')
if pos not in ["upper", "lower", "top", "bottom"]:
raise ValueError('pos should by either "upper", "lower", "top" or "bottom"')
if not any([isinstance(cld, pd.DataFrame), isinstance(cld, dict)]):
raise ValueError('cld should by either dataframe or dictionary')
if fs == None:
fs = labels[0].get_fontsize()

#set limits
if axis=="y":
va = 'center'
lims = figax.get_xlim()
figax.set_xlim(min(lims)-lim*step, max(lims)+lim*step)
if axis=="x":
lims = figax.get_ylim()
figax.set_ylim(min(lims)-lim*step, max(lims)+lim*step)

#plot letters
pos_dict = {
'top': max(figax.get_ylim())-step if axis=='x' else max(figax.get_xlim())-step,
'bottom': min(figax.get_ylim())+step if axis=='x' else min(figax.get_xlim())+step}

if axis=='x':
ha = 'center'
va = 'bottom' if pos in ['upper', 'bottom'] else 'top'
if axis=='y':
va = 'center'
ha = 'left' if pos in ['upper', 'bottom'] else 'right'
if not isinstance(cld, dict):
cld = {i: cld.loc[i, 'Letters'] for i in cld.index}


for i, label in enumerate(labels):
df = data.loc[data[group]==label.get_text()]
rel_step = (df[vals].max() - df[vals].min())/1.7
x, y = label.get_position()

#boxplot
if plot == 'boxplot':
pos_dict.update({'upper': boxplot_stats(df[vals])[0]['whishi'] + step})
pos_dict.update({'lower': boxplot_stats(df[vals])[0]['whislo'] - step})

#violinplot
if plot == 'violinplot':
pos_dict.update({'upper': df[vals].max() + rel_step + step})
pos_dict.update({'lower': df[vals].min() - rel_step - step})

#barplot
if plot == 'barplot':
line = figax.lines[i]
gain = line.get_ydata() if axis=="x" else line.get_xdata()
pos_dict.update({'upper': step + abs(max(gain))})
pos_dict.update({'lower': -step + abs(min(gain))})

#swarmplot
if plot == 'swarmplot':
pos_dict.update({'upper': df[vals].max() + step})
pos_dict.update({'lower': df[vals].min() - step})

xpos = x if axis=="x" else pos_dict[pos]
ypos = y if axis=="y" else pos_dict[pos]
if plot == 'barplot':
mn, mx = 0, max(lims)+lim*step
[figax.set_ylim(mn, mx) if axis=='x' else figax.set_xlim(mn, mx)]

figax.text(xpos, ypos, cld[label.get_text()], size=fs, color=c, ha=ha, va=va,)
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"

[project]
name = "cld4py"
version = "1.0.1"
version = "1.1.0"
authors = [
{ name="Timur Yergaliyev", email="[email protected]" },
]
description = "Python 3 function to assign compact letter display letters"
description = "Python 3 function to assign compact letter display letters and for plotting them with matplotlib/seaborn"
readme = "README.md"
requires-python = ">=3.7"
classifiers = [
Expand Down
54 changes: 0 additions & 54 deletions src/cld4py.egg-info/PKG-INFO

This file was deleted.

8 changes: 0 additions & 8 deletions src/cld4py.egg-info/SOURCES.txt

This file was deleted.

1 change: 0 additions & 1 deletion src/cld4py.egg-info/dependency_links.txt

This file was deleted.

1 change: 0 additions & 1 deletion src/cld4py.egg-info/top_level.txt

This file was deleted.

Empty file removed src/cld4py/__init__.py
Empty file.
54 changes: 0 additions & 54 deletions src/cld4py/cld4py.py

This file was deleted.

0 comments on commit 8472938

Please sign in to comment.