熊猫的concat函数中的“级别”,“键”和名称参数是什么?


93

问题

  • 如何使用pd.concat
  • 什么是levels对的说法?
  • 什么是keys对的说法?
  • 有大量示例可以帮助解释如何使用所有参数吗?

熊猫的concat职能是合并后的公用事业公司的瑞士军刀。有用的情况多种多样。现有文档在一些可选参数上省略了一些细节。其中有levelskeys参数。我着手弄清楚这些论点的作用。

我将提出一个问题,将其作为的许多方面的门户pd.concat

考虑数据帧d1d2以及d3

import pandas as pd

d1 = pd.DataFrame(dict(A=.1, B=.2, C=.3), [2, 3])
d2 = pd.DataFrame(dict(B=.4, C=.5, D=.6), [1, 2])
d3 = pd.DataFrame(dict(A=.7, B=.8, D=.9), [1, 3])

如果我将这些与

pd.concat([d1, d2, d3], keys=['d1', 'd2', 'd3'])

pandas.MultiIndex对于我的columns对象,我得到了预期的结果:

        A    B    C    D
d1 2  0.1  0.2  0.3  NaN
   3  0.1  0.2  0.3  NaN
d2 1  NaN  0.4  0.5  0.6
   2  NaN  0.4  0.5  0.6
d3 1  0.7  0.8  NaN  0.9
   3  0.7  0.8  NaN  0.9

但是,我想使用levels参数文档

级别:序列列表,默认为无。用于构造MultiIndex的特定级别(唯一值)。否则,将从键中推断出它们。

所以我通过了

pd.concat([d1, d2, d3], keys=['d1', 'd2', 'd3'], levels=[['d1', 'd2']])

并得到一个 KeyError

ValueError: Key d3 not in level Index(['d1', 'd2'], dtype='object')

这很有道理。我通过的级别不足以描述按键指示的必要级别。如果没有像上面那样通过任何操作,则可以推断出级别(如文档中所述)。但是,我还能如何使用该参数来获得更好的效果?

如果我尝试这样做:

pd.concat([d1, d2, d3], keys=['d1', 'd2', 'd3'], levels=[['d1', 'd2', 'd3']])

我并获得了与上述相同的结果。但是当我在水平上再增加一个值时,

df = pd.concat([d1, d2, d3], keys=['d1', 'd2', 'd3'], levels=[['d1', 'd2', 'd3', 'd4']])

我最终得到了相同外观的数据帧,但是结果MultiIndex具有未使用的级别。

df.index.levels[0]

Index(['d1', 'd2', 'd3', 'd4'], dtype='object')

那么,重点是什么 level争论,我应该以keys不同的方式使用吗?

我正在使用Python 3.6和Pandas 0.22。

Answers:


122

在为我自己回答这个问题的过程中,我学到了很多东西,并且我想将示例和一些解释汇总到一起。

关于levels论点的具体答案即将结束。

pandas.concat:缺少手册

链接到当前文档

导入和定义对象

import pandas as pd

d1 = pd.DataFrame(dict(A=.1, B=.2, C=.3), index=[2, 3])
d2 = pd.DataFrame(dict(B=.4, C=.5, D=.6), index=[1, 2])
d3 = pd.DataFrame(dict(A=.7, B=.8, D=.9), index=[1, 3])

s1 = pd.Series([1, 2], index=[2, 3])
s2 = pd.Series([3, 4], index=[1, 2])
s3 = pd.Series([5, 6], index=[1, 3])

争论

objs

我们遇到的第一个参数是objs

objs:Series,DataFrame或Panel对象的序列或映射。如果传递了dict,则除非传递了排序的键,否则它将用作keys参数,在这种情况下,将选择值(请参见下文)。除非所有对象都为None,否则所有None对象都将被静默删除,在这种情况下,将引发ValueError

  • 我们通常将其与SeriesDataFrame对象列表一起使用。
  • 我将展示它dict也非常有用。
  • 生成器也可以使用,并且map在按map(f, list_of_df)

现在,我们将坚持上面定义的DataFrameSeries对象的列表。稍后我将展示如何利用字典来给出非常有用的MultiIndex结果。

pd.concat([d1, d2])

     A    B    C    D
2  0.1  0.2  0.3  NaN
3  0.1  0.2  0.3  NaN
1  NaN  0.4  0.5  0.6
2  NaN  0.4  0.5  0.6

