SQL被广泛使用,数据科学家(分析师)通常需要使用SQL来查询和处理日常工作中的数据。许多企业认为,只要IT部门构建数据仓库(数据平台)并提供SQL,数据科学家就可以自由查询和分析企业数据。
这种观点似乎是正确的,因为SQL使数据科学家能够查询和计算数据。此外,SQL非常喜欢英语,似乎很容易开始,并且一些简单的SQL语句甚至可以直接读取为英语。
例如,用于过滤的SQL语句:
Select id,name from T where id=1
如果ID等于1。
另一个示例:用于分组和汇总的SQL语句:
Select area,sum(amount) from T group by area
此陈述也与英语表达式非常相似。
总结数量。看起来像英语(自然语言)具有很大的好处,也就是说,编码很简单。使用自然语言实施数据查询,即使是商业人员(数据科学家通常是熟悉业务但熟练IT技术的人)也可以掌握,这正是设计SQL的最初意图:启用普通业务使用的人员。
那实际上是什么?
如果所有计算任务都像分组和过滤一样简单,那么大多数业务人员确实可以掌握,并且在SQL中编码也很简单。但是,数据科学家面临的业务情况通常并不那么简单,例如:
-
找出基于销售数据的销售额占总销售额的50%和80%的顶级客户,以进行精确营销;
-
分析哪些餐厅最受欢迎,哪个时间段是最繁忙的,哪些菜肴是根据客户数量,消费量,消费时间,消费时间,消费地点以及每个连锁餐厅的其他数据最受欢迎的;
-
根据汽车销售数据计算每个模型的销售量,销量,平均价格和销售区域等设计;
-
根据股票交易数据,发现连续三个交易日(上升速度> = 10%)的股票增加了每日限额,以构建投资组合;
-
根据其市场数据来评估其历史绩效的最大天数继续增加;
-
根据游戏登录数据进行用户分析,列出了用户的第一个登录记录以及上次登录的时间间隔,并在上次登录之前的三天内计数用户数量的登录数; <<<<<<<<<< /p>
-
评估客户是否会根据其帐户的某些数据(例如余额,交易历史记录和信用等级)拖欠贷款,并确定最有可能违约的客户;
-
根据他们的病历,诊断结果和治疗计划确定哪些患者最需要预防和治疗;
-
根据操作员的数据(例如用户的呼叫记录,SMS记录和数据),计算用户每月的平均呼叫持续时间,数据消耗量和高峰消耗时间段,并确定哪些用户是高消费用户消费;
-
基于电子商务用户的行为数据执行漏斗分析,计算每个事件之后的用户流失率,例如页面浏览,添加到购物车,下订单和付款;
li> -
将客户分为不同的群体,例如具有强大购买力的小组,该小组更喜欢女装,该小组喜欢根据电子商务公司的客户数据(例如购买历史记录)和偏好来促进不同的男士服装不同群体的促销活动;
-
, p> li>
这些示例仅说明实际计算任务的一小部分。我们可以从示例中可以看出,大多数具有业务意义的数据分析有些复杂,而不是简单地过滤和分组。对于此类分析,我们在SQL中进行编码并不容易甚至不可能。让我们尝试在SQL中编码几个任务以查看实现的困难。
找出累积销售占总销售额一半的顶级客户,并按销售订购订单:
with A as
(select customer, amount, row_number() over(order by amount) ranking
from orders)
select customer, amount
from (select customer,
amount,
sum(amount) over(order by ranking) cumulative_amount
from A)
where cumulative_amount > (select sum(amount) / 2 from orders)
order by amount desc
找出连续三个交易日的每日限额增加的股票(> = 10%):
with A as
(select code,
trade_date,
close_price / lag(close_price) over(partition by code order by trade_date) - 1 rising_range
from stock_price),
B as
(select code,
Case
when rising_range >= 0.1 and lag(rising_range)
over(partition by code order by trade_date) >= 0.1 and
lag(rising_range, 2)
over(partition by code order by trade_date) >= 0.1 then
1
Else
0
end rising_three_days
from A)
select distinct code from B where rising_three_days = 1
计算某个股票不断上升的最大交易日数:
SELECT max(consecutive_day)
FROM (SELECT count(*) consecutive_day
FROM (SELECT sum(rise_or_fall) OVER(ORDER BY trade_date) day_no_gain
FROM (SELECT trade_date,
CASE
when close_price > lag(close_price)
OVER(ORDER BY trade_date) then
0
Else
1
end rise_or_fall
FROM stock_price))
GROUP BY day_no_gain)
电子商务渠道分析(此代码仅计算三个步骤的用户数:页面浏览,添加到购物车和下达订单):
with e1 as
(select uid, 1 as step1, min(etime) as t1
from event
where etime >= to_date('2021-01-10')
and etime < to_date('2021-01-25')
and eventtype = 'eventtype1'
and …
group by 1),
e2 as
(select uid, 1 as step2, min(e1.t1) as t1, min(e2.etime) as t2
from event as e2
inner join e1
on e2.uid = e1.uid
where e2.etime >= to_date('2021-01-10')
and e2.etime < to_date('2021-01-25')
and e2.etime > t1
and e2.etime < t1 + 7
and eventtype = 'eventtype2'
and …
group by 1),
e3 as
(select uid, 1 as step3, min(e2.t1) as t1, min(e3.etime) as t3
from event as e3
inner join e2
on e3.uid = e2.uid
where e3.etime >= to_date('2021-01-10')
and e3.etime < to_date('2021-01-25')
and e3.etime > t2
and e3.etime < t1 + 7
and eventtype = 'eventtype3'
and …
group by 1)
select sum(step1) as step1, sum(step2) as step2, sum(step3) as step3
from e1
left join e2
on e1.uid = e2.uid
left join e3
on e2.uid = e3.uid
任务毫无例外地嵌套乘数层子子量。尽管某些SQL代码不长,但很难理解(例如计算股票不断上升的天数的示例),更不用说代码了;某些任务是如此困难,几乎不可能编码(例如漏斗分析)。
对于简单的计算,在SQL中代码的代码确实很容易且方便,但是当计算任务变得略有复杂时,这并不容易。但是,实际的计算任务,尤其是数据科学家面临的任务,其中大多数非常复杂。此外,简单的任务根本不需要数据科学家来编码,因为许多BI工具都提供了可直接删除简单查询的视觉界面。因此,我们基本上可以得出结论:
需要数据科学家编写的SQL代码并不简单!
这将导致什么后果?
这将直接导致数据科学家需要花费大量时间和精力来编写复杂的SQL代码,从而导致工作效率较低。简而言之, sql正在消耗数据科学家的生活。
SQL如何消耗数据科学家的生命
遇到复杂任务时难以编码
就像上面给出的SQL代码示例一样,尽管某些代码不长,但很难理解,更难编写。这种现象的原因之一是,英语sql导致逐步计算的困难。
将SQL设计为像英语这样的语言的目的是使商业人员(非技术人员)也可以使用。如前所述,对于简单的计算,确实可以实现此目的。但是,对于像数据科学家这样的专业分析师而言,他们面临的计算情况要复杂得多,此目的将造成困难,而不是在计算任务变得复杂后带来便利。
自然语言的优点之一是它可以模糊地表达,但是SQL需要遵循非常严格的语法,而解释器将拒绝次要的违规语法。结果,它不是像英语一样受益,而是导致严重的缺点。将语法设计为自然语言似乎很容易掌握,但实际上是相反的。
为了使整个SQL语句符合英语习惯,需要添加许多不必要的介词。例如,从作为语句的主要操作元素中,放置在语句末尾;组后添加冗余。
像自然语言一样的主要缺点是程序性特征。我们知道,逐步计算是处理复杂计算的有效方法,几乎所有高级语言都支持此功能。但是,自然语言并非如此,它需要依靠几个代词来维持两个句子之间的关系,但是一些代词不能充分准确地描述这种关系,因此,更常见的做法是放置尽可能多的东西尽可能分为一个句子,在处理复杂情况时,导致大量下属子句出现。当这种做法在SQL中表现出来时,需要将多个操作(例如Select,were and of ofer组)放入一个陈述中。例如,尽管在哪里并且具有相同的含义,但它仍然需要使用两者以显示差异,这将导致一种现象,当查询要求变得复杂时,一个SQL语句会筑巢的多层子查询,这现象将不可避免地给编码和理解带来困难。在实践中也是如此,在这种情况下,分析师面部的复杂SQL语句很少在行中测量,但通常以KBS进行测量。对于相同的100行代码,将其编写为100个语句和一个语句的复杂性是完全不同的。很难理解这种SQL语句。即使程序员付出了巨大的努力来解决问题,他们也可能不知道两个月后的含义。
除了缺乏程序性特征外,在SQL中难以编码的更重要的原因是其理论上的缺陷:50年前出生的关系代数缺乏必要的数据类型和操作,很难支持现代数据分析业务。
虽然SQL系统具有记录数据类型的概念,但它没有明确的记录数据类型。 SQL将把单个记录视为只有一个记录的临时表,即单个记录。缺乏离散性的特征将使数据科学家无法根据自然的思维方式处理分析任务,从而导致了严重的理解和编码困难。
例如,对于上面的漏斗分析案例,尽管CTE语法使SQL具有一定程度的逐步计算能力,但代码仍然非常复杂。具体而言,每个子查询都需要将原始表与以前的子问题的结果相关联,并且很难对这种回旋处的加入操作进行编码,这超出了许多数据科学家的能力。通常,我们只需要按用户分组,然后按时间对组内数据进行分组,最后分别遍历每个数据(用户)。具体步骤取决于实际要求。将符合条件或分组数据的数据视为单独计算的记录(即离散性)可以极大地简化漏斗分析过程。不幸的是,由于缺乏离散性的支持,SQL无法提供此类有序的计算,因此必须反复关联,从而导致编码和运行缓慢。
实际上,这种离散的记录概念在高级语言(例如Java和C ++)中非常普遍,但是SQL不支持此概念。关系代数定义了丰富的设定操作,但离散性较差。结果,SQL很难描述复杂的多步操作(性能也很差)。这种理论缺陷无法通过工程方法来解决。
有关数据类型和操作中SQL缺陷的更多信息,请访问:Why a SQL Statement Often Consists of Hundreds of Lines, Measured by KBs?
难以调试
除了难以编码外,还很难调试SQL代码,这加剧了消耗数据科学家生活的现象。
众所周知,调试SQL代码是困难的。 SQL代码越复杂,调试的困难越难,并且复杂的SQL代码通常是最需要调试的代码,因为正确性应该始终是当务之急。
当我们执行一个长长的SQL代码嵌套子征服并发现结果不正确时,我们应该如何调试?在正常情况下,我们唯一能做的就是将代码分开并按一层执行以确定问题。
但是,当SQL语句太复杂时,此调试方法可能非常耗时且困难,因为该语句中可能存在大量嵌套的子查询和关联查询,而分裂通常不是容易。
尽管许多SQL编辑器提供了交互式开发界面,但它对调试复杂的SQL语句无济于事。此外,调试困难会影响发展效率,从而进一步降低了发展效率。
低性能
除了上述两个缺点外,复杂的SQL代码通常会导致性能低。低性能意味着等待,在一些大数据计算方案中,数据科学家需要等待数小时甚至一天,并且在此过程中消耗了生活。当遇到一个不幸的情况时,数据科学家在等待很长一段时间后发现计算结果不正确时,他们必须重复此过程,这将导致时间成本乘以时间。
为什么复杂的SQL代码缓慢运行?
复杂SQL代码的查询性能主要取决于数据库的优化引擎。一个好的数据库将根据计算目标采用更有效的算法(而不是根据SQL代码的字面表达逻辑执行)。但是,面对复杂情况,自动优化机制通常会失败,而太透明的机制将使我们难以手动干预执行路径,更不用说使SQL执行我们指定的算法。
。让我们以一个简单的例子:以1亿个数据中的前十名,SQL代码:
SELECT TOP 10 x FROM T ORDER BY x DESC
尽管此代码包含与排序相关的单词(顺序),但数据库优化引擎实际上会进行大量分类(大数据排序是一个非常缓慢的动作),并且会选择一个更有效的算法。
如果我们对上述任务进行稍微更改:计算每个组的前10个,则SQL代码:
SELECT * FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY Area ORDER BY Amount
DESC) rn
FROM Orders )
WHERE rn<=10
虽然该代码的复杂性并没有增加太大,但是大多数数据库的优化引擎会感到困惑,并且无法识别其真正的意图,相反,他们必须根据字面表达的逻辑进行分类(仍然存在语句中的单词顺序)。结果,性能急剧下降。
现实世界业务中的SQL代码比此代码复杂得多,并且未能识别代码的真实意图对于数据库优化引擎来说是很常见的。例如,前面提到的漏斗分析的SQL语句需要反复关联,从而导致编码难度,并且执行的极端性能。
当然,使用用户定义的功能(UDF)可以增强SQL的能力,从而使我们能够实现所需的算法。但是,这种方法通常是不现实的。更不用说数据库的存储无法确保算法发生变化时的性能,实施UDF本身的困难超出了绝大多数数据科学家的技术能力。即使付出了很大的努力,UDF也会面临前面提到的复杂性问题,并且通常无法保证性能。
封闭
SQL的缺点不止于此。
sql是数据库的形式语言,但是数据库具有闭合性,这将导致数据处理的困难。所谓的封闭性意味着要通过数据库进行计算和处理的数据必须提前加载到数据库中,并且清楚地定义了数据是在数据库内还是外部。
。实际上,数据分析师通常需要从其他来源处理数据,包括文本,Excel,程序界面和Web爬网等。其中一些数据仅临时使用,如果仅在每次加载到数据库后才可以使用它们,它不仅会占据数据库的空间,而且ETL过程会浪费很多时间。另外,将数据加载到数据库通常受到限制。某些不合格的数据不能写入数据库。在这种情况下,首先需要花费时间和精力来组织数据,然后将有组织的数据写入数据库(将数据写入数据库是耗时的)。一旦浪费时间,生命就浪费了。
当然,除了SQL之外,数据科学家还拥有Java和Python等其他工具。然后,这些工具有效吗?
Java支持程序计算并具有良好的离散性,但其对设定操作的支持很差。 Java缺乏基本的数据类型和计算库,这使数据处理非常麻烦。例如,对于Java而言,在SQL中易于实现的分组和汇总操作并不容易,Java更难实现其他操作,例如过滤,加入和多个混合操作。此外,Java对于数据分析师来说太重了,并且在交互性方面非常差,因此在理论上Java中可以实施任何计算,在实践中也无法使用。
与Java相比,Python好一些。 Python具有更丰富的计算库,并且在实现相同的计算时更简单(其计算能力通常与SQL相当)。但是,对于复杂的计算,它也很麻烦,而Python比SQL没有太大优势。此外,Python的交互性也不好(它仍然需要手动打印和输出中间结果)。而且,由于缺乏真正的并行计算机制和存储保证,Python还面临大数据计算中的性能问题。
它还有其他选择吗?
ESPROC SPL,一种救援数据科学家的工具
对于经常处理结构化数据的数据科学家来说,ESPROC SPL是一种值得添加到其数据分析的工具。
ESPROC是一种专门设计用于处理结构化数据的工具,其正式语言SPL(结构化过程语言)具有与SQL和Java完全不同的数据处理能力。
编码更简单
首先,让我们看一下SPL与SQL在完成上述任务时与SQL的不同之处:
找出累积销售占总销售额一半的顶级客户,并按销售订购订单:
A
1 =db.query(“select * from orders”).sort(amount:-1)
2 =A1.cumulate(amount) Get a sequence of cumulative amounts
3 =A2.m(-1)/2 Calculate the final cumulative amount, i.e., the total
4 =A2.pselect(~>=A3) Find the position where the amount exceeds half of the total
5 =A1(to(A4))
计算股票保持上升的最大天数:
A
1 =stock.sort(tradeDate)
2 =0
3 =A1.max(A2=if(closePrice>closePrice[-1],A2+1,0))
找出连续三个交易日的每日限额增加的股票(> = 10%):
A B C D
1 =db.query(“select * from stock_price”).group(code).(~.sort(trade_date))
=[] Result set is in C1
2 for A1 =0
3
if A2.pselect(B2=if(close_price/close_price[-1]>=1.1,B2+1,0):3)>0
Get stocks that end limit-up in three consecutive trading days
4
>C1=C1|A2.code
电子商务业务的渠道分析:
A
1 =["etype1","etype2","etype3"]
2 =file("event.ctx").open()
3 =A2.cursor(id,etime,etype;etime>=date("2021-01-10") && etime<date("2021-01-25") && A1.contain(etype) && …)
4 =A3.group(uid).(~.sort(etime))
5 =A4.new(~.select@1(etype==A1(1)):first,~:all).select(first)
6 =A5.(A1.(t=if(#==1,t1=first.etime,if(t,all.select@1(etype==A1.~ && etime>t && etime<t1+7).etime, null))))
7 =A6.groups(;count(~(1)):STEP1,count(~(2)):STEP2,count(~(3)):STEP3)
从上面的SPL代码中,我们可以看到SPL比SQL更简单。即使是那些不知道SPL语法的人,基本上也可以理解这些SPL代码。如果他们熟悉SPL语法,那么实施这些计算就不困难了。
SPL更简单的原因是自然支持程序计算。
如前所述,程序计算可以有效地减少复杂业务的实施难度,并且提高发展效率可以帮助数据科学家创造更多的价值。尽管CTE语法和存储过程使SQL具有一定程度的程序计算能力,但它远非足够。相反,SPL自然支持程序计算,并可以将复杂的计算分为多个步骤,从而降低实施难度。
例如,对于股票不断上升的天数的计算,SPL允许我们根据自然的思想列车来计算:先按交易日进行排序,然后将当天的收盘价与前一天(如果比较结果大于1,则在中间变量的帮助下累积,否则将重置为零),最后在序列中找到最大值,即我们想要的值。整个计算过程不需要嵌套,并且可以根据自然思维轻松地逐步实现,这是程序计算带来的好处。同样,对于漏斗分析,通过逐步计算来减少实现难度,并且代码更为通用,并且可以使用任何数量的步骤处理漏斗计算(唯一需要做的就是修改参数)。<<<<<<<<<<<< /p>
SPL更简单的另一个原因是提供了更丰富的数据类型和计算库,可以进一步简化计算。
SPL提供了一个专业的结构化数据对象:表序列,并根据表序列提供丰富的计算库,从而使SPL具有完整而简单的结构化数据过程。
以下是SPL中常规计算的一部分:
Orders.sort(Amount) // sort
Orders.select(Amount*Quantity>3000 && like(Client,"*S*")) // filter
Orders.groups(Client; sum(Amount)) // group
Orders.id(Client) // distinct
join(Orders:o,SellerId ; Employees:e,EId) // join
通过程序计算和表顺序,SPL可以实施更多的计算。例如,SPL支持有序操作更直接,更彻底。在上述SPL代码中,用于计算股票上涨天数的数量,它使用[-1]引用先前的记录以比较股票价格。如果我们想计算移动平均值,我们可以编写AVG(价格[-1:1])。通过有序操作,可以通过这种方式编码股票的最大天数计算:
stock.sort(trife_date).group@i(colled_price
对于分组操作,SPL可以保留分组的子集,即集合集,这使得在分组结果上执行进一步的操作变得方便。相比之下,SQL没有明确的集合数据类型,也无法返回数据类型,例如集合集。由于SQL无法实施独立的分组,因此必须整体绑定分组和汇总。
此外,SPL对聚合操作有了新的了解。除了常见的单个值之类的总和,计数,最大和最小值外,聚合结果还可以是集合。例如,SPL将公共顶部视为诸如和计数之类的聚合计算,可以在整个集合或分组的子集上执行。
。实际上,SPL具有许多其他功能,比SQL比Java/Python更完整。例如,离散性允许构成数据表的记录分离并反复计算。 通用集支持由任何数据组成的集合,并允许该集合参与计算; 加入操作区分三种不同类型的连接,使我们可以根据实际情况选择合适的连接...
这些功能使数据科学家能够更简单有效地处理数据,从而结束了浪费生命。
易于编辑和调试
影响发展效率的另一个因素是调试环境。如何更方便地调试代码并与数据科学家进行互动也是SPL的关键考虑因素。为此,SPL提供了一个独立的IDE:
与使用文本编程的其他编程语言不同,SPL采用网格式代码进行编程。网格式代码具有一些自然优势,主要反映在三个方面。首先,编码时无需定义变量。通过直接在随后的步骤中引用先前单元格(例如A1)的名称,我们可以利用单元格的计算结果。这样,可以避免将我们的大脑置于命名变量的名称。当然,SPL还支持定义变量。其次,网格风格的代码看起来非常整洁。即使单元格中的代码很长,它也只会占据一个单元格,不会影响整个代码的结构,从而使代码更方便地读取;第三,SPL的IDE提供了多种调试方式,例如运行,调试,运行到光标。简而言之,易于使用的编辑和调试功能提高了编码效率。
此外,在IDE的右侧,有一个结果面板,可以实时显示每个单元格的计算结果。实时查看每个步骤的结果进一步改善了调试的便利。借助此功能,数据科学家不仅可以轻松地实施常规数据分析,而且还可以进行交互式分析,并根据上一步的结果立即决定下一步做什么。此外,此功能使数据科学家可以方便地回顾一定的中间步骤的结果。
高性能
支持程序计算并提供丰富的计算库可以使SPL快速完成数据分析任务,并且其易于使用的IDE进一步提高了开发效率。此外,SPL的性能呢?毕竟,计算性能对于数据科学家也至关重要。
为了应对数据量超过内存容量的大数据计算方案,SPL提供了光标计算方法。
=file("orders.txt").cursor@t(area,amount).groups(area;sum(amount):amount)
此外,SPL为内存和外部存储计算提供并行计算支持。通过仅添加一个@M选项,可以实现并行计算,并且可以完全利用多核CPU的优势,这非常方便。
=file("orders.txt").cursor@tm(area,amount;4).groups(area;sum(amount):amount)
除了光标和并行计算外,SPL还提供许多内置的高性能算法。例如,在SPL将前面提到的TOPN视为普通的聚合操作之后,在相应的语句中避免了排序操作,因此执行更有效。
同样,SPL提供了许多这样的高性能算法,包括:
-
内存计算:二进制搜索,序列编号定位,位置索引,哈希索引,多层序列编号定位...
-
外部存储搜索:二进制搜索,哈希索引,排序索引,索引 - 值,全文检索...
-
遍历计算:延迟光标,多功能遍历,并行多弯曲,有序分组和聚合,序列编号分组...
-
外国密钥协会:外键通讯,外键序列编号,索引重复使用,对齐顺序,一侧分区...
-
合并并加入:订购合并,通过细分合并,关联定位,附加表...
-
多维分析:部分预处理,时间段预聚集,冗余排序,布尔维度序列,标签位尺寸...
-
集群计算:群集多区域复合表,重复尺寸表,分段尺寸表,冗余 - 图案 - 模式容忍度和备用轮毂模式的容错,负载平衡...
为了全面发挥高性能算法的有效性,SPL还设计了高性能文件存储,并采用了多种性能保证机制,例如代码压缩,柱状存储,索引和细分。一旦获得了灵活,有效的存储,数据科学家就可以根据要进行的计算以及要处理的数据的特征设计数据存储表(例如排序,索引和附件表),并根据存储采用更有效的算法形式以获得极端的性能体验。节省时间是挽救生命。
开放
与需要在计算之前将数据加载到数据库中的数据库不同,SPL可以直接计算在面对不同的数据源时,因此它具有良好的开放性。
Spl没有传统数据仓库基础的概念,也没有它具有元数据的概念,更不用说约束了。任何可访问的数据源都可以视为SPL的数据,并且可以直接计算。计算之前不需要将数据导入数据库,并且在计算后也不需要故意将数据库导出数据库,可以通过接口将计算结果写入目标数据源。
SPL封装了常见数据源的访问接口,例如各种关系数据库(JDBC数据源),MongoDB,HBASE,HDFS,HTTP/HTTP/RESTFUL,Salesforces,Salesforces和SAP BW。从逻辑上讲,这些数据源基本上具有相同的状态,并且可以在访问后分别或以混合方式进行计算,而唯一的区别是不同的数据源具有不同的访问接口,并且不同的接口具有不同的性能。
。在开放性的支持下,数据科学家可以直接,快速处理各种来源的数据,节省数据组织上的时间,从数据库中导入和导出,并提高数据处理效率。
总体而言,SPL为数据科学家提供了全面的结构化数据处理能力,而结构化数据当前是数据分析的首要任务。使用SPL,不仅可以提高分析效率,而且还可以充分保证性能。只有使用一个简单的编码工具,快速运行,开放性和互动性良好,不会浪费数据科学家的生活。
SPL源代码:https://github.com/SPLWare/esProc
来源:https://blog.scudata.com/sql-is-consuming-the-lives-of-data-scientists/