axis

我们遇到的第二个参数axis的默认值为0

axis:{0 /'index',1 /'columns'},默认值为0。

2 DataFrames与axis=0(堆叠)

对于0或的值,index我们要说:“沿列对齐并添加到索引”。

如上所示axis=0,因为0是默认值,所以使用了,因为它的索引d2扩展了的索引,d1尽管该值存在重叠2

pd.concat([d1, d2], axis=0)

     A    B    C    D
2  0.1  0.2  0.3  NaN
3  0.1  0.2  0.3  NaN
1  NaN  0.4  0.5  0.6
2  NaN  0.4  0.5  0.6

两个DataFrames的axis=1(并排)

对于值,1或者columns我们要说:“沿索引对齐并添加到列中”,

pd.concat([d1, d2], axis=1)

     A    B    C    B    C    D
1  NaN  NaN  NaN  0.4  0.5  0.6
2  0.1  0.2  0.3  0.4  0.5  0.6
3  0.1  0.2  0.3  NaN  NaN  NaN

我们可以看到,最终得到的指数是指数的工会所得列列从延伸d1通过的列d2

两个(或三个)Seriesaxis=0(堆叠)

当合并pandas.Seriesaxis=0,我们得到一个pandas.Series。除非所有合并的结果具有相同的名称,否则结果的名称Series将为。当我们打印出结果时要注意。如果不存在,我们可以假设名称为。NoneSeries'Name: A'SeriesSeriesNone

               |                       |                        |  pd.concat(
               |  pd.concat(           |  pd.concat(            |      [s1.rename('A'),
 pd.concat(    |      [s1.rename('A'), |      [s1.rename('A'),  |       s2.rename('B'),
     [s1, s2]) |       s2])            |       s2.rename('A')]) |       s3.rename('A')])
-------------- | --------------------- | ---------------------- | ----------------------
2    1         | 2    1                | 2    1                 | 2    1
3    2         | 3    2                | 3    2                 | 3    2
1    3         | 1    3                | 1    3                 | 1    3
2    4         | 2    4                | 2    4                 | 2    4
dtype: int64   | dtype: int64          | Name: A, dtype: int64  | 1    5
               |                       |                        | 3    6
               |                       |                        | dtype: int64

两个(或三个)Seriesaxis=1(并排)

当结合在一起pandas.Seriesaxis=1,它是name我们引用的属性,以推断结果中的列名称pandas.DataFrame

                       |                       |  pd.concat(
                       |  pd.concat(           |      [s1.rename('X'),
 pd.concat(            |      [s1.rename('X'), |       s2.rename('Y'),
     [s1, s2], axis=1) |       s2], axis=1)    |       s3.rename('Z')], axis=1)
---------------------- | --------------------- | ------------------------------
     0    1            |      X    0           |      X    Y    Z
1  NaN  3.0            | 1  NaN  3.0           | 1  NaN  3.0  5.0
2  1.0  4.0            | 2  1.0  4.0           | 2  1.0  4.0  NaN
3  2.0  NaN            | 3  2.0  NaN           | 3  2.0  NaN  6.0

混合SeriesDataFrameaxis=0(堆叠)

当执行的串联SeriesDataFrame一起axis=0,我们把所有的Series单柱DataFrame秒。

要特别注意这是串联axis=0; 这意味着在对齐列时扩展索引(行)。在下面的示例中,我们看到索引变成[2, 3, 2, 3]了一个随意添加的索引。除非我强制Series使用以下参数来命名列,否则列不会重叠to_frame

 pd.concat(               |
     [s1.to_frame(), d1]) |  pd.concat([s1, d1])
------------------------- | ---------------------
     0    A    B    C     |      0    A    B    C
2  1.0  NaN  NaN  NaN     | 2  1.0  NaN  NaN  NaN
3  2.0  NaN  NaN  NaN     | 3  2.0  NaN  NaN  NaN
2  NaN  0.1  0.2  0.3     | 2  NaN  0.1  0.2  0.3
3  NaN  0.1  0.2  0.3     | 3  NaN  0.1  0.2  0.3

您可以看到的结果与pd.concat([s1, d1])我对to_frame自己的表现一样。

但是,我可以使用参数来控制结果列的名称to_frameSeriesrename方法重命名不会控制结果中的列名称DataFrame

 # Effectively renames       |                            |
 # `s1` but does not align   |  # Does not rename.  So    |  # Renames to something
 # with columns in `d1`      |  # Pandas defaults to `0`  |  # that does align with `d1`
 pd.concat(                  |  pd.concat(                |  pd.concat(
     [s1.to_frame('X'), d1]) |      [s1.rename('X'), d1]) |      [s1.to_frame('B'), d1])
---------------------------- | -------------------------- | ----------------------------
     A    B    C    X        |      0    A    B    C      |      A    B    C
2  NaN  NaN  NaN  1.0        | 2  1.0  NaN  NaN  NaN      | 2  NaN  1.0  NaN
3  NaN  NaN  NaN  2.0        | 3  2.0  NaN  NaN  NaN      | 3  NaN  2.0  NaN
2  0.1  0.2  0.3  NaN        | 2  NaN  0.1  0.2  0.3      | 2  0.1  0.2  0.3
3  0.1  0.2  0.3  NaN        | 3  NaN  0.1  0.2  0.3      | 3  0.1  0.2  0.3

混合SeriesDataFrameaxis=1(并排)

这是相当直观的。当属性不可用时,Series列名默认为此类Series对象的枚举name

                    |  pd.concat(
 pd.concat(         |      [s1.rename('X'),
     [s1, d1],      |       s2, s3, d1],
     axis=1)        |      axis=1)
------------------- | -------------------------------
   0    A    B    C |      X    0    1    A    B    C
2  1  0.1  0.2  0.3 | 1  NaN  3.0  5.0  NaN  NaN  NaN
3  2  0.1  0.2  0.3 | 2  1.0  4.0  NaN  0.1  0.2  0.3
                    | 3  2.0  NaN  6.0  0.1  0.2  0.3

join

第三个参数join描述生成的合并应该是外部合并(默认)还是内部合并。

join:{'inner','outer'},默认为'outer'
如何处理其他轴上的索引。

事实证明,没有leftright选项pd.concat可以处理多个合并的对象。

对于d1d2,选项如下:

outer

pd.concat([d1, d2], axis=1, join='outer')

     A    B    C    B    C    D
1  NaN  NaN  NaN  0.4  0.5  0.6
2  0.1  0.2  0.3  0.4  0.5  0.6
3  0.1  0.2  0.3  NaN  NaN  NaN

inner

pd.concat([d1, d2], axis=1, join='inner')

     A    B    C    B    C    D
2  0.1  0.2  0.3  0.4  0.5  0.6

join_axes

第四个论点是允许我们进行left合并以及更多工作的事物。

join_axes:索引对象的列表
用于其他n-1轴而不是执行内部/外部设置逻辑的特定索引。

左合并

pd.concat([d1, d2, d3], axis=1, join_axes=[d1.index])

     A    B    C    B    C    D    A    B    D
2  0.1  0.2  0.3  0.4  0.5  0.6  NaN  NaN  NaN
3  0.1  0.2  0.3  NaN  NaN  NaN  0.7  0.8  0.9

右合并

pd.concat([d1, d2, d3], axis=1, join_axes=[d3.index])

     A    B    C    B    C    D    A    B    D
1  NaN  NaN  NaN  0.4  0.5  0.6  0.7  0.8  0.9
3  0.1  0.2  0.3  NaN  NaN  NaN  0.7  0.8  0.9

ignore_index

ignore_index:布尔值,默认为False
如果为True,则不要沿串联轴使用索引值。结果轴将标记为0,...,n-1。如果要串联对象时,串联轴没有有意义的索引信息,这将很有用。请注意,联接中仍会考虑其他轴上的索引值。

就像当我堆叠d1在上时d2,如果我不在乎索引值,则可以重置它们或忽略它们。

                      |  pd.concat(             |  pd.concat(
                      |      [d1, d2],          |      [d1, d2]
 pd.concat([d1, d2])  |      ignore_index=True) |  ).reset_index(drop=True)
--------------------- | ----------------------- | -------------------------
     A    B    C    D |      A    B    C    D   |      A    B    C    D
2  0.1  0.2  0.3  NaN | 0  0.1  0.2  0.3  NaN   | 0  0.1  0.2  0.3  NaN
3  0.1  0.2  0.3  NaN | 1  0.1  0.2  0.3  NaN   | 1  0.1  0.2  0.3  NaN
1  NaN  0.4  0.5  0.6 | 2  NaN  0.4  0.5  0.6   | 2  NaN  0.4  0.5  0.6
2  NaN  0.4  0.5  0.6 | 3  NaN  0.4  0.5  0.6   | 3  NaN  0.4  0.5  0.6

当使用时axis=1

                                   |     pd.concat(
                                   |         [d1, d2], axis=1,
 pd.concat([d1, d2], axis=1)       |         ignore_index=True)
-------------------------------    |    -------------------------------
     A    B    C    B    C    D    |         0    1    2    3    4    5
1  NaN  NaN  NaN  0.4  0.5  0.6    |    1  NaN  NaN  NaN  0.4  0.5  0.6
2  0.1  0.2  0.3  0.4  0.5  0.6    |    2  0.1  0.2  0.3  0.4  0.5  0.6
3  0.1  0.2  0.3  NaN  NaN  NaN    |    3  0.1  0.2  0.3  NaN  NaN  NaN

keys

我们可以传递标量值或元组的列表,以便将元组或标量值分配给相应的MultiIndex。传递的列表的长度必须与要串联的项目数相同。

keys:序列,默认值None
如果通过多个级别,则应包含元组。使用传递的键作为最外层来构造层次结构索引

axis=0

Series沿着axis=0(并扩展索引)串联对象时。

这些键成为MultiIndexindex属性中对象的新初始级别。

 #           length 3             length 3           #         length 2        length 2
 #          /--------\         /-----------\         #          /----\         /------\
 pd.concat([s1, s2, s3], keys=['A', 'B', 'C'])       pd.concat([s1, s2], keys=['A', 'B'])
----------------------------------------------      -------------------------------------
A  2    1                                           A  2    1
   3    2                                              3    2
B  1    3                                           B  1    3
   2    4                                              2    4
C  1    5                                           dtype: int64
   3    6
dtype: int64

但是,我们可以在keys参数中使用多个标量值来创建更深的MultiIndex。在这里,我们通过tuples长度为2的a的两个新级别MultiIndex

 pd.concat(
     [s1, s2, s3],
     keys=[('A', 'X'), ('A', 'Y'), ('B', 'X')])
-----------------------------------------------
A  X  2    1
      3    2
   Y  1    3
      2    4
B  X  1    5
      3    6
dtype: int64

axis=1

沿列延伸时有点不同。当我们使用axis=0(见上文)时,除现有索引外,我们keys还充当MultiIndex级别。对于axis=1,我们指的是Series对象没有的轴,即columns属性。

两个Series重量的变化axis=1

请注意,只要没有通过,就命名s1and和s2事项keys,但如果通过,它将被覆盖keys

               |                       |                        |  pd.concat(
               |  pd.concat(           |  pd.concat(            |      [s1.rename('U'),
 pd.concat(    |      [s1, s2],        |      [s1.rename('U'),  |       s2.rename('V')],
     [s1, s2], |      axis=1,          |       s2.rename('V')], |       axis=1,
     axis=1)   |      keys=['X', 'Y']) |       axis=1)          |       keys=['X', 'Y'])
-------------- | --------------------- | ---------------------- | ----------------------
     0    1    |      X    Y           |      U    V            |      X    Y
1  NaN  3.0    | 1  NaN  3.0           | 1  NaN  3.0            | 1  NaN  3.0
2  1.0  4.0    | 2  1.0  4.0           | 2  1.0  4.0            | 2  1.0  4.0
3  2.0  NaN    | 3  2.0  NaN           | 3  2.0  NaN            | 3  2.0  NaN
MultiIndexSeriesaxis=1
 pd.concat(
     [s1, s2],
     axis=1,
     keys=[('W', 'X'), ('W', 'Y')])
-----------------------------------
     W
     X    Y
1  NaN  3.0
2  1.0  4.0
3  2.0  NaN
两个DataFrame一起axis=1

axis=0示例一样,keys将级别添加到中MultiIndex,但这一次添加到columns属性中存储的对象中。

 pd.concat(                     |  pd.concat(
     [d1, d2],                  |      [d1, d2],
     axis=1,                    |      axis=1,
     keys=['X', 'Y'])           |      keys=[('First', 'X'), ('Second', 'X')])
------------------------------- | --------------------------------------------
     X              Y           |   First           Second
     A    B    C    B    C    D |       X                X
1  NaN  NaN  NaN  0.4  0.5  0.6 |       A    B    C      B    C    D
2  0.1  0.2  0.3  0.4  0.5  0.6 | 1   NaN  NaN  NaN    0.4  0.5  0.6
3  0.1  0.2  0.3  NaN  NaN  NaN | 2   0.1  0.2  0.3    0.4  0.5  0.6
                                | 3   0.1  0.2  0.3    NaN  NaN  NaN
SeriesDataFrameaxis=1

这很棘手。在这种情况下,标量密钥值不能充当索引为唯一的水平Series时,它成为一列,同时还充当的第一级对象MultiIndexDataFrame。因此,Pandas将再次使用对象的name属性Series作为列名称的来源。

 pd.concat(           |  pd.concat(
     [s1, d1],        |      [s1.rename('Z'), d1],
     axis=1,          |      axis=1,
     keys=['X', 'Y']) |      keys=['X', 'Y'])
--------------------- | --------------------------
   X    Y             |    X    Y
   0    A    B    C   |    Z    A    B    C
2  1  0.1  0.2  0.3   | 2  1  0.1  0.2  0.3
3  2  0.1  0.2  0.3   | 3  2  0.1  0.2  0.3
局限keysMultiIndex自卑。

熊猫似乎只是从Series名称推断出列名,但是当在具有不同列级别数的数据帧之间进行类似串联时,Pandas 不会填空。

d1_ = pd.concat(
    [d1], axis=1,
    keys=['One'])
d1_

   One
     A    B    C
2  0.1  0.2  0.3
3  0.1  0.2  0.3

然后将其与仅在列对象中具有一个级别的另一个数据帧连接起来,Pandas将拒绝尝试创建该MultiIndex对象的元组,并组合所有数据帧,就好像对象,标量和元组的单个级别一样。

pd.concat([d1_, d2], axis=1)

   (One, A)  (One, B)  (One, C)    B    C    D
1       NaN       NaN       NaN  0.4  0.5  0.6
2       0.1       0.2       0.3  0.4  0.5  0.6
3       0.1       0.2       0.3  NaN  NaN  NaN

通过dict而不是list

传递字典时,pandas.concat将使用字典中的键作为keys参数。

 # axis=0               |  # axis=1
 pd.concat(             |  pd.concat(
     {0: d1, 1: d2})    |      {0: d1, 1: d2}, axis=1)
----------------------- | -------------------------------
       A    B    C    D |      0              1
0 2  0.1  0.2  0.3  NaN |      A    B    C    B    C    D
  3  0.1  0.2  0.3  NaN | 1  NaN  NaN  NaN  0.4  0.5  0.6
1 1  NaN  0.4  0.5  0.6 | 2  0.1  0.2  0.3  0.4  0.5  0.6
  2  NaN  0.4  0.5  0.6 | 3  0.1  0.2  0.3  NaN  NaN  NaN

levels

它与keys参数一起使用。当levels保留默认值时None,Pandas将获取结果每个级别的唯一值MultiIndex,并将其用作结果index.levels属性中的对象。

级别:序列列表,默认值无无
用于构造MultiIndex的特定级别(唯一值)。否则,将从按键推断出它们。

如果熊猫已经推断出这些水平应该是多少,那么自己指定它有什么优势?我将举一个例子,让您自己思考可能有用的其他原因。

根据文档,levels参数是序列的列表。这意味着我们可以将另一个pandas.Index用作这些序列之一。

考虑该数据帧df是的级联d1d2并且d3

df = pd.concat(
    [d1, d2, d3], axis=1,
    keys=['First', 'Second', 'Fourth'])

df

  First           Second           Fourth
      A    B    C      B    C    D      A    B    D
1   NaN  NaN  NaN    0.4  0.5  0.6    0.7  0.8  0.9
2   0.1  0.2  0.3    0.4  0.5  0.6    NaN  NaN  NaN
3   0.1  0.2  0.3    NaN  NaN  NaN    0.7  0.8  0.9

列对象的级别为:

print(df, *df.columns.levels, sep='\n')

Index(['First', 'Second', 'Fourth'], dtype='object')
Index(['A', 'B', 'C', 'D'], dtype='object')

如果sum在a内使用,groupby则会得到:

df.groupby(axis=1, level=0).sum()

   First  Fourth  Second
1    0.0     2.4     1.5
2    0.6     0.0     1.5
3    0.6     2.4     0.0

但是,如果没有['First', 'Second', 'Fourth']另一个名为Third和的缺失类别Fifth怎么办?我想将它们包括在groupby汇总结果中吗?如果我们有一个,我们可以做到这一点pandas.CategoricalIndex。我们可以提前使用levels参数指定该值。

因此,让我们定义df为:

cats = ['First', 'Second', 'Third', 'Fourth', 'Fifth']
lvl = pd.CategoricalIndex(cats, categories=cats, ordered=True)

df = pd.concat(
    [d1, d2, d3], axis=1,
    keys=['First', 'Second', 'Fourth'],
    levels=[lvl]
)

df

   First  Fourth  Second
1    0.0     2.4     1.5
2    0.6     0.0     1.5
3    0.6     2.4     0.0

但是column对象的第一级是:

df.columns.levels[0]

CategoricalIndex(
    ['First', 'Second', 'Third', 'Fourth', 'Fifth'],
    categories=['First', 'Second', 'Third', 'Fourth', 'Fifth'],
    ordered=True, dtype='category')

我们的groupby总和如下所示:

df.groupby(axis=1, level=0).sum()

   First  Second  Third  Fourth  Fifth
1    0.0     1.5    0.0     2.4    0.0
2    0.6     1.5    0.0     0.0    0.0
3    0.6     0.0    0.0     2.4    0.0

names

这用于命名结果的级别MultiIndexnames列表的长度应与结果中的级别数匹配MultiIndex

names:列表,默认值无
结果级索引中的级别名称

 # axis=0                     |  # axis=1
 pd.concat(                   |  pd.concat(
     [d1, d2],                |      [d1, d2],
     keys=[0, 1],             |      axis=1, keys=[0, 1],
     names=['lvl0', 'lvl1'])  |      names=['lvl0', 'lvl1'])
----------------------------- | ----------------------------------
             A    B    C    D | lvl0    0              1
lvl0 lvl1                     | lvl1    A    B    C    B    C    D
0    2     0.1  0.2  0.3  NaN | 1     NaN  NaN  NaN  0.4  0.5  0.6
     3     0.1  0.2  0.3  NaN | 2     0.1  0.2  0.3  0.4  0.5  0.6
1    1     NaN  0.4  0.5  0.6 | 3     0.1  0.2  0.3  NaN  NaN  NaN
     2     NaN  0.4  0.5  0.6 |

verify_integrity

自我说明文件

verify_integrity:布尔值,默认为False
检查新的串联轴是否包含重复项。相对于实际数据串联而言,这可能会非常昂贵。

因为从串联结果索引d1d2不唯一,它会失败的完整性检查。

pd.concat([d1, d2])

     A    B    C    D
2  0.1  0.2  0.3  NaN
3  0.1  0.2  0.3  NaN
1  NaN  0.4  0.5  0.6
2  NaN  0.4  0.5  0.6

pd.concat([d1, d2], verify_integrity=True)

> ValueError:索引的值重叠:[2]


23
对于社区来说,简单地执行拉取请求以将一些缺少的示例(仅几个)添加到主要文档中,这实际上对社区有用得多;这样只能搜索而不能浏览; 进一步把一个链接到文档会很有用在这里-在绝大多数情况已经很好,完全记录
杰夫

6
@Jeff我成长的某些方面进展缓慢。使用git是其中之一。我保证那是我要开始做的。
piRSquared'Apr 3'18

使用pd.concat(..., levels=[lvl]).groupby(axis=1, level=0).sum()会产生不同于的结果pd.concat(..., levels=[cats]).groupby(axis=1, level=0).sum()。你知道为什么吗?文档只说levels应该是序列列表。
unutbu

1
很好的答案,但是我认为上一节Passing a dict instead of a list需要使用字典而不是列表的示例。
unutbu

1
@unutbu我已经修复了dict示例,谢谢。原因是lvl分类索引,cats仅是列表。当按类别类型分组时,在适当的地方,缺失的类别会用零和零填充。 看到这个
piRSquared
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